From 5d28d614e07197a69bfecaa3f1818ff7916ac166 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Fri, 13 Nov 2026 18:46:27 +0400 Subject: [PATCH] i18n: generate zh-Hant from zh-CN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zh-Hant is the zh-CN text in Traditional characters, but contributors still translated it by hand from `--missing zh-Hant`, and a model asked for Traditional Chinese writes Taiwan vocabulary (影片, 伺服器, 載入). `npm run i18n -- --zh-hant` now rewrites zh-Hant.json from zh-CN.json with OpenCC s2tw (opencc-js 1.4.2, cn -> tw, characters only), then the fixed list of characters it picks wrongly. The check that `npm test` runs fails when zh-Hant differs from that conversion, naming the key, and `--missing zh-Hant` points to the command instead of printing a template. The conversion reproduces every one of the 3346 entries on main. 25 keys were out of zh-CN order and move; no value changes. The converter loads on first use, so a copy of the script without node_modules (stale-index.test.mjs) still extracts messages. --- docs/CURRENT-STATE.md | 4 +- docs/I18N.md | 2 + docs/TRANSLATORS.md | 2 + docs/translations/zh-Hant.md | 16 +++-- package-lock.json | 8 +++ package.json | 1 + scripts/i18n.mjs | 129 +++++++++++++++++++++++++++++++--- src/i18n/catalog.test.mjs | 56 +++++++++++++++ src/i18n/locales/zh-Hant.json | 50 ++++++------- 9 files changed, 230 insertions(+), 38 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 6073b5c84..e5ebea3e2 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -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`) extracts every message and fails on missing, empty, unused, 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 fields, voice tool schemas, provider names, feed data (callsigns, vessel and station names) and attribution. diff --git a/docs/I18N.md b/docs/I18N.md index bacfdc509..1326790e1 100644 --- a/docs/I18N.md +++ b/docs/I18N.md @@ -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/.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). 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`. diff --git a/docs/TRANSLATORS.md b/docs/TRANSLATORS.md index a1e108379..48acab242 100644 --- a/docs/TRANSLATORS.md +++ b/docs/TRANSLATORS.md @@ -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. +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 Edit `src/i18n/locales/.json` and run `npm run i18n`. To see your change, open the app with `?lang=`. diff --git a/docs/translations/zh-Hant.md b/docs/translations/zh-Hant.md index d44c510c4..9b58ba347 100644 --- a/docs/translations/zh-Hant.md +++ b/docs/translations/zh-Hant.md @@ -2,16 +2,24 @@ 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 -- 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 前. - The picker displays 中文(繁體) / Chinese (Traditional); explicit Hans tags select Simplified Chinese. ## 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} 艘船」. diff --git a/package-lock.json b/package-lock.json index df28aba4b..6b2fe1823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@vue/test-utils": "^2.5.1", "flag-icons": "7.5.0", "happy-dom": "^20.14.6", + "opencc-js": "1.4.2", "prettier": "3.9.6", "puppeteer": "^25.16.0", "sharp": "^0.35.4", @@ -2976,6 +2977,13 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", diff --git a/package.json b/package.json index c1476342d..efc50f0b5 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@vue/test-utils": "^2.5.1", "flag-icons": "7.5.0", "happy-dom": "^20.14.6", + "opencc-js": "1.4.2", "prettier": "3.9.6", "puppeteer": "^25.16.0", "sharp": "^0.35.4", diff --git a/scripts/i18n.mjs b/scripts/i18n.mjs index c424666a2..01321605a 100644 --- a/scripts/i18n.mjs +++ b/scripts/i18n.mjs @@ -3,17 +3,21 @@ // 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 -- --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 -// 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/, // 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). import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; +import { createRequire } from 'node:module'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isDeepStrictEqual } from 'node:util'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const LOCALES_DIR = path.join(ROOT, 'src/i18n/locales'); @@ -618,16 +622,123 @@ export function missingTemplate(locale, catalog, messages) { 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) { const messages = extractMessages(); const missingFor = args.includes('--missing') ? args[args.indexOf('--missing') + 1] : 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) { const out = missingTemplate(missingFor, readCatalog(missingFor), messages); console.log(JSON.stringify(out, null, 2)); 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; for (const locale of shippedLocales()) { let catalog = readCatalog(locale); @@ -636,10 +747,7 @@ function main(args) { catalog = Object.fromEntries( Object.entries(catalog).filter(([text]) => messages.has(text)), ); - writeFileSync( - catalogPath(locale), - `${JSON.stringify(catalog, null, 2)}\n`, - ); + writeFileSync(catalogPath(locale), serialize(catalog)); console.log(`${locale}: pruned ${stale.length} unused entries`); } else if (stale.length) { failed = true; @@ -648,11 +756,16 @@ function main(args) { ); 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) { failed = true; - console.log( - `${locale}: ${missing.length} missing (npm run i18n -- --missing ${locale}):`, - ); + const fill = + 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) console.log(` ${JSON.stringify(text)} ${messages.get(text).where}`); } diff --git a/src/i18n/catalog.test.mjs b/src/i18n/catalog.test.mjs index 8107a86e9..9b6bb5d9b 100644 --- a/src/i18n/catalog.test.mjs +++ b/src/i18n/catalog.test.mjs @@ -3,13 +3,16 @@ import assert from 'node:assert/strict'; import { checkCatalog, + checkZhHant, extractMessages, findUnmarked, markedTemplate, readCatalog, shippedLocales, + toZhHant, unmarkedMarkup, unmarkedScript, + zhHantCatalog, } from '../../scripts/i18n.mjs'; 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)', () => { assert.deepEqual( findUnmarked().map(({ where, text }) => `${where} ${text}`), diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index fd201af36..55874fcd8 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -612,6 +612,7 @@ "Cyclone advisories unavailable": "氣旋公告不可用", "Cyclone data unavailable": "氣旋數據不可用", "Potential tropical cyclone": "潛在熱帶氣旋", + "Post-tropical cyclone": "後熱帶氣旋", "Hurricane": "颶風", "Tropical storm": "熱帶風暴", "Tropical depression": "熱帶低壓", @@ -707,11 +708,15 @@ "Merge onto {road}": "併入 {road}", "maneuver|Merge": "併道", "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 {direction}": "{direction}駛入匝道", "Take the ramp onto {road}": "駛入匝道,進入 {road}", "Take the ramp": "駛入匝道", "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 {direction}": "從出口{direction}駛出", "Take the exit onto {road}": "從出口駛出,進入 {road}", "Take the exit": "從出口駛出", "At the roundabout, take the {exit} exit onto {road}": "在環島從{exit}出口駛出,進入 {road}", @@ -1599,6 +1604,9 @@ "LIVE SIGNALS": "實時信號", "CCTV layer unavailable": "CCTV 圖層不可用", "Link copied!": "鏈接已複製!", + "Link shared": "鏈接已分享", + "This share link can't be opened": "無法打開此分享鏈接", + "Shared vessel is unavailable": "分享的船舶不可用", "Copy failed": "複製失敗", "Return map to straight-down view": "將地圖恢復為垂直俯視", "Tilt map to oblique view": "將地圖傾斜至斜視角", @@ -1952,6 +1960,8 @@ "ORBIT": "環繞", "Location not found": "未找到位置", "Search failed": "搜索失敗", + "Check the spelling and press Enter to search again.": "請檢查拼寫,然後按 Enter 重新搜索。", + "Press Enter to try again.": "按 Enter 重試。", "Detailed outline unavailable": "無法獲取詳細輪廓", "Fly to a POI first": "請先飛往一個 POI", "Reset to full globe view": "重置為完整地球視圖", @@ -2977,16 +2987,6 @@ "Search": "搜索", "More": "更多", "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": "最近語音", "Voice Settings": "語音設置", "Preview": "預覽", @@ -3025,8 +3025,19 @@ "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 產品。新上傳的內容會在數秒內被檢測到,並自動替換舊版本。點擊標記查看報告。", - "routing busy — too many routes at once": "路線服務繁忙:同時請求的路線過多", - "route proxy error": "路線代理出錯", + "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}", "{lower} to {upper}": "{lower} 至 {upper}", "Up to {upper}": "最高 {upper}", "From {lower}": "最低 {lower}", @@ -3039,6 +3050,8 @@ "Prohibited": "禁區", "Restricted": "限制區", "Prohibited & Restricted Airspace": "禁區與限制區空域", + "routing busy — too many routes at once": "路線服務繁忙:同時請求的路線過多", + "route proxy error": "路線代理出錯", "AUTHORING": "創作", "Create and edit scenes": "創建和編輯場景", "SCENE": "場景", @@ -3064,19 +3077,6 @@ "Download this scene as JSON or an asset bundle": "將此場景下載為 JSON 或資源包", "Left 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": "工具", "Exit cockpit to fly to a drawing": "請先退出座艙,再飛往已繪製的形狀", "Directions, drawing and your own files": "路線導航、繪製和你自己的文件",