i18n: add Japanese locale support (#9768)

This commit is contained in:
N
2026-06-13 15:04:06 +09:00
committed by GitHub
parent f40d56fe09
commit d6d99f548a
5 changed files with 4831 additions and 2 deletions
+2 -1
View File
@@ -43,7 +43,8 @@
"test:integration:debug": "DEBUG=1 vitest run --config vitest.integration.config.ts",
"i18n:extract": "formatjs extract 'src/**/*.{ts,tsx}' --ignore '**/*.d.ts' --out-file src/i18n/messages/en.json --flatten && pnpm run i18n:compile",
"i18n:check": "node scripts/i18n-check.js",
"i18n:compile": "node scripts/i18n-compile.js"
"i18n:compile": "node scripts/i18n-compile.js",
"i18n:validate-ja": "node scripts/i18n-validate-locale.js ja"
},
"dependencies": {
"@aaif/goose-sdk": "workspace:*",
@@ -0,0 +1,56 @@
#!/usr/bin/env node
/**
* Validate that a translated locale catalog mirrors en.json and preserves ICU placeholders.
*/
const fs = require('fs');
const path = require('path');
const locale = process.argv[2] || 'ja';
const projectDir = path.join(__dirname, '..');
const messagesDir = path.join(projectDir, 'src', 'i18n', 'messages');
const enPath = path.join(messagesDir, 'en.json');
const localePath = path.join(messagesDir, locale + '.json');
function readJson(file) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
function extractPlaceholders(message) {
const placeholders = new Set();
const re = /\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?=[},])/g;
let match;
while ((match = re.exec(message)) !== null) {
placeholders.add(match[1]);
}
return [...placeholders].sort();
}
const en = readJson(enPath);
const translated = readJson(localePath);
const enKeys = Object.keys(en).sort();
const localeKeys = Object.keys(translated).sort();
const missing = enKeys.filter((key) => !Object.prototype.hasOwnProperty.call(translated, key));
const extra = localeKeys.filter((key) => !Object.prototype.hasOwnProperty.call(en, key));
const placeholderIssues = [];
for (const key of enKeys) {
if (!translated[key]) continue;
const source = en[key].defaultMessage || '';
const target = translated[key].defaultMessage || '';
const sourcePlaceholders = extractPlaceholders(source);
const targetPlaceholders = extractPlaceholders(target);
if (JSON.stringify(sourcePlaceholders) !== JSON.stringify(targetPlaceholders)) {
placeholderIssues.push({ key, sourcePlaceholders, targetPlaceholders });
}
}
if (missing.length || extra.length || placeholderIssues.length) {
if (missing.length) console.error('Missing ' + locale + ' keys:', missing.slice(0, 50));
if (extra.length) console.error('Extra ' + locale + ' keys:', extra.slice(0, 50));
if (placeholderIssues.length) console.error('Placeholder issues:', placeholderIssues.slice(0, 20));
process.exit(1);
}
console.log('i18n locale "' + locale + '" is valid (' + enKeys.length + ' messages).');
+12
View File
@@ -73,6 +73,18 @@ describe('getLocale', () => {
expect(getLocale()).toEqual({ locale: 'tr', messageLocale: 'tr' });
});
it('supports Japanese from navigator.languages', () => {
vi.stubGlobal('navigator', { languages: ['ja-JP'] });
expect(getLocale()).toEqual({ locale: 'ja-JP', messageLocale: 'ja' });
});
it('supports POSIX-style Japanese locale from GOOSE_LOCALE', () => {
mockAppConfig({ GOOSE_LOCALE: 'ja_JP' });
vi.stubGlobal('navigator', { languages: ['xx-XX'] });
expect(getLocale()).toEqual({ locale: 'ja-JP', messageLocale: 'ja' });
});
it('supports Hindi from navigator.languages', () => {
vi.stubGlobal('navigator', { languages: ['hi-IN'] });
expect(getLocale()).toEqual({ locale: 'hi-IN', messageLocale: 'hi' });
+1 -1
View File
@@ -15,7 +15,7 @@
export { defineMessages, useIntl } from 'react-intl';
/** The set of locales that have translation catalogs. */
const SUPPORTED_LOCALES = new Set(['en', 'hi', 'ru', 'tr', 'zh-CN']);
const SUPPORTED_LOCALES = new Set(['en', 'hi', 'ru', 'tr', 'zh-CN', 'ja']);
/**
* Map Simplified Chinese aliases (zh, zh-Hans*, zh-SG, zh-MY) to "zh-CN".
File diff suppressed because it is too large Load Diff