Test that a t() key with no entry is caught, that a prop merely named like a key is not, and that a locale format the checker cannot read is exit 2 rather than a pass.
Add "Translation key check tests" from ntent to this repo.
Fetch https://ntent.app/r/f/check-locale-keys-test as plain text. Write it verbatim to scripts/check-locale-keys.test.mjs.
It needs check-locale-keys.mjs. If this repo does not have it, stop and tell me.
Then check it: node --test passes. Stage an edit to check-locale-keys.mjs that stops a missing key being caught and the commit is refused, naming this file.Reads https://ntent.app/r/f/check-locale-keys-test
Read the code
check-locale-keys.test.mjsscripts/check-locale-keys.test.mjs
143 lines
/**
* Tests for the locale-key checker, run against real files on disk.
*
* The field study read 48% of this checker's findings as false, and the cause
* was one line: any `fooKey="..."` prop was taken for a locale key. So the
* cases here come in pairs. What it must still catch (a t() call with no
* entry), and the near miss that cost it its credibility (a React `key=` or a
* `sortKey=` prop, which are not translations at all).
*
* Every run is of a copy of the script with localeDir, baseLocale and
* sourceDirs pinned, because the installer's customise step edits them. Nothing
* here touches the repository you are running from.
*
* Run: node --test scripts/check-locale-keys.test.mjs
*/
import test, { after } from 'node:test'
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
// Every temp dir this file makes. They are removed when the file's tests end, pass or fail.
const made = []
const tmp = (prefix) => {
const dir = mkdtempSync(join(tmpdir(), prefix))
made.push(dir)
return dir
}
after(() => {
for (const dir of made) rmSync(dir, { recursive: true, force: true })
})
const CHECK = join(dirname(fileURLToPath(import.meta.url)), 'check-locale-keys.mjs')
const PINNED = { localeDir: "'messages'", baseLocale: "'en'", sourceDirs: "['src']" }
/** Write { path: text } into a fresh repo and run a pinned copy there. */
function check(tree, args = ['--check'], pins = {}) {
const dir = tmp('locale-keys-')
for (const [path, text] of Object.entries(tree)) {
mkdirSync(dirname(join(dir, path)), { recursive: true })
writeFileSync(join(dir, path), typeof text === 'string' ? text : JSON.stringify(text))
}
let source = readFileSync(CHECK, 'utf8')
for (const [key, literal] of Object.entries({ ...PINNED, ...pins })) {
const re = new RegExp(`^ ${key}: .*,$`, 'm')
assert.match(source, re, `CONFIG.${key} is on one line, so a test can replace it`)
source = source.replace(re, ` ${key}: ${literal},`)
}
const script = join(dir, 'check-locale-keys.pinned.mjs')
writeFileSync(script, source)
try {
const out = execFileSync('node', [script, ...args], { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
return { code: 0, out }
} catch (e) {
return { code: e.status, out: (e.stdout ?? '') + (e.stderr ?? '') }
}
}
const EN = { nav: { settings: 'Settings' }, title: 'Hi' }
// ------------------------------------------------------ what is a key ---
test('a t() call with no entry is caught', () => {
const run = check({
'messages/en.json': EN,
'src/a.tsx': "export const A = () => <b>{t('nav.missing')}</b>\n",
})
assert.equal(run.code, 1, run.out)
assert.match(run.out, /USED-BUT-UNDEFINED {2}nav\.missing/)
})
test('a *Key prop, a React key and a sortKey are not locale keys', () => {
const run = check({
'messages/en.json': EN,
'src/a.tsx':
"export const A = () => <List sortKey=\"createdAt\" rowKey=\"id\" key=\"x\" cacheKey=\"user.profile\" titleKey=\"offline.title\" />\n",
})
assert.equal(run.code, 0, run.out)
})
test('a configured function name is read, and so is i18n.t', () => {
const tree = {
'messages/en.json': EN,
'src/a.tsx': "export const A = () => [translate('nav.gone'), i18n.t('nav.also_gone'), t('nav.settings')]\n",
}
const plain = check(tree)
assert.match(plain.out, /nav\.also_gone/, 'i18n.t is a t call')
assert.doesNotMatch(plain.out, /nav\.gone/, 'translate() is not read unless it is configured')
const configured = check(tree, ['--check'], { keyFunctions: "['t', 'translate']" })
assert.match(configured.out, /nav\.gone/)
})
test('a *Key prop is read when it is named in keyProps', () => {
const tree = {
'messages/en.json': EN,
'src/a.tsx': 'export const A = () => <Banner titleKey="offline.title" sortKey="createdAt" />\n',
}
assert.equal(check(tree).code, 0)
const configured = check(tree, ['--check'], { keyProps: "['titleKey']" })
assert.equal(configured.code, 1, configured.out)
assert.match(configured.out, /offline\.title/)
assert.doesNotMatch(configured.out, /createdAt/)
})
// ------------------------------------------------------------ layouts ---
test('namespaced i18next folders, <lang>/<ns>.json, are read', () => {
const tree = {
'messages/en/common.json': { nav: { settings: 'Settings' } },
'messages/en/billing.json': { invoice: 'Invoice' },
'messages/de/common.json': { nav: { settings: 'Einstellungen' } },
'messages/de/billing.json': { invoice: 'Rechnung' },
'src/a.tsx': "export const A = () => [t('nav.settings'), t('billing:invoice'), t('common:nav.settings')]\n",
}
const ok = check(tree)
assert.equal(ok.code, 0, ok.out)
const bad = check({ ...tree, 'messages/de/billing.json': {}, 'src/b.tsx': "export const B = () => t('billing:nope')\n" })
assert.equal(bad.code, 1, bad.out)
assert.match(bad.out, /MISSING-IN-DE {2}billing:invoice/)
assert.match(bad.out, /USED-BUT-UNDEFINED {2}billing:nope/)
})
test('gettext .po files are exit 2 and named', () => {
const run = check({ 'messages/en.po': 'msgid "a"\nmsgstr "b"\n', 'src/a.tsx': "t('a')\n" })
assert.equal(run.code, 2, run.out)
assert.match(run.out, /\.po/)
assert.match(run.out, /reads JSON/)
})
test('locales written as TS modules are exit 2 and named', () => {
const run = check({ 'messages/en.ts': "export default { a: 'b' }\n", 'src/a.tsx': "t('a')\n" })
assert.equal(run.code, 2, run.out)
assert.match(run.out, /\.ts/)
})
test('no locale directory is still exit 2', () => {
assert.equal(check({ 'src/a.tsx': "t('a')\n" }).code, 2)
})
How to tell it works: node --test passes. Stage an edit to check-locale-keys.mjs that stops a missing key being caught and the commit is refused, naming this file.