feat: more o11y for i18n pojo (#5148)

This commit is contained in:
Calum H.
2026-01-18 19:18:07 +00:00
committed by GitHub
parent 6efdfdf17e
commit a0e8c7f924
5 changed files with 76 additions and 36 deletions

View File

@@ -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)
}
})
}
})

View File

@@ -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
}

View 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)
})

View 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}`)
}
}
}
})
})