feat: add translation key collection utility and update test for completeness
This commit is contained in:
@@ -804,6 +804,7 @@ export function EmailViewer({
|
|||||||
const t = useTranslations('email_viewer');
|
const t = useTranslations('email_viewer');
|
||||||
const tNotifications = useTranslations('notifications');
|
const tNotifications = useTranslations('notifications');
|
||||||
const tCommon = useTranslations('common');
|
const tCommon = useTranslations('common');
|
||||||
|
const tSmime = useTranslations('smime');
|
||||||
const tFiles = useTranslations('files');
|
const tFiles = useTranslations('files');
|
||||||
const externalContentPolicy = useSettingsStore((state) => state.externalContentPolicy);
|
const externalContentPolicy = useSettingsStore((state) => state.externalContentPolicy);
|
||||||
const mailAttachmentAction = useSettingsStore((state) => state.mailAttachmentAction);
|
const mailAttachmentAction = useSettingsStore((state) => state.mailAttachmentAction);
|
||||||
@@ -3944,8 +3945,8 @@ export function EmailViewer({
|
|||||||
setSmimeUnlockError(null);
|
setSmimeUnlockError(null);
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSmimeUnlockSubmit}
|
onSubmit={handleSmimeUnlockSubmit}
|
||||||
title={tCommon('smime.unlock_key')}
|
title={tSmime('unlock_key')}
|
||||||
description={tCommon('smime.unlock_key_desc')}
|
description={tSmime('unlock_key_desc')}
|
||||||
error={smimeUnlockError}
|
error={smimeUnlockError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,22 @@ function extractUsedKeys(filePath: string): string[] {
|
|||||||
return [...new Set(keys)];
|
return [...new Set(keys)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectUsedKeysByFile(files: string[]): Map<string, string[]> {
|
||||||
|
const keyToFiles = new Map<string, string[]>();
|
||||||
|
|
||||||
|
for (const filePath of files) {
|
||||||
|
for (const key of extractUsedKeys(filePath)) {
|
||||||
|
const existing = keyToFiles.get(key) ?? [];
|
||||||
|
if (!existing.includes(filePath)) {
|
||||||
|
existing.push(filePath);
|
||||||
|
keyToFiles.set(key, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyToFiles;
|
||||||
|
}
|
||||||
|
|
||||||
const locales = fs
|
const locales = fs
|
||||||
.readdirSync(localesDir)
|
.readdirSync(localesDir)
|
||||||
.filter((entry) => fs.statSync(path.join(localesDir, entry)).isDirectory());
|
.filter((entry) => fs.statSync(path.join(localesDir, entry)).isDirectory());
|
||||||
@@ -120,19 +136,22 @@ describe('translations completeness', () => {
|
|||||||
describe('translations used in source code exist in en locale', () => {
|
describe('translations used in source code exist in en locale', () => {
|
||||||
const srcDirs = ['components', 'app', 'hooks', 'lib', 'stores', 'contexts'].map((d) => path.join(rootDir, d));
|
const srcDirs = ['components', 'app', 'hooks', 'lib', 'stores', 'contexts'].map((d) => path.join(rootDir, d));
|
||||||
const allFiles = srcDirs.flatMap((d) => getSourceFiles(d));
|
const allFiles = srcDirs.flatMap((d) => getSourceFiles(d));
|
||||||
const usedKeys = new Set<string>();
|
const usedKeysByFile = collectUsedKeysByFile(allFiles);
|
||||||
for (const f of allFiles) {
|
const usedKeys = [...usedKeysByFile.keys()].sort();
|
||||||
for (const k of extractUsedKeys(f)) {
|
|
||||||
usedKeys.add(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it('all translation keys referenced in source should exist in en locale', () => {
|
it('all translation keys referenced in source should exist in en locale', () => {
|
||||||
const missing = [...usedKeys].sort().filter((key) => resolveKey(referenceData, key) === undefined);
|
const missing = usedKeys.filter((key) => resolveKey(referenceData, key) === undefined);
|
||||||
|
const details = missing.map((key) => {
|
||||||
|
const relativeFiles = (usedKeysByFile.get(key) ?? [])
|
||||||
|
.map((filePath) => path.relative(rootDir, filePath))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
return `${key}\n used in:\n - ${relativeFiles.join('\n - ')}`;
|
||||||
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
missing,
|
missing,
|
||||||
`${missing.length} translation key(s) used in source code but missing from en locale:\n${missing.join('\n')}`,
|
`${missing.length} translation key(s) used in source code but missing from en locale:\n${details.join('\n')}`,
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "next lint --fix",
|
||||||
|
"test:translations": "vitest run lib/__tests__/translations.test.ts",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user