Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions frontend/scripts/check-i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
import { ENGLISH_FALLBACK_KEYS } from '../src/lib/locales/adminOperationsFallback.js';
import { mergeInto } from '../src/lib/locales/createLocale.js';
import { DATE_FORMAT_KEYS, DATE_FORMATS } from '../src/lib/locales/dateFormats.js';
import { findMixedKeyQuoting } from './locale-key-quoting.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const LOCALES_DIR = join(__dirname, '..', 'src', 'lib', 'locales');
Expand Down Expand Up @@ -515,6 +516,36 @@ async function main() {
}
console.log('');

// --- Check 6: Locale key quoting ---
console.log(' --- Locale key quoting ---');
const localeSourceFiles = [];
for (const locale of localeDirs) {
const localeDir = join(LOCALES_DIR, locale);
const files = (await readdir(localeDir)).filter((f) => f.endsWith('.js') && f !== 'index.js');
for (const file of files) {
localeSourceFiles.push({
file: `src/lib/locales/${locale}/${file}`,
content: await readFile(join(localeDir, file), 'utf-8'),
});
}
}

const quotingProblems = findMixedKeyQuoting(localeSourceFiles);
let quotingErrors = 0;
if (quotingProblems.length > 0) {
for (const { file, unquoted } of quotingProblems) {
console.log(` ✗ ${file}: unquoted key(s) in a quoted-key file`);
for (const { line, key } of unquoted) {
console.log(` line ${line}: ${key}`);
}
quotingErrors += unquoted.length;
}
exitCode = 1;
} else {
console.log(' ✓ No mixed key quoting in locale files');
}
console.log('');

