You've already forked AstralRinth
forked from didirus/AstralRinth
feat: more o11y for i18n pojo (#5148)
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
import { defineNuxtPlugin } from '#imports'
|
||||
|
||||
export default defineNuxtPlugin((nuxt) => {
|
||||
if (import.meta.server) {
|
||||
nuxt.hooks.hook('app:rendered', (ctx) => {
|
||||
if (ctx.ssrContext?.payload?.data) {
|
||||
const check = (obj: any, path = 'payload') => {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
if (
|
||||
obj.constructor &&
|
||||
obj.constructor.name !== 'Object' &&
|
||||
obj.constructor.name !== 'Array'
|
||||
) {
|
||||
console.error(`Non-POJO at ${path}:`, obj.constructor.name)
|
||||
}
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
check(v, `${path}.${k}`)
|
||||
}
|
||||
}
|
||||
check(ctx.ssrContext.payload.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -61,22 +61,26 @@ export default defineNuxtPlugin({
|
||||
name: 'i18n',
|
||||
enforce: 'pre',
|
||||
async setup(nuxtApp) {
|
||||
// ONLY locale needs request-scoping (what language this request uses)
|
||||
const locale = useState<string>('i18n-locale', () => DEFAULT_LOCALE)
|
||||
|
||||
function t(key: string, values?: Record<string, unknown>): string {
|
||||
const msg = messageCache.get(locale.value)?.[key] ?? messageCache.get(DEFAULT_LOCALE)?.[key]
|
||||
const currentLocale = locale.value
|
||||
const msg = messageCache.get(currentLocale)?.[key] ?? messageCache.get(DEFAULT_LOCALE)?.[key]
|
||||
if (!msg) return key
|
||||
if (!values || Object.keys(values).length === 0) return msg
|
||||
|
||||
const cacheKey = `${locale.value}:${msg}`
|
||||
const cacheKey = `${currentLocale}:${msg}`
|
||||
let formatter = formatterCache.get(cacheKey)
|
||||
if (!formatter) {
|
||||
formatter = new IntlMessageFormat(msg, locale.value)
|
||||
formatter = new IntlMessageFormat(msg, currentLocale)
|
||||
formatterCache.set(cacheKey, formatter)
|
||||
}
|
||||
try {
|
||||
return formatter.format(values) as string
|
||||
const result = formatter.format(values) as string
|
||||
if (import.meta.dev && typeof result !== 'string') {
|
||||
console.error('[i18n] t() returned non-string:', typeof result)
|
||||
}
|
||||
return result
|
||||
} catch {
|
||||
return msg
|
||||
}
|
||||
|
||||
17
apps/frontend/src/plugins/intl-payload-safety.ts
Normal file
17
apps/frontend/src/plugins/intl-payload-safety.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export default definePayloadPlugin(() => {
|
||||
definePayloadReducer('IntlMessageFormat', (value) => {
|
||||
if (value?.constructor?.name === 'IntlMessageFormat' || value?._ast !== undefined) {
|
||||
if (import.meta.dev) {
|
||||
console.warn('[i18n] IntlMessageFormat instance leaked into payload - returning null')
|
||||
console.warn('[i18n] This indicates a bug that should be fixed upstream')
|
||||
console.warn('[i18n] Leaked value:', value)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
definePayloadReviver('IntlMessageFormat', () => null)
|
||||
})
|
||||
37
apps/frontend/src/plugins/payload-debugger.ts
Normal file
37
apps/frontend/src/plugins/payload-debugger.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
function findNonPOJOs(
|
||||
obj: unknown,
|
||||
path: string,
|
||||
found: Array<{ path: string; type: string }> = [],
|
||||
): Array<{ path: string; type: string }> {
|
||||
if (obj === null || typeof obj !== 'object') return found
|
||||
|
||||
const proto = Object.getPrototypeOf(obj)
|
||||
if (proto !== Object.prototype && proto !== null && !Array.isArray(obj)) {
|
||||
found.push({ path, type: obj.constructor?.name ?? 'Unknown' })
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
findNonPOJOs(value, `${path}.${key}`, found)
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
if (!import.meta.dev || !import.meta.server) return
|
||||
|
||||
nuxtApp.hooks.hook('app:rendered', () => {
|
||||
try {
|
||||
JSON.stringify(nuxtApp.payload)
|
||||
} catch (e) {
|
||||
console.error('[payload-debugger] Payload serialization would fail:', e)
|
||||
const nonPOJOs = findNonPOJOs(nuxtApp.payload, 'payload')
|
||||
if (nonPOJOs.length > 0) {
|
||||
console.error('[payload-debugger] Non-POJO objects found in payload:')
|
||||
for (const { path, type } of nonPOJOs) {
|
||||
console.error(` - ${path}: ${type}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import IntlMessageFormat, { type FormatXMLElementFn, type PrimitiveType } from 'intl-messageformat'
|
||||
import { computed, useSlots, type VNode } from 'vue'
|
||||
import { computed, markRaw, useSlots, type VNode } from 'vue'
|
||||
|
||||
import type { MessageDescriptor } from '../../composables/i18n'
|
||||
import { injectI18n } from '../../providers/i18n'
|
||||
@@ -32,11 +32,13 @@ const formattedParts = computed(() => {
|
||||
slotHandlers[normalizedName] = (chunks) => {
|
||||
const slot = slots[slotName]
|
||||
if (slot) {
|
||||
return slot({
|
||||
children: chunks,
|
||||
})
|
||||
return markRaw(
|
||||
slot({
|
||||
children: chunks,
|
||||
}),
|
||||
) as VNode[]
|
||||
}
|
||||
return chunks as VNode[]
|
||||
return markRaw(chunks) as VNode[]
|
||||
}
|
||||
|
||||
msg = msg.replace(
|
||||
@@ -52,10 +54,14 @@ const formattedParts = computed(() => {
|
||||
...slotHandlers,
|
||||
})
|
||||
|
||||
// ensure result array items are marked as raw if they're VNodes
|
||||
// prevents VNodes from entering the reactive system and SSR payload
|
||||
if (Array.isArray(result)) {
|
||||
return result
|
||||
return result.map((part) =>
|
||||
typeof part === 'object' && part !== null ? markRaw(part) : part,
|
||||
)
|
||||
}
|
||||
return [result]
|
||||
return [typeof result === 'object' && result !== null ? markRaw(result) : result]
|
||||
} catch {
|
||||
return [msg]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user