mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 14:52:19 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c5fbd3223 | ||
|
|
5d28d614e0 |
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
- Generate the Traditional Chinese catalog instead of translating it:
|
||||||
|
`npm run i18n -- --zh-hant` writes `zh-Hant` from `zh-CN` with OpenCC `s2tw`
|
||||||
|
and its character corrections, and `npm test` fails, naming the key, when
|
||||||
|
`zh-Hant` is edited by hand (German, @gekh, #1699).
|
||||||
@@ -5096,7 +5096,9 @@ Shipped locales: **en** (source and fallback), **ar**, **de**, **es**, **fr**, *
|
|||||||
- **Gate.** `npm run i18n` (and `src/i18n/catalog.test.mjs` under `npm test`)
|
- **Gate.** `npm run i18n` (and `src/i18n/catalog.test.mjs` under `npm test`)
|
||||||
extracts every message and fails on missing, empty, unused,
|
extracts every message and fails on missing, empty, unused,
|
||||||
placeholder-mismatched, still-English (plural forms included) or
|
placeholder-mismatched, still-English (plural forms included) or
|
||||||
wrongly-shaped plural entries.
|
wrongly-shaped plural entries. `zh-Hant` is generated from `zh-CN`
|
||||||
|
(`npm run i18n -- --zh-hant`: OpenCC `s2tw` through `opencc-js`, then a
|
||||||
|
fixed list of character corrections), and the gate fails when it differs.
|
||||||
- **Not translated by design:** layer ids, share-link and hash parameters, API
|
- **Not translated by design:** layer ids, share-link and hash parameters, API
|
||||||
fields, voice tool schemas, provider names, feed data (callsigns, vessel and
|
fields, voice tool schemas, provider names, feed data (callsigns, vessel and
|
||||||
station names) and attribution.
|
station names) and attribution.
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ npm run i18n -- --prune # drop entries whose English no longer exists
|
|||||||
|
|
||||||
Changing the English changes the key: the old translation goes unused and the new text shows as missing. Add the translation to every `src/i18n/locales/<code>.json` (or ask a translator), prune, and run `npm test`; it fails until every language is complete.
|
Changing the English changes the key: the old translation goes unused and the new text shows as missing. Add the translation to every `src/i18n/locales/<code>.json` (or ask a translator), prune, and run `npm test`; it fails until every language is complete.
|
||||||
|
|
||||||
|
Do not translate `zh-Hant`: it is generated. After `zh-CN`, run `npm run i18n -- --zh-hant` (see [zh-Hant.md](./translations/zh-Hant.md)).
|
||||||
|
|
||||||
To see a language, open the app with `?lang=ru`, or use the language picker (the globe button in the bottom-right corner).
|
To see a language, open the app with `?lang=ru`, or use the language picker (the globe button in the bottom-right corner).
|
||||||
|
|
||||||
An empty translation is reported, and so is one left as the English, plural forms included. When English is right on purpose (a code such as CRT, a product name), list it under the language in `src/i18n/same-as-english.json`.
|
An empty translation is reported, and so is one left as the English, plural forms included. When English is right on purpose (a code such as CRT, a product name), list it under the language in `src/i18n/same-as-english.json`.
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ npm run i18n -- --prune # drop entries the code no longer uses
|
|||||||
|
|
||||||
Changing English text changes its key: the old translation becomes unused and the new text shows as missing. Translate it and prune.
|
Changing English text changes its key: the old translation becomes unused and the new text shows as missing. Translate it and prune.
|
||||||
|
|
||||||
|
Do not translate `zh-Hant`: it is generated. Translate `zh-CN`, then run `npm run i18n -- --zh-hant` (see [zh-Hant.md](./translations/zh-Hant.md)).
|
||||||
|
|
||||||
## Fixing a translation
|
## Fixing a translation
|
||||||
|
|
||||||
Edit `src/i18n/locales/<code>.json` and run `npm run i18n`. To see your change, open the app with `?lang=<code>`.
|
Edit `src/i18n/locales/<code>.json` and run `npm run i18n`. To see your change, open the app with `?lang=<code>`.
|
||||||
|
|||||||
@@ -2,16 +2,24 @@
|
|||||||
|
|
||||||
Catalog: `src/i18n/locales/zh-Hant.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
|
Catalog: `src/i18n/locales/zh-Hant.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
|
||||||
|
|
||||||
`zh-Hant` is the Simplified Chinese catalog (`zh-CN`, credited to abiyeenzo, #914, and EDIT-Aeterna, #979) written in Traditional characters with OpenCC character conversion. Wording and terminology are the same as in `zh-CN`: change a term in [zh-CN.md](./zh-CN.md) and the `zh-CN` catalog first, then convert. Only the script differs.
|
**Do not translate or edit this catalog: it is generated.** `zh-Hant` is the Simplified Chinese catalog (`zh-CN`, credited to abiyeenzo, #914, and EDIT-Aeterna, #979) written in Traditional characters. Wording and terminology are the same as in `zh-CN`; only the script differs. To add or change an entry, edit [zh-CN.md](./zh-CN.md) and the `zh-CN` catalog, then run:
|
||||||
|
|
||||||
After `s2tw` conversion, fix the characters OpenCC picks wrongly: 年曆史 → 年歷史, 下遊 → 下游, 並道 → 併道, 發佈 → 發布, 公佈 → 公布, 控制檯 → 控制台, and 周 → 週 where it means a week (1 週/秒).
|
```sh
|
||||||
|
npm run i18n -- --zh-hant # rewrite zh-Hant.json from zh-CN.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm test` fails when `zh-Hant.json` differs from the conversion of `zh-CN.json`, naming the entry, and `npm run i18n -- --missing zh-Hant` points to this command instead of printing a template.
|
||||||
|
|
||||||
|
The command converts with OpenCC `s2tw` ([opencc-js](https://github.com/nk2028/opencc-js), `cn` → `tw`), which changes characters only. Never `s2twp`: it also swaps in Taiwan vocabulary (影片, 伺服器, 載入, 圖示), which this catalog does not use. After the conversion it fixes the characters OpenCC picks wrongly here: 年曆史 → 年歷史, 下遊 → 下游, 並道 → 併道, 發佈 → 發布, 公佈 → 公布, 控制檯 → 控制台, and 周 → 週 where it means a week (1 週/秒). The list is `ZH_HANT_CORRECTIONS` in `scripts/i18n.mjs`; when the conversion picks another wrong character, add the pair there and run the command.
|
||||||
|
|
||||||
|
An entry `zh-CN` keeps in English on purpose needs the same line under `zh-Hant` in `src/i18n/same-as-english.json`.
|
||||||
|
|
||||||
## Style
|
## Style
|
||||||
|
|
||||||
- Follow [zh-CN.md](./zh-CN.md) for wording, punctuation, spacing, units and the glossary, written in Traditional characters.
|
- Wording, punctuation, spacing, units and the glossary are those of [zh-CN.md](./zh-CN.md), written in Traditional characters.
|
||||||
- Relative-time placeholders may already include 前. In `LIVE · updated {age}`, the caller supplies the complete relative age; do not append another 前.
|
- Relative-time placeholders may already include 前. In `LIVE · updated {age}`, the caller supplies the complete relative age; do not append another 前.
|
||||||
- The picker displays 中文(繁體) / Chinese (Traditional); explicit Hans tags select Simplified Chinese.
|
- The picker displays 中文(繁體) / Chinese (Traditional); explicit Hans tags select Simplified Chinese.
|
||||||
|
|
||||||
## Plurals
|
## Plurals
|
||||||
|
|
||||||
`Intl.PluralRules('zh-Hant')` uses only `other`. Keep the measure words of the `zh-CN` entry: 「{count} 架飛機」、「{count} 艘船」.
|
`Intl.PluralRules('zh-Hant')` uses only `other`. The measure words come from the `zh-CN` entry: 「{count} 架飛機」、「{count} 艘船」.
|
||||||
Generated
+8
@@ -32,6 +32,7 @@
|
|||||||
"@vue/test-utils": "^2.5.1",
|
"@vue/test-utils": "^2.5.1",
|
||||||
"flag-icons": "7.5.0",
|
"flag-icons": "7.5.0",
|
||||||
"happy-dom": "^20.14.6",
|
"happy-dom": "^20.14.6",
|
||||||
|
"opencc-js": "1.4.2",
|
||||||
"prettier": "3.9.6",
|
"prettier": "3.9.6",
|
||||||
"puppeteer": "^25.16.0",
|
"puppeteer": "^25.16.0",
|
||||||
"sharp": "^0.35.4",
|
"sharp": "^0.35.4",
|
||||||
@@ -2976,6 +2977,13 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/opencc-js": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-EdcspgdROiCCZDM+jZR6Pa670xW+EW2QhctPBQ8gRjg6lrx4IHEl+2s+kt5izk/TkxVRgdWuFgaGkaEdIUfdaw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT AND Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/pako": {
|
"node_modules/pako": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
"@vue/test-utils": "^2.5.1",
|
"@vue/test-utils": "^2.5.1",
|
||||||
"flag-icons": "7.5.0",
|
"flag-icons": "7.5.0",
|
||||||
"happy-dom": "^20.14.6",
|
"happy-dom": "^20.14.6",
|
||||||
|
"opencc-js": "1.4.2",
|
||||||
"prettier": "3.9.6",
|
"prettier": "3.9.6",
|
||||||
"puppeteer": "^25.16.0",
|
"puppeteer": "^25.16.0",
|
||||||
"sharp": "^0.35.4",
|
"sharp": "^0.35.4",
|
||||||
|
|||||||
+121
-8
@@ -3,17 +3,21 @@
|
|||||||
// npm run i18n check every catalog and find unmarked UI text
|
// npm run i18n check every catalog and find unmarked UI text
|
||||||
// npm run i18n -- --prune also drop entries the code no longer uses
|
// npm run i18n -- --prune also drop entries the code no longer uses
|
||||||
// npm run i18n -- --missing ru print what ru lacks, as JSON to translate
|
// npm run i18n -- --missing ru print what ru lacks, as JSON to translate
|
||||||
|
// npm run i18n -- --zh-hant rewrite zh-Hant from zh-CN (see toZhHant)
|
||||||
//
|
//
|
||||||
// A translation left as the English fails unless src/i18n/same-as-english.json
|
// A translation left as the English fails unless src/i18n/same-as-english.json
|
||||||
// lists it.
|
// lists it. zh-Hant is not translated: it is generated from zh-CN, and the
|
||||||
|
// check fails when it differs.
|
||||||
//
|
//
|
||||||
// Messages are the English literals passed to t(), tn() and msg() in src/,
|
// Messages are the English literals passed to t(), tn() and msg() in src/,
|
||||||
// plus the markup opted in with data-i18n* in index.html and the Vue views.
|
// plus the markup opted in with data-i18n* in index.html and the Vue views.
|
||||||
// It also lists UI text that is not marked at all (see findUnmarked).
|
// It also lists UI text that is not marked at all (see findUnmarked).
|
||||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { isDeepStrictEqual } from 'node:util';
|
||||||
|
|
||||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const LOCALES_DIR = path.join(ROOT, 'src/i18n/locales');
|
const LOCALES_DIR = path.join(ROOT, 'src/i18n/locales');
|
||||||
@@ -618,16 +622,123 @@ export function missingTemplate(locale, catalog, messages) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zh-Hant is zh-CN in Traditional characters (docs/translations/zh-Hant.md):
|
||||||
|
// the same wording, only the script differs. OpenCC s2tw (opencc-js, cn → tw)
|
||||||
|
// converts characters only; s2twp would also swap in Taiwan vocabulary
|
||||||
|
// (影片, 伺服器, 載入), which the catalog does not use.
|
||||||
|
const ZH_HANT = 'zh-Hant';
|
||||||
|
const ZH_HANT_SOURCE = 'zh-CN';
|
||||||
|
const ZH_HANT_COMMAND = 'npm run i18n -- --zh-hant';
|
||||||
|
|
||||||
|
// Characters s2tw picks wrongly in this text, replaced after the conversion.
|
||||||
|
const ZH_HANT_CORRECTIONS = [
|
||||||
|
['年曆史', '年歷史'],
|
||||||
|
['下遊', '下游'],
|
||||||
|
['並道', '併道'],
|
||||||
|
['發佈', '發布'],
|
||||||
|
['公佈', '公布'],
|
||||||
|
['控制檯', '控制台'],
|
||||||
|
// 周 as a week: the time-lapse speed in weeks per second.
|
||||||
|
[' 周/秒', ' 週/秒'],
|
||||||
|
];
|
||||||
|
|
||||||
|
let s2tw;
|
||||||
|
|
||||||
|
/** zh-CN text in Traditional characters, as zh-Hant shows it. */
|
||||||
|
export function toZhHant(text) {
|
||||||
|
// Loaded on first use: extracting messages needs no converter, and a copy
|
||||||
|
// of this script without node_modules (stale-index.test.mjs) still runs.
|
||||||
|
s2tw ??= createRequire(import.meta.url)('opencc-js/cn2t').Converter({
|
||||||
|
from: 'cn',
|
||||||
|
to: 'tw',
|
||||||
|
});
|
||||||
|
return ZH_HANT_CORRECTIONS.reduce(
|
||||||
|
(out, [wrong, right]) => out.replaceAll(wrong, right),
|
||||||
|
s2tw(text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The zh-Hant catalog for a zh-CN catalog: the same keys in the same order. */
|
||||||
|
export function zhHantCatalog(zhCN) {
|
||||||
|
const convert = (entry) => {
|
||||||
|
if (typeof entry === 'string') return toZhHant(entry);
|
||||||
|
if (entry && typeof entry === 'object')
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(entry).map(([category, form]) => [
|
||||||
|
category,
|
||||||
|
convert(form),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(zhCN).map(([text, entry]) => [text, convert(entry)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a zh-Hant catalog is not the conversion of the zh-CN one.
|
||||||
|
* @returns {string[]} One error per entry, naming its key.
|
||||||
|
*/
|
||||||
|
export function checkZhHant(zhCN, zhHant) {
|
||||||
|
const expected = zhHantCatalog(zhCN);
|
||||||
|
const fix = `zh-Hant is generated: edit ${ZH_HANT_SOURCE}, then run ${ZH_HANT_COMMAND}`;
|
||||||
|
const errors = [];
|
||||||
|
for (const [text, entry] of Object.entries(expected)) {
|
||||||
|
if (!Object.hasOwn(zhHant, text))
|
||||||
|
errors.push(`"${text}" is in ${ZH_HANT_SOURCE} but not here. ${fix}`);
|
||||||
|
else if (!isDeepStrictEqual(zhHant[text], entry))
|
||||||
|
errors.push(
|
||||||
|
`"${text}" is ${JSON.stringify(zhHant[text])}, not ${ZH_HANT_SOURCE} converted (${JSON.stringify(entry)}). ${fix}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const text of Object.keys(zhHant)) {
|
||||||
|
if (!Object.hasOwn(expected, text))
|
||||||
|
errors.push(`"${text}" is not in ${ZH_HANT_SOURCE}. ${fix}`);
|
||||||
|
}
|
||||||
|
if (!errors.length) {
|
||||||
|
const order = Object.keys(expected);
|
||||||
|
const keys = Object.keys(zhHant);
|
||||||
|
const at = order.findIndex((text, i) => keys[i] !== text);
|
||||||
|
if (at >= 0)
|
||||||
|
errors.push(
|
||||||
|
`entries leave the ${ZH_HANT_SOURCE} order at "${order[at]}": run ${ZH_HANT_COMMAND}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialize = (catalog) => `${JSON.stringify(catalog, null, 2)}\n`;
|
||||||
|
|
||||||
|
/** Rewrite zh-Hant.json as the conversion of zh-CN.json. */
|
||||||
|
export function writeZhHant() {
|
||||||
|
const catalog = zhHantCatalog(readCatalog(ZH_HANT_SOURCE));
|
||||||
|
writeFileSync(catalogPath(ZH_HANT), serialize(catalog));
|
||||||
|
return Object.keys(catalog).length;
|
||||||
|
}
|
||||||
|
|
||||||
function main(args) {
|
function main(args) {
|
||||||
const messages = extractMessages();
|
const messages = extractMessages();
|
||||||
const missingFor = args.includes('--missing')
|
const missingFor = args.includes('--missing')
|
||||||
? args[args.indexOf('--missing') + 1]
|
? args[args.indexOf('--missing') + 1]
|
||||||
: null;
|
: null;
|
||||||
|
if (missingFor?.toLowerCase() === ZH_HANT.toLowerCase()) {
|
||||||
|
console.error(
|
||||||
|
`${ZH_HANT} is generated, not translated: add the entries to ${ZH_HANT_SOURCE} (npm run i18n -- --missing ${ZH_HANT_SOURCE}), then run ${ZH_HANT_COMMAND}`,
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if (missingFor) {
|
if (missingFor) {
|
||||||
const out = missingTemplate(missingFor, readCatalog(missingFor), messages);
|
const out = missingTemplate(missingFor, readCatalog(missingFor), messages);
|
||||||
console.log(JSON.stringify(out, null, 2));
|
console.log(JSON.stringify(out, null, 2));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (args.includes('--zh-hant')) {
|
||||||
|
const count = writeZhHant();
|
||||||
|
console.log(
|
||||||
|
`${ZH_HANT}: wrote ${count} entries converted from ${ZH_HANT_SOURCE}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
let failed = false;
|
let failed = false;
|
||||||
for (const locale of shippedLocales()) {
|
for (const locale of shippedLocales()) {
|
||||||
let catalog = readCatalog(locale);
|
let catalog = readCatalog(locale);
|
||||||
@@ -636,10 +747,7 @@ function main(args) {
|
|||||||
catalog = Object.fromEntries(
|
catalog = Object.fromEntries(
|
||||||
Object.entries(catalog).filter(([text]) => messages.has(text)),
|
Object.entries(catalog).filter(([text]) => messages.has(text)),
|
||||||
);
|
);
|
||||||
writeFileSync(
|
writeFileSync(catalogPath(locale), serialize(catalog));
|
||||||
catalogPath(locale),
|
|
||||||
`${JSON.stringify(catalog, null, 2)}\n`,
|
|
||||||
);
|
|
||||||
console.log(`${locale}: pruned ${stale.length} unused entries`);
|
console.log(`${locale}: pruned ${stale.length} unused entries`);
|
||||||
} else if (stale.length) {
|
} else if (stale.length) {
|
||||||
failed = true;
|
failed = true;
|
||||||
@@ -648,11 +756,16 @@ function main(args) {
|
|||||||
);
|
);
|
||||||
for (const text of stale) console.log(` ${JSON.stringify(text)}`);
|
for (const text of stale) console.log(` ${JSON.stringify(text)}`);
|
||||||
}
|
}
|
||||||
|
// shippedLocales() is sorted, so --prune has already pruned zh-CN.
|
||||||
|
if (locale === ZH_HANT)
|
||||||
|
errors.push(...checkZhHant(readCatalog(ZH_HANT_SOURCE), catalog));
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
failed = true;
|
failed = true;
|
||||||
console.log(
|
const fill =
|
||||||
`${locale}: ${missing.length} missing (npm run i18n -- --missing ${locale}):`,
|
locale === ZH_HANT
|
||||||
);
|
? `add them to ${ZH_HANT_SOURCE}, then ${ZH_HANT_COMMAND}`
|
||||||
|
: `npm run i18n -- --missing ${locale}`;
|
||||||
|
console.log(`${locale}: ${missing.length} missing (${fill}):`);
|
||||||
for (const text of missing)
|
for (const text of missing)
|
||||||
console.log(` ${JSON.stringify(text)} ${messages.get(text).where}`);
|
console.log(` ${JSON.stringify(text)} ${messages.get(text).where}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import assert from 'node:assert/strict';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
checkCatalog,
|
checkCatalog,
|
||||||
|
checkZhHant,
|
||||||
extractMessages,
|
extractMessages,
|
||||||
findUnmarked,
|
findUnmarked,
|
||||||
markedTemplate,
|
markedTemplate,
|
||||||
readCatalog,
|
readCatalog,
|
||||||
shippedLocales,
|
shippedLocales,
|
||||||
|
toZhHant,
|
||||||
unmarkedMarkup,
|
unmarkedMarkup,
|
||||||
unmarkedScript,
|
unmarkedScript,
|
||||||
|
zhHantCatalog,
|
||||||
} from '../../scripts/i18n.mjs';
|
} from '../../scripts/i18n.mjs';
|
||||||
import { LOCALES } from './locale.js';
|
import { LOCALES } from './locale.js';
|
||||||
|
|
||||||
@@ -36,6 +39,59 @@ for (const locale of shippedLocales()) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('zh-Hant is zh-CN converted to Traditional characters (npm run i18n -- --zh-hant)', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
checkZhHant(readCatalog('zh-CN'), readCatalog('zh-Hant')),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toZhHant converts characters, not vocabulary, and fixes what s2tw picks wrongly', () => {
|
||||||
|
// s2twp would write 影片, 伺服器 and 載入; zh-Hant keeps the zh-CN words.
|
||||||
|
assert.equal(toZhHant('视频'), '視頻');
|
||||||
|
assert.equal(toZhHant('服务器'), '服務器');
|
||||||
|
assert.equal(toZhHant('加载'), '加載');
|
||||||
|
assert.equal(toZhHant('50 年历史'), '50 年歷史');
|
||||||
|
assert.equal(toZhHant('发布于 {date}'), '發布於 {date}');
|
||||||
|
assert.equal(toZhHant('1 周/秒'), '1 週/秒');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checkZhHant names each entry that is not the conversion of zh-CN', () => {
|
||||||
|
const zhCN = {
|
||||||
|
Video: '视频',
|
||||||
|
Server: '服务器',
|
||||||
|
'{count} ships': { other: '{count} 艘船' },
|
||||||
|
};
|
||||||
|
const generated = zhHantCatalog(zhCN);
|
||||||
|
assert.deepEqual(generated, {
|
||||||
|
Video: '視頻',
|
||||||
|
Server: '服務器',
|
||||||
|
'{count} ships': { other: '{count} 艘船' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(checkZhHant(zhCN, generated), []);
|
||||||
|
|
||||||
|
const fix =
|
||||||
|
'zh-Hant is generated: edit zh-CN, then run npm run i18n -- --zh-hant';
|
||||||
|
const { Server, ...withoutServer } = generated;
|
||||||
|
assert.deepEqual(
|
||||||
|
checkZhHant(zhCN, {
|
||||||
|
...withoutServer,
|
||||||
|
Video: '影片',
|
||||||
|
'{count} ships': { other: '{count} 艘船隻' },
|
||||||
|
Extra: '多餘',
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
`"Video" is "影片", not zh-CN converted ("視頻"). ${fix}`,
|
||||||
|
`"Server" is in zh-CN but not here. ${fix}`,
|
||||||
|
`"{count} ships" is {"other":"{count} 艘船隻"}, not zh-CN converted ({"other":"{count} 艘船"}). ${fix}`,
|
||||||
|
`"Extra" is not in zh-CN. ${fix}`,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.deepEqual(checkZhHant(zhCN, { Server, ...withoutServer }), [
|
||||||
|
'entries leave the zh-CN order at "Video": run npm run i18n -- --zh-hant',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('every UI string is marked for translation or kept on purpose (npm run i18n)', () => {
|
test('every UI string is marked for translation or kept on purpose (npm run i18n)', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
findUnmarked().map(({ where, text }) => `${where} ${text}`),
|
findUnmarked().map(({ where, text }) => `${where} ${text}`),
|
||||||
|
|||||||
@@ -612,6 +612,7 @@
|
|||||||
"Cyclone advisories unavailable": "氣旋公告不可用",
|
"Cyclone advisories unavailable": "氣旋公告不可用",
|
||||||
"Cyclone data unavailable": "氣旋數據不可用",
|
"Cyclone data unavailable": "氣旋數據不可用",
|
||||||
"Potential tropical cyclone": "潛在熱帶氣旋",
|
"Potential tropical cyclone": "潛在熱帶氣旋",
|
||||||
|
"Post-tropical cyclone": "後熱帶氣旋",
|
||||||
"Hurricane": "颶風",
|
"Hurricane": "颶風",
|
||||||
"Tropical storm": "熱帶風暴",
|
"Tropical storm": "熱帶風暴",
|
||||||
"Tropical depression": "熱帶低壓",
|
"Tropical depression": "熱帶低壓",
|
||||||
@@ -707,11 +708,15 @@
|
|||||||
"Merge onto {road}": "併入 {road}",
|
"Merge onto {road}": "併入 {road}",
|
||||||
"maneuver|Merge": "併道",
|
"maneuver|Merge": "併道",
|
||||||
"Take the ramp on the {direction} onto {road}": "{direction}駛入匝道,進入 {road}",
|
"Take the ramp on the {direction} onto {road}": "{direction}駛入匝道,進入 {road}",
|
||||||
|
"Take the ramp {direction} onto {road}": "{direction}駛入匝道,進入 {road}",
|
||||||
"Take the ramp on the {direction}": "{direction}駛入匝道",
|
"Take the ramp on the {direction}": "{direction}駛入匝道",
|
||||||
|
"Take the ramp {direction}": "{direction}駛入匝道",
|
||||||
"Take the ramp onto {road}": "駛入匝道,進入 {road}",
|
"Take the ramp onto {road}": "駛入匝道,進入 {road}",
|
||||||
"Take the ramp": "駛入匝道",
|
"Take the ramp": "駛入匝道",
|
||||||
"Take the exit on the {direction} onto {road}": "從出口{direction}駛出,進入 {road}",
|
"Take the exit on the {direction} onto {road}": "從出口{direction}駛出,進入 {road}",
|
||||||
|
"Take the exit {direction} onto {road}": "從出口{direction}駛出,進入 {road}",
|
||||||
"Take the exit on the {direction}": "從出口{direction}駛出",
|
"Take the exit on the {direction}": "從出口{direction}駛出",
|
||||||
|
"Take the exit {direction}": "從出口{direction}駛出",
|
||||||
"Take the exit onto {road}": "從出口駛出,進入 {road}",
|
"Take the exit onto {road}": "從出口駛出,進入 {road}",
|
||||||
"Take the exit": "從出口駛出",
|
"Take the exit": "從出口駛出",
|
||||||
"At the roundabout, take the {exit} exit onto {road}": "在環島從{exit}出口駛出,進入 {road}",
|
"At the roundabout, take the {exit} exit onto {road}": "在環島從{exit}出口駛出,進入 {road}",
|
||||||
@@ -1599,6 +1604,9 @@
|
|||||||
"LIVE SIGNALS": "實時信號",
|
"LIVE SIGNALS": "實時信號",
|
||||||
"CCTV layer unavailable": "CCTV 圖層不可用",
|
"CCTV layer unavailable": "CCTV 圖層不可用",
|
||||||
"Link copied!": "鏈接已複製!",
|
"Link copied!": "鏈接已複製!",
|
||||||
|
"Link shared": "鏈接已分享",
|
||||||
|
"This share link can't be opened": "無法打開此分享鏈接",
|
||||||
|
"Shared vessel is unavailable": "分享的船舶不可用",
|
||||||
"Copy failed": "複製失敗",
|
"Copy failed": "複製失敗",
|
||||||
"Return map to straight-down view": "將地圖恢復為垂直俯視",
|
"Return map to straight-down view": "將地圖恢復為垂直俯視",
|
||||||
"Tilt map to oblique view": "將地圖傾斜至斜視角",
|
"Tilt map to oblique view": "將地圖傾斜至斜視角",
|
||||||
@@ -1952,6 +1960,8 @@
|
|||||||
"ORBIT": "環繞",
|
"ORBIT": "環繞",
|
||||||
"Location not found": "未找到位置",
|
"Location not found": "未找到位置",
|
||||||
"Search failed": "搜索失敗",
|
"Search failed": "搜索失敗",
|
||||||
|
"Check the spelling and press Enter to search again.": "請檢查拼寫,然後按 Enter 重新搜索。",
|
||||||
|
"Press Enter to try again.": "按 Enter 重試。",
|
||||||
"Detailed outline unavailable": "無法獲取詳細輪廓",
|
"Detailed outline unavailable": "無法獲取詳細輪廓",
|
||||||
"Fly to a POI first": "請先飛往一個 POI",
|
"Fly to a POI first": "請先飛往一個 POI",
|
||||||
"Reset to full globe view": "重置為完整地球視圖",
|
"Reset to full globe view": "重置為完整地球視圖",
|
||||||
@@ -2977,16 +2987,6 @@
|
|||||||
"Search": "搜索",
|
"Search": "搜索",
|
||||||
"More": "更多",
|
"More": "更多",
|
||||||
"Panels": "面板",
|
"Panels": "面板",
|
||||||
"Post-tropical cyclone": "後熱帶氣旋",
|
|
||||||
"Take the ramp {direction} onto {road}": "{direction}駛入匝道,進入 {road}",
|
|
||||||
"Take the ramp {direction}": "{direction}駛入匝道",
|
|
||||||
"Take the exit {direction} onto {road}": "從出口{direction}駛出,進入 {road}",
|
|
||||||
"Take the exit {direction}": "從出口{direction}駛出",
|
|
||||||
"Link shared": "鏈接已分享",
|
|
||||||
"This share link can't be opened": "無法打開此分享鏈接",
|
|
||||||
"Shared vessel is unavailable": "分享的船舶不可用",
|
|
||||||
"Check the spelling and press Enter to search again.": "請檢查拼寫,然後按 Enter 重新搜索。",
|
|
||||||
"Press Enter to try again.": "按 Enter 重試。",
|
|
||||||
"Recent speech": "最近語音",
|
"Recent speech": "最近語音",
|
||||||
"Voice Settings": "語音設置",
|
"Voice Settings": "語音設置",
|
||||||
"Preview": "預覽",
|
"Preview": "預覽",
|
||||||
@@ -3025,8 +3025,19 @@
|
|||||||
"other": "{count} 個地標"
|
"other": "{count} 個地標"
|
||||||
},
|
},
|
||||||
"S2 Underground publishes these KMZ products to GitHub. New uploads are detected within seconds and replace the previous edition automatically. Click a marker for its report.": "S2 Underground 在 GitHub 發布這些 KMZ 產品。新上傳的內容會在數秒內被檢測到,並自動替換舊版本。點擊標記查看報告。",
|
"S2 Underground publishes these KMZ products to GitHub. New uploads are detected within seconds and replace the previous edition automatically. Click a marker for its report.": "S2 Underground 在 GitHub 發布這些 KMZ 產品。新上傳的內容會在數秒內被檢測到,並自動替換舊版本。點擊標記查看報告。",
|
||||||
"routing busy — too many routes at once": "路線服務繁忙:同時請求的路線過多",
|
"fire confidence filter|All": "全部",
|
||||||
"route proxy error": "路線代理出錯",
|
"fire confidence filter|Nominal+": "中+",
|
||||||
|
"fire confidence filter|High": "高",
|
||||||
|
"Every detection — many of the strongest fires are rated low confidence": "所有火點探測——許多強度最高的火點被評為低置信度",
|
||||||
|
"Nominal and high confidence detections": "中、高置信度的火點探測",
|
||||||
|
"High confidence detections only": "僅高置信度的火點探測",
|
||||||
|
"Fire intensity (FRP)": "火勢強度(FRP)",
|
||||||
|
"≥ {value} MW": "≥ {value} MW",
|
||||||
|
"Confidence": "置信度",
|
||||||
|
"Minimum fire radiative power, megawatts": "最小火輻射功率(兆瓦)",
|
||||||
|
"fire intensity filter|All": "全部",
|
||||||
|
"Detection confidence": "探測置信度",
|
||||||
|
"{shown} of {total} shown": "顯示 {shown} / {total}",
|
||||||
"{lower} to {upper}": "{lower} 至 {upper}",
|
"{lower} to {upper}": "{lower} 至 {upper}",
|
||||||
"Up to {upper}": "最高 {upper}",
|
"Up to {upper}": "最高 {upper}",
|
||||||
"From {lower}": "最低 {lower}",
|
"From {lower}": "最低 {lower}",
|
||||||
@@ -3039,6 +3050,8 @@
|
|||||||
"Prohibited": "禁區",
|
"Prohibited": "禁區",
|
||||||
"Restricted": "限制區",
|
"Restricted": "限制區",
|
||||||
"Prohibited & Restricted Airspace": "禁區與限制區空域",
|
"Prohibited & Restricted Airspace": "禁區與限制區空域",
|
||||||
|
"routing busy — too many routes at once": "路線服務繁忙:同時請求的路線過多",
|
||||||
|
"route proxy error": "路線代理出錯",
|
||||||
"AUTHORING": "創作",
|
"AUTHORING": "創作",
|
||||||
"Create and edit scenes": "創建和編輯場景",
|
"Create and edit scenes": "創建和編輯場景",
|
||||||
"SCENE": "場景",
|
"SCENE": "場景",
|
||||||
@@ -3064,19 +3077,6 @@
|
|||||||
"Download this scene as JSON or an asset bundle": "將此場景下載為 JSON 或資源包",
|
"Download this scene as JSON or an asset bundle": "將此場景下載為 JSON 或資源包",
|
||||||
"Left panels": "左側面板",
|
"Left panels": "左側面板",
|
||||||
"Right panels": "右側面板",
|
"Right panels": "右側面板",
|
||||||
"fire confidence filter|All": "全部",
|
|
||||||
"fire confidence filter|Nominal+": "中+",
|
|
||||||
"fire confidence filter|High": "高",
|
|
||||||
"Every detection — many of the strongest fires are rated low confidence": "所有火點探測——許多強度最高的火點被評為低置信度",
|
|
||||||
"Nominal and high confidence detections": "中、高置信度的火點探測",
|
|
||||||
"High confidence detections only": "僅高置信度的火點探測",
|
|
||||||
"Fire intensity (FRP)": "火勢強度(FRP)",
|
|
||||||
"≥ {value} MW": "≥ {value} MW",
|
|
||||||
"Confidence": "置信度",
|
|
||||||
"Minimum fire radiative power, megawatts": "最小火輻射功率(兆瓦)",
|
|
||||||
"fire intensity filter|All": "全部",
|
|
||||||
"Detection confidence": "探測置信度",
|
|
||||||
"{shown} of {total} shown": "顯示 {shown} / {total}",
|
|
||||||
"UTILITIES": "工具",
|
"UTILITIES": "工具",
|
||||||
"Exit cockpit to fly to a drawing": "請先退出座艙,再飛往已繪製的形狀",
|
"Exit cockpit to fly to a drawing": "請先退出座艙,再飛往已繪製的形狀",
|
||||||
"Directions, drawing and your own files": "路線導航、繪製和你自己的文件",
|
"Directions, drawing and your own files": "路線導航、繪製和你自己的文件",
|
||||||
|
|||||||
Reference in new issue
Block a user