i18n(pt): complete and ship Brazilian Portuguese

- Translate the remaining 1,868 strings (pt-BR, as in #909) and review the
  contributor entries: HUD codes BRG/SPD/RANGE stay as in the other
  languages, sentence-case layer and scene names, Space key named as on the
  keyboard, short PREV/NEXT/ON/OFF chips, and the CLDR many form (exact
  millions take «de») on every plural.
- Register pt: any pt tag (pt-BR, pt-PT) maps to it; the picker shows
  «Português (Brasil)».
- Codes, units and product names identical to English go to
  same-as-english.json.
- docs/translations/pt.md: style, plurals, units and glossary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-28 12:52:01 +04:00
1 parent ce17d0cade
commit 8269f0e4e6
10 files changed
+2483 -86

No files matched your search

+16
View File
@@ -34,6 +34,8 @@ test('normalizeLocale folds regional variants and rejects unshipped tags', () =>
assert.equal(normalizeLocale('ru-RU'), 'ru');
assert.equal(normalizeLocale(' RU '), 'ru');
assert.equal(normalizeLocale('en_GB'), 'en');
assert.equal(normalizeLocale('pt-BR'), 'pt');
assert.equal(normalizeLocale('pt_PT'), 'pt');
assert.equal(normalizeLocale('xx'), null);
// Explicit script tags take precedence over regional defaults.
assert.equal(normalizeLocale('zh'), 'zh-CN');
@@ -142,6 +144,20 @@ test('tn() picks English and Russian plural forms', () => {
assert.match(label(21), / 21 КЛЮЧ(?![А-ЯЁ])/);
});
test('tn() picks the Portuguese one, many and other forms', () => {
const label = (count) =>
tn(
count,
'POWER UP · {count} KEY WAITING',
'POWER UP · {count} KEYS WAITING',
);
setLocale('pt');
assert.equal(label(0), 'ATIVAR · 0 CHAVE PENDENTE');
assert.equal(label(1), 'ATIVAR · 1 CHAVE PENDENTE');
assert.equal(label(3), 'ATIVAR · 3 CHAVES PENDENTES');
assert.equal(label(1_000_000), 'ATIVAR · 1000000 DE CHAVES PENDENTES');
});
test('t() context separates identical English with different meanings', () => {
assert.equal(t('CLEAR', { context: 'action' }), 'CLEAR');
setLocale('ru');
+8 -1
View File
@@ -17,11 +17,18 @@ import {
writeStoredLocale,
} from './locale.js';
import es from './locales/es.json' with { type: 'json' };
import pt from './locales/pt.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,
pt,
ru,
'zh-CN': zhCN,
'zh-Hant': zhHant,
});
let currentLocale = DEFAULT_LOCALE;
+10 -1
View File
@@ -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',
'pt',
'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',
pt: 'Português (Brasil)',
ru: 'Русский',
'zh-CN': '中文(简体)',
'zh-Hant': '中文(繁體)',
@@ -24,6 +32,7 @@ const RTL_LOCALES = Object.freeze([]);
/**
* Map any language tag to a shipped locale: 'ru-RU' and 'RU' → 'ru',
* 'pt-BR' and 'pt-PT' → 'pt' (Brazilian Portuguese),
* 'zh', 'zh-CN' and 'zh-Hans-SG' → 'zh-CN';
* 'zh-Hant', 'zh-TW', 'zh-HK' and 'zh-MO' → 'zh-Hant'.
* @returns {string|null}
+2193 -77
View File
File diff suppressed because it is too large. Load diff
+125
View File
@@ -124,6 +124,131 @@
"ΔN",
"▲ {frp} MW"
],
"pt": [
"1090 MHz",
"978 MHz UAT",
"AIS: --",
"AIS: {name}",
"ALTITUDE",
"ALTITUDE · FT",
"ANIME",
"AUTO",
"Altitude {altitude}",
"Anime",
"Austin",
"BIKE",
"BRG {value}°",
"BRG —",
"Bloom",
"Blues",
"CAL {value}",
"CAL · {badge}",
"CCTV",
"CRS {value}°",
"CRT",
"Country",
"DEST ---°",
"DEST {bearing}",
"Dance",
"Drone",
"Dubai",
"ENE",
"ESE",
"FLIR",
"FLT",
"FM MHZ",
"FOV",
"FOV {value}°",
"FT",
"Folk",
"Funk",
"GEO",
"GOOGLE STREET VIEW",
"Global",
"Global · 60°S–60°N",
"Google Street View",
"HDG",
"HDG {value}°",
"Hip-Hop",
"House",
"Indie",
"Ironbow",
"Jazz",
"LOCAL",
"Landsat 8/9",
"Landsat 8/9 via HLS",
"Layout",
"Los Angeles",
"MIC",
"MIL",
"MINI",
"MM",
"MONITOR",
"MSG/S",
"Metal",
"NAV",
"NE",
"NNE",
"NOIR",
"NORMAL",
"NVG",
"Noir",
"Normal",
"OCEANIA",
"OK",
"POS: {time}Z",
"PRECIP",
"Paris",
"Pop",
"Punk",
"RANGE",
"Reggae",
"Rock",
"S2 GhostMaps",
"SAR",
"SE",
"SIG",
"SNAPSHOT",
"SQUAWK {code} · {status}",
"SSE",
"STATUS",
"STATUS · ",
"Sentinel-2",
"Sentinel-2 via HLS",
"Sonar",
"Soul",
"TAGS · {tags}",
"TEMP",
"Tallinn",
"Techno",
"Trance",
"USB {rate} msg/s",
"VISUAL",
"VOLUME",
"VS",
"WHOT/BHOT",
"WX",
"dB",
"fire confidence|nominal",
"ft",
"hPa",
"installation class|BASE",
"km",
"km/h",
"kt",
"min",
"mini",
"mph",
"nominal",
"view band|GLOBAL",
"{rate} msg/s",
"{type} SPD: {speed} HDG: {heading}",
"{value} KM",
"{value} KM/H",
"{value} KM/S",
"ΔN",
"▲ {frp} MW"
],
"ru": [
"AIS: --",
"AIS: {name}",
+3 -3
View File
@@ -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', 'pt', 'zh-CN', 'zh-Hant'],
);
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', 'pt', 'ru', 'zh-CN', 'zh-Hant'],
);
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', 'pt', 'ru', 'zh-CN', 'zh-Hant'],
);
const rows = buildLocaleOptions({
locales: MANY,