mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
i18n(ja): ship Japanese
- Register ja: CATALOGS, LOCALES, LOCALE_NAMES (日本語) and the package boundaries; ja and ja-JP tags select it. - Japanese unit words follow the number directly, as in the catalog: formatQuantity writes 5分 and 1時間5分 (km and kt keep the space), and formatRelativeTime drops the space CLDR puts in 5 分前. - Tests: locale tags, picker order, Intl output, durations and the ramp/exit/lane wording in Japanese. - docs/translations/ja.md: style, units, plurals and glossary; ja added to TRANSLATORS.md and CURRENT-STATE.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7ad073aa9c
commit
ed40201481
12 files changed
+221
-16
No files matched your search
@@ -875,6 +875,13 @@ test('route numbers follow the page language and never break from their unit', (
|
||||
);
|
||||
const list = directionsStepList({ route });
|
||||
assert.equal(list.items[1].lead, '240\u00a0м');
|
||||
|
||||
// Japanese unit words follow the number: 1時間5分, but 1.5 km.
|
||||
setLocale('ja');
|
||||
assert.equal(routeDistanceText(1500), '1.5\u00a0km');
|
||||
assert.equal(routeDurationText(39), '39秒');
|
||||
assert.equal(routeDurationText(3900), '1時間5分');
|
||||
assert.equal(routeDurationText(7200), '2時間');
|
||||
});
|
||||
|
||||
test('ramp and lane wording stays localized after choosing the maneuver form', (t) => {
|
||||
@@ -892,6 +899,12 @@ test('ramp and lane wording stays localized after choosing the maneuver form', (
|
||||
'Toma la salida bruscamente a la derecha',
|
||||
'Toma el carril izquierdo',
|
||||
],
|
||||
[
|
||||
'ja',
|
||||
'ランプを斜め左に進んでI-95に入る',
|
||||
'出口を急角度で右に出る',
|
||||
'左車線を通行',
|
||||
],
|
||||
[
|
||||
'zh-CN',
|
||||
'向左前方驶入匝道,进入 I-95',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
applyDocumentTranslations,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatRelativeTime,
|
||||
msg,
|
||||
persistLocaleAndReload,
|
||||
setLocale,
|
||||
@@ -35,6 +36,9 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
|
||||
assert.equal(normalizeLocale(' RU '), 'ru');
|
||||
assert.equal(normalizeLocale('en_GB'), 'en');
|
||||
assert.equal(normalizeLocale('xx'), null);
|
||||
assert.equal(normalizeLocale('ja'), 'ja');
|
||||
assert.equal(normalizeLocale('ja-JP'), 'ja');
|
||||
assert.equal(normalizeLocale('JA_jp'), 'ja');
|
||||
// Explicit script tags take precedence over regional defaults.
|
||||
assert.equal(normalizeLocale('zh'), 'zh-CN');
|
||||
assert.equal(normalizeLocale('zh-CN'), 'zh-CN');
|
||||
@@ -164,7 +168,13 @@ test('Intl formatters follow the active locale', () => {
|
||||
assert.equal(formatNumber(1234.5), '1,234.5');
|
||||
setLocale('ru');
|
||||
assert.equal(formatDate(date, options), '15 января 2026 г.');
|
||||
assert.equal(formatRelativeTime(-5, 'minute'), '5 минут назад');
|
||||
assert.equal(formatNumber(1234.5), '1 234,5');
|
||||
// Japanese keeps a number and its unit together, as the catalog does.
|
||||
setLocale('ja');
|
||||
assert.equal(formatDate(date, options), '2026年1月15日');
|
||||
assert.equal(formatRelativeTime(-5, 'minute'), '5分前');
|
||||
assert.equal(formatRelativeTime(-3, 'day'), '3日前');
|
||||
});
|
||||
|
||||
function fakeElement({ text = '', attributes = {} } = {}) {
|
||||
|
||||
+16
-3
@@ -17,11 +17,18 @@ import {
|
||||
writeStoredLocale,
|
||||
} from './locale.js';
|
||||
import es from './locales/es.json' with { type: 'json' };
|
||||
import ja from './locales/ja.json' with { type: 'json' };
|
||||
import ru from './locales/ru.json' with { type: 'json' };
|
||||
import zhCN from './locales/zh-CN.json' with { type: 'json' };
|
||||
import zhHant from './locales/zh-Hant.json' with { type: 'json' };
|
||||
|
||||
const CATALOGS = Object.freeze({ es, ru, 'zh-CN': zhCN, 'zh-Hant': zhHant });
|
||||
const CATALOGS = Object.freeze({
|
||||
es,
|
||||
ja,
|
||||
ru,
|
||||
'zh-CN': zhCN,
|
||||
'zh-Hant': zhHant,
|
||||
});
|
||||
|
||||
let currentLocale = DEFAULT_LOCALE;
|
||||
|
||||
@@ -201,13 +208,19 @@ export function formatUtcDayMonth(value) {
|
||||
|
||||
/**
|
||||
* Locale-aware relative time (Intl.RelativeTimeFormat), e.g.
|
||||
* formatRelativeTime(-5, 'minute') → '5 minutes ago' / '5 минут назад'.
|
||||
* formatRelativeTime(-5, 'minute') → '5 minutes ago' / '5 минут назад' /
|
||||
* '5分前'.
|
||||
*/
|
||||
export function formatRelativeTime(value, unit = 'second', options) {
|
||||
return cachedFormatter('relative', Intl.RelativeTimeFormat, {
|
||||
const text = cachedFormatter('relative', Intl.RelativeTimeFormat, {
|
||||
numeric: 'auto',
|
||||
...options,
|
||||
}).format(value, unit);
|
||||
// CLDR spaces Japanese (5 分前); the ja catalog writes the number and its
|
||||
// unit together (5分前), so the two must not differ on one screen.
|
||||
return currentLocale === 'ja'
|
||||
? text.replace(/(?<=\d) (?=\p{Script=Han})/gu, '')
|
||||
: text;
|
||||
}
|
||||
|
||||
/** Collapse markup whitespace so a message reads the same in HTML and JS. */
|
||||
|
||||
+10
-2
@@ -8,12 +8,20 @@ export const LOCALE_STORAGE_KEY = 'gev:locale:v1';
|
||||
export const DEFAULT_LOCALE = 'en';
|
||||
|
||||
/** Shipped locales; order does not matter. Adding one: docs/TRANSLATORS.md. */
|
||||
export const LOCALES = Object.freeze(['en', 'es', 'ru', 'zh-CN', 'zh-Hant']);
|
||||
export const LOCALES = Object.freeze([
|
||||
'en',
|
||||
'es',
|
||||
'ja',
|
||||
'ru',
|
||||
'zh-CN',
|
||||
'zh-Hant',
|
||||
]);
|
||||
|
||||
/** Each locale named in its own language, for the selector. */
|
||||
export const LOCALE_NAMES = Object.freeze({
|
||||
en: 'English',
|
||||
es: 'Español',
|
||||
ja: '日本語',
|
||||
ru: 'Русский',
|
||||
'zh-CN': '中文(简体)',
|
||||
'zh-Hant': '中文(繁體)',
|
||||
@@ -23,7 +31,7 @@ export const LOCALE_NAMES = Object.freeze({
|
||||
const RTL_LOCALES = Object.freeze([]);
|
||||
|
||||
/**
|
||||
* Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru',
|
||||
* Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru', 'ja-JP' → 'ja',
|
||||
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN';
|
||||
* 'zh-Hant', 'zh-TW', 'zh-HK' and 'zh-MO' → 'zh-Hant'.
|
||||
* @returns {string|null}
|
||||
|
||||
@@ -20,7 +20,11 @@ import * as Cesium from 'cesium';
|
||||
import { roadLabel } from '../../data/routeSteps.js';
|
||||
import { t, msg, formatNumber, translateKnown } from '../../i18n/index.js';
|
||||
import { pointerOwnerName } from '../../data/pointerOwnerNames.js';
|
||||
import { formatQuantity } from '../../ui/unitLabels.js';
|
||||
import {
|
||||
formatQuantity,
|
||||
quantitySeparator,
|
||||
unitLabel,
|
||||
} from '../../ui/unitLabels.js';
|
||||
|
||||
export const DIRECTIONS_STEP_OVERLAY_SOURCE_ID = 'directions-step';
|
||||
export const DIRECTIONS_STEP_OVERLAY_SOURCE_OPTIONS = Object.freeze({
|
||||
@@ -137,7 +141,9 @@ export function routeDurationText(seconds) {
|
||||
if (minutes < 60) return formatQuantity(minutes, 'min');
|
||||
const hours = formatQuantity(Math.floor(minutes / 60), 'h');
|
||||
const rest = minutes % 60;
|
||||
return rest ? `${hours}\u00a0${formatQuantity(rest, 'min')}` : hours;
|
||||
if (!rest) return hours;
|
||||
// "1 h 5 min"; Japanese runs the parts together: 1時間5分.
|
||||
return `${hours}${quantitySeparator(unitLabel('h'))}${formatQuantity(rest, 'min')}`;
|
||||
}
|
||||
|
||||
/** "left", "slightly right", … as it reads inside a maneuver sentence. */
|
||||
|
||||
@@ -40,7 +40,7 @@ test('the current language comes first, the rest by their own names', () => {
|
||||
nativeNames: LOCALE_NAMES,
|
||||
}),
|
||||
),
|
||||
['ru', 'en', 'es', 'zh-CN', 'zh-Hant'],
|
||||
['ru', 'en', 'es', 'zh-CN', 'zh-Hant', 'ja'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
codes(
|
||||
@@ -50,7 +50,7 @@ test('the current language comes first, the rest by their own names', () => {
|
||||
nativeNames: LOCALE_NAMES,
|
||||
}),
|
||||
),
|
||||
['es', 'en', 'ru', 'zh-CN', 'zh-Hant'],
|
||||
['es', 'en', 'ru', 'zh-CN', 'zh-Hant', 'ja'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
codes(
|
||||
@@ -60,7 +60,7 @@ test('the current language comes first, the rest by their own names', () => {
|
||||
nativeNames: LOCALE_NAMES,
|
||||
}),
|
||||
),
|
||||
['en', 'es', 'ru', 'zh-CN', 'zh-Hant'],
|
||||
['en', 'es', 'ru', 'zh-CN', 'zh-Hant', 'ja'],
|
||||
);
|
||||
const rows = buildLocaleOptions({
|
||||
locales: MANY,
|
||||
@@ -263,7 +263,7 @@ test('keyboard model: open on the current language, move, choose, close', () =>
|
||||
assert.deepEqual(model.keydown({ key: 'ArrowDown' }), { handled: true });
|
||||
assert.equal(model.activeOption.code, 'en');
|
||||
model.keydown({ key: 'End' });
|
||||
assert.equal(model.activeOption.code, 'zh-Hant');
|
||||
assert.equal(model.activeOption.code, 'ja');
|
||||
model.keydown({ key: 'Home' });
|
||||
assert.equal(model.activeOption.code, 'ru');
|
||||
model.keydown({ key: 'ArrowUp' });
|
||||
|
||||
+13
-2
@@ -1,4 +1,4 @@
|
||||
import { formatNumber, msg, t } from '../i18n/index.js';
|
||||
import { formatNumber, getLocale, msg, t } from '../i18n/index.js';
|
||||
|
||||
// Unit ids stay English in data, params and AI-tool readings; only the label
|
||||
// shown on the page is translated. dBZ and °C read the same in every language.
|
||||
@@ -23,6 +23,17 @@ export function unitLabel(unit) {
|
||||
return Object.hasOwn(UNIT_LABELS, unit) ? t(UNIT_LABELS[unit]) : unit || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* What goes between a number and its unit label: a no-break space, except
|
||||
* before a Japanese unit word, which follows the number directly (5分, 1時間).
|
||||
*/
|
||||
export function quantitySeparator(label) {
|
||||
return getLocale() === 'ja' &&
|
||||
/^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(label)
|
||||
? ''
|
||||
: '\u00a0';
|
||||
}
|
||||
|
||||
/**
|
||||
* A number and its unit that never break apart: "22,0 км/ч". Readings carry
|
||||
* no digit grouping ("1008 hPa"), as on weather charts.
|
||||
@@ -30,7 +41,7 @@ export function unitLabel(unit) {
|
||||
export function formatQuantity(value, unit, options) {
|
||||
const label = unitLabel(unit);
|
||||
const number = formatNumber(value, { useGrouping: false, ...options });
|
||||
return label ? `${number}\u00a0${label}` : number;
|
||||
return label ? `${number}${quantitySeparator(label)}${label}` : number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,3 +44,14 @@ test('other languages get their decimal separator and no digit grouping', () =>
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
|
||||
test('Japanese unit words follow the number directly; symbols keep the space', () => {
|
||||
try {
|
||||
setLocale('ja');
|
||||
assert.equal(formatQuantity(5, 'min'), '5分');
|
||||
assert.equal(formatQuantity(1, 'h'), '1時間');
|
||||
assert.equal(formatQuantity(95, 'kt'), '95\u00a0kt');
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user