// --- Summary ---
if (requiredMissing > 0 || totalExtra > 0) {
exitCode = 1;
Expand All @@ -532,13 +563,17 @@ async function main() {
if (placeholderErrors > 0) issues.push(`${placeholderErrors} placeholder mismatch(es)`);
if (carryoverTotal > 0) issues.push(`${carryoverTotal} suspected carryover(s)`);
if (dateFormatErrors > 0) issues.push(`${dateFormatErrors} date format coverage error(s)`);
if (quotingErrors > 0)
issues.push(`${quotingErrors} unquoted key(s) in quoted-key locale file(s)`);
console.log(` ${issues.join(', ')}`);
console.log('');
process.exit(1);
}
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}
40 changes: 40 additions & 0 deletions frontend/scripts/locale-key-quoting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// The machine-translated catalogs quote every object key. Quoting an
// identifier key is never required in JavaScript, so a file that quotes some
// identifier keys and leaves others bare is a hand-edit or regeneration
// artifact. Keys that are not valid identifiers (e.g. "field-mappings") must
// stay quoted and do not signal the file's convention.
const QUOTED_IDENTIFIER_KEY_PATTERN = /^\s*"([A-Za-z_$][A-Za-z0-9_$]*)":\s*(['"`])/;
const UNQUOTED_IDENTIFIER_KEY_PATTERN = /^\s*([A-Za-z_$][A-Za-z0-9_$]*):\s*(['"`])/;

/**
* Finds locale files that mix quoted and unquoted object keys. Files that
* quote their keys are expected to quote all of them.
*
* @param {Array<{file: string, content: string}>} files
* @returns {Array<{file: string, unquoted: Array<{line: number, key: string}>}>}
*/
export function findMixedKeyQuoting(files) {
const problems = [];

for (const { file, content } of files) {
const unquoted = [];
let quoted = 0;

content.split('\n').forEach((line, index) => {
if (QUOTED_IDENTIFIER_KEY_PATTERN.test(line)) {
quoted += 1;
return;
}
const match = line.match(UNQUOTED_IDENTIFIER_KEY_PATTERN);
if (match) {
unquoted.push({ line: index + 1, key: match[1] });
}
});

if (quoted > 0 && unquoted.length > 0) {
problems.push({ file, unquoted });
}
}

return problems;
}
8 changes: 4 additions & 4 deletions frontend/src/lib/locales/ar/supplemental.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ export default {
"orSignInWithEmail": "أو قم بتسجيل الدخول باستخدام البريد الإلكتروني",
"profileAndSecurity": "الملف الشخصي والأمان",
"profileTitle": "الملف الشخصي",
languageTitle: "اللغة",
languageSubtitle: "اختر لغة هذه البوابة والرسائل التي تصلك.",
languageLabel: "اللغة",
languageSaved: "تم تحديث اللغة",
"languageTitle": "اللغة",
"languageSubtitle": "اختر لغة هذه البوابة والرسائل التي تصلك.",
"languageLabel": "اللغة",
"languageSaved": "تم تحديث اللغة",
"profileSubtitle": "إدارة كيفية تسجيل الدخول إلى هذه البوابة.",
"profileSection": "الحساب",
"passkeysTitle": "مفاتيح المرور",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/lib/locales/de/supplemental.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@ export default {
"orSignInWithEmail": "oder melden Sie sich mit E-Mail an",
"profileAndSecurity": "Profil und Sicherheit",
"profileTitle": "Profil",
languageTitle: "Sprache",
languageSubtitle: "Wählen Sie die Sprache für dieses Portal und die E-Mails, die Sie erhalten.",
languageLabel: "Sprache",
languageSaved: "Sprache aktualisiert",
"languageTitle": "Sprache",
"languageSubtitle": "Wählen Sie die Sprache für dieses Portal und die E-Mails, die Sie erhalten.",
"languageLabel": "Sprache",
"languageSaved": "Sprache aktualisiert",
"profileSubtitle": "Verwalten Sie, wie Sie sich bei diesem Portal anmelden.",
"profileSection": "Konto",
"passkeysTitle": "Passkeys",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/lib/locales/es/supplemental.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ export default {
"orSignInWithEmail": "o inicie sesión con el correo electrónico",
"profileAndSecurity": "Perfil y seguridad",
"profileTitle": "Perfil",
languageTitle: "Idioma",
languageSubtitle: "Elige el idioma de este portal y de los correos que recibes.",
languageLabel: "Idioma",
languageSaved: "Idioma actualizado",
"languageTitle": "Idioma",
"languageSubtitle": "Elige el idioma de este portal y de los correos que recibes.",
"languageLabel": "Idioma",
"languageSaved": "Idioma actualizado",
"profileSubtitle": "Administre cómo inicia sesión en este portal.",
"profileSection": "Cuenta",
"passkeysTitle": "Claves de acceso",
Expand Down
16 changes: 8 additions & 8 deletions frontend/src/lib/locales/ko/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -658,14 +658,14 @@ export default {
"textBody": "일반 텍스트 본문",
"adminNotes": "설명(관리자 메모)",
"activeHint": "활성(해제하면 기본 내장 템플릿 사용)",
localeLabel: "언어",
defaultLocale: "기본(모든 언어)",
translationHint: "이 언어의 버전을 편집하고 있습니다. 다른 언어는 기본 템플릿을 사용합니다.",
translationSaved: "번역이 저장되었습니다",
translationDelete: "번역 삭제",
translationDeleted: "번역이 삭제되었습니다",
translationSaveFailed: "번역을 저장하지 못했습니다: {error}",
translationDeleteFailed: "번역을 삭제하지 못했습니다: {error}",
"localeLabel": "언어",
"defaultLocale": "기본(모든 언어)",
"translationHint": "이 언어의 버전을 편집하고 있습니다. 다른 언어는 기본 템플릿을 사용합니다.",
"translationSaved": "번역이 저장되었습니다",
"translationDelete": "번역 삭제",
"translationDeleted": "번역이 삭제되었습니다",
"translationSaveFailed": "번역을 저장하지 못했습니다: {error}",
"translationDeleteFailed": "번역을 삭제하지 못했습니다: {error}",
"availableVariables": "사용 가능한 변수",
"syntaxHint": "템플릿은 Go의 html/template 문법을 사용합니다. 값에는 {open} .Field {close}를, 반복에는 {open} range .Items {close}…{open} end {close}를 사용하세요.",
"preview": "미리보기",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/lib/locales/ko/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,10 @@ export default {
"orSignInWithEmail": "또는 이메일로 로그인",
"profileAndSecurity": "프로필 및 보안",
"profileTitle": "프로필",
languageTitle: "언어",
languageSubtitle: "이 포털과 받은 이메일에 사용할 언어를 선택하세요.",
languageLabel: "언어",
languageSaved: "언어가 업데이트되었습니다",
"languageTitle": "언어",
"languageSubtitle": "이 포털과 받은 이메일에 사용할 언어를 선택하세요.",
"languageLabel": "언어",
"languageSaved": "언어가 업데이트되었습니다",
"profileSubtitle": "이 포털의 로그인 방식을 관리합니다.",
"profileSection": "계정",
"passkeysTitle": "패스키",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/lib/locales/pt-BR/supplemental.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ export default {
"orSignInWithEmail": "ou faça login com o e-mail",
"profileAndSecurity": "Perfil e segurança",
"profileTitle": "Perfil",
languageTitle: "Idioma",
languageSubtitle: "Escolha o idioma deste portal e dos e-mails que você recebe.",
languageLabel: "Idioma",
languageSaved: "Idioma atualizado",
"languageTitle": "Idioma",
"languageSubtitle": "Escolha o idioma deste portal e dos e-mails que você recebe.",
"languageLabel": "Idioma",
"languageSaved": "Idioma atualizado",
"profileSubtitle": "Gerencie como você entra neste portal.",
"profileSection": "Conta",
"passkeysTitle": "Chaves de acesso",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/lib/locales/zh-CN/supplemental.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ export default {
"orSignInWithEmail": "或使用电子邮件登录",
"profileAndSecurity": "个人资料和安全",
"profileTitle": "个人资料",
languageTitle: "语言",
languageSubtitle: "选择此门户和您收到的邮件的语言。",
languageLabel: "语言",
languageSaved: "语言已更新",
"languageTitle": "语言",
"languageSubtitle": "选择此门户和您收到的邮件的语言。",
"languageLabel": "语言",
"languageSaved": "语言已更新",
"profileSubtitle": "管理您登录此门户的方式。",
"profileSection": "帐户",
"passkeysTitle": "通行密钥",
Expand Down
Loading