|
| 1 | +// @ts-check |
| 2 | +// |
| 3 | +// Static i18n checks — no browser, no dev server needed. |
| 4 | +// `en.json` is the reference (it is the i18next `fallbackLng`): every other |
| 5 | +// locale must expose exactly the same set of leaf keys, with no empty values. |
| 6 | +// |
| 7 | +// These tests read the JSON files directly, so they run in milliseconds and |
| 8 | +// catch the most common translation regressions: a key added to `en` but not |
| 9 | +// translated elsewhere (-> silent English fallback in the UI), a stale key left |
| 10 | +// behind after a rename, or a key translated to an empty string. |
| 11 | + |
| 12 | +const { test, expect } = require('@playwright/test'); |
| 13 | + |
| 14 | +const en = require('../../langs/en.json'); |
| 15 | + |
| 16 | +/** Locales to check against `en` — keep in sync with langs/i18n_conf.js. */ |
| 17 | +const LOCALES = { |
| 18 | + fr: require('../../langs/fr.json'), |
| 19 | + es: require('../../langs/es.json'), |
| 20 | + ja: require('../../langs/ja.json'), |
| 21 | + ta: require('../../langs/ta.json'), |
| 22 | + 'zh-Hans': require('../../langs/zh-Hans.json'), |
| 23 | + 'zh-Hant': require('../../langs/zh-Hant.json'), |
| 24 | +}; |
| 25 | + |
| 26 | +/** |
| 27 | + * Flatten a nested translation object into dot-separated leaf keys. |
| 28 | + * @param {Record<string, any>} obj |
| 29 | + * @param {string} prefix |
| 30 | + * @param {Record<string, string>} out |
| 31 | + * @returns {Record<string, string>} |
| 32 | + */ |
| 33 | +function flatten(obj, prefix = '', out = {}) { |
| 34 | + for (const [k, v] of Object.entries(obj)) { |
| 35 | + const key = prefix ? `${prefix}.${k}` : k; |
| 36 | + if (v && typeof v === 'object' && !Array.isArray(v)) { |
| 37 | + flatten(v, key, out); |
| 38 | + } else { |
| 39 | + out[key] = v; |
| 40 | + } |
| 41 | + } |
| 42 | + return out; |
| 43 | +} |
| 44 | + |
| 45 | +const enFlat = flatten(en); |
| 46 | +const enKeys = Object.keys(enFlat); |
| 47 | + |
| 48 | +for (const [name, dict] of Object.entries(LOCALES)) { |
| 49 | + test.describe(`i18n: ${name}`, () => { |
| 50 | + const flat = flatten(dict); |
| 51 | + const keys = Object.keys(flat); |
| 52 | + |
| 53 | + test('has no missing keys (vs en)', () => { |
| 54 | + const missing = enKeys.filter((k) => !(k in flat)); |
| 55 | + expect(missing, `Keys present in en.json but missing in ${name}.json`).toEqual([]); |
| 56 | + }); |
| 57 | + |
| 58 | + test('has no stale keys (absent from en)', () => { |
| 59 | + const extra = keys.filter((k) => !(k in enFlat)); |
| 60 | + expect(extra, `Keys present in ${name}.json but no longer in en.json`).toEqual([]); |
| 61 | + }); |
| 62 | + |
| 63 | + test('has no empty translations', () => { |
| 64 | + const empty = keys.filter((k) => typeof flat[k] === 'string' && flat[k].trim() === ''); |
| 65 | + expect(empty, `Keys translated to an empty string in ${name}.json`).toEqual([]); |
| 66 | + }); |
| 67 | + }); |
| 68 | +} |
0 commit comments