Skip to content

Commit a98d244

Browse files
committed
add translation checker
1 parent 0deb325 commit a98d244

3 files changed

Lines changed: 104 additions & 0 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jobs:
1414
- uses: actions/checkout@v4
1515
- uses: actions/setup-node@v4
1616
- uses: bahmutov/npm-install@v1
17+
- run: npm run check-translations
1718
- run: npm run build-keycloak-theme
1819

1920
check_if_version_upgraded:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dev": "vite",
1212
"storybook": "storybook dev -p 6006",
1313
"build-intl": "npx --package=@helpwave/internationalization build-intl --force -i ./locales -o ./src/i18n/translations.ts -n helpwaveIdTranslation",
14+
"check-translations": "node scripts/check-translation-keys.mjs",
1415
"build": "npm run build-intl && tsc && vite build",
1516
"build-keycloak-theme": "npm run build && keycloakify build",
1617
"build-storybook": "storybook build",

scripts/check-translation-keys.mjs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import { fileURLToPath } from 'url'
4+
5+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
6+
const projectRoot = path.resolve(__dirname, '..')
7+
const localesDir = path.join(projectRoot, 'locales')
8+
const srcDir = path.join(projectRoot, 'src')
9+
10+
const T_CALL_RE = /\bt\s*\(\s*['"]([^'"]+)['"]/g
11+
12+
function loadArbKeys(filePath) {
13+
const raw = fs.readFileSync(filePath, 'utf8')
14+
const json = JSON.parse(raw)
15+
return new Set(
16+
Object.keys(json).filter(
17+
(k) => !k.startsWith('@') && k !== '@@locale'
18+
)
19+
)
20+
}
21+
22+
function allArbFiles() {
23+
const names = fs.readdirSync(localesDir)
24+
return names
25+
.filter((n) => n.endsWith('.arb') && !n.includes('/'))
26+
.map((n) => path.join(localesDir, n))
27+
}
28+
29+
function collectKeysFromSource(dir, keys = new Set()) {
30+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
31+
return keys
32+
}
33+
const entries = fs.readdirSync(dir, { withFileTypes: true })
34+
for (const e of entries) {
35+
const full = path.join(dir, e.name)
36+
if (e.name === 'node_modules' || e.name === 'translations.ts') continue
37+
if (e.isDirectory()) {
38+
collectKeysFromSource(full, keys)
39+
continue
40+
}
41+
if (!/\.(tsx?|jsx?|mjs|cjs)$/.test(e.name)) continue
42+
const content = fs.readFileSync(full, 'utf8')
43+
let m
44+
T_CALL_RE.lastIndex = 0
45+
while ((m = T_CALL_RE.exec(content)) !== null) {
46+
keys.add(m[1])
47+
}
48+
}
49+
return keys
50+
}
51+
52+
function main() {
53+
const usedKeys = collectKeysFromSource(srcDir)
54+
55+
const arbPaths = allArbFiles()
56+
if (arbPaths.length === 0) {
57+
console.error('No ARB files found in', localesDir)
58+
process.exit(1)
59+
}
60+
61+
const localeKeys = new Map()
62+
for (const p of arbPaths) {
63+
const locale = path.basename(p, '.arb')
64+
localeKeys.set(locale, loadArbKeys(p))
65+
}
66+
67+
const allLocales = [...localeKeys.keys()]
68+
const missing = []
69+
for (const key of usedKeys) {
70+
for (const locale of allLocales) {
71+
if (!localeKeys.get(locale).has(key)) {
72+
missing.push({ key, locale })
73+
}
74+
}
75+
}
76+
77+
if (missing.length > 0) {
78+
console.error('Missing translation keys (used in code but not in ARB):\n')
79+
const byKey = new Map()
80+
for (const { key, locale } of missing) {
81+
if (!byKey.has(key)) byKey.set(key, [])
82+
byKey.get(key).push(locale)
83+
}
84+
for (const [key, locales] of byKey) {
85+
console.error(` [${key}] missing in: ${locales.join(', ')}`)
86+
}
87+
process.exit(1)
88+
}
89+
90+
const arbKeySet = localeKeys.get(allLocales[0])
91+
const unused = [...arbKeySet].filter((k) => !usedKeys.has(k))
92+
if (unused.length > 0) {
93+
console.warn('Unused keys in ARB (present in locale but not found in src):')
94+
for (const k of unused.sort()) {
95+
console.warn(` ${k}`)
96+
}
97+
}
98+
99+
console.log('All translation keys used in code exist in every locale.')
100+
}
101+
102+
main()

0 commit comments

Comments
 (0)