i18n(ar): apply review round 3

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-28 23:27:00 +04:00
1 parent 8540535817
commit df866f33a5
13 files changed
+110 -38

No files matched your search

+7 -5
View File
@@ -8,8 +8,8 @@ Translation sources: abiyeenzo (#915).
- Modern Standard Arabic, neutral across regions. Address the user with plain imperatives: «انقر»، «فعّل»، «أعد المحاولة». - Modern Standard Arabic, neutral across regions. Address the user with plain imperatives: «انقر»، «فعّل»، «أعد المحاولة».
- Arabic has no letter case: an UPPERCASE English HUD label becomes the same short Arabic label. Keep labels as short as the English: «مباشر»، «متقادم»، «متوقف». - Arabic has no letter case: an UPPERCASE English HUD label becomes the same short Arabic label. Keep labels as short as the English: «مباشر»، «متقادم»، «متوقف».
- Status words in progress use «جارٍ …» («جارٍ التحميل»، «… جارٍ تشغيل اللقطة»); finished actions use the passive («تم الحفظ»، «أُلغي التحميل»). Data that refreshes itself «يُحدَّث» (يتحدّث means "speaks"). A sentence about a `{placeholder}` whose gender and number vary stays impersonal: «{layers}: تم التفعيل للتو». - Status words in progress use «جارٍ …» («جارٍ التحميل»، «جارٍ الضبط»، «… جارٍ تشغيل اللقطة»), so a status chip never reads as the command next to it («ضبط» is the TUNE button); finished actions use the passive («تم الحفظ»، «أُلغي التحميل»). Data that refreshes itself «يُحدَّث» (يتحدّث means "speaks"). A sentence about a `{placeholder}` whose gender and number vary stays impersonal: «{layers}: تم التفعيل للتو».
- A label followed by its value takes a colon, so it does not read as a sentence: «الصوت: نشط»، «السياق: {value}». - A label followed by its value takes a colon, so it does not read as a sentence: «الصوت: نشط»، «السياق: {value}»، «الاحتواء: {percent}%». A named mode or control quoted in a sentence takes «»: «يظهر في وضع «كثيف» فقط».
- Arabic punctuation in Arabic text: ، ؛ ؟ and «» for quotes. Keep `·` as the HUD separator, `—` with spaces, and `…`. - Arabic punctuation in Arabic text: ، ؛ ؟ and «» for quotes. Keep `·` as the HUD separator, `—` with spaces, and `…`.
- Arrows that show a sequence point the way Arabic reads: «الإشارات ← الأخبار ← المحلي», «{origin} ← {destination}». - Arrows that show a sequence point the way Arabic reads: «الإشارات ← الأخبار ← المحلي», «{origin} ← {destination}».
- Product names, provider names and codes stay in Latin letters: Google, Cesium ion, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, Street View, Mapillary. Aviation and camera abbreviations stay Latin too: HDG, FOV, BRG, CRS, GS, TRK, V/S, SQUAWK. Local place names from the Bhote Koshi scene (Rasuwagadhi, Bidur, Timure…) stay Latin; well-known cities and countries are written in Arabic (أوستن، طوكيو، نيبال). - Product names, provider names and codes stay in Latin letters: Google, Cesium ion, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, Street View, Mapillary. Aviation and camera abbreviations stay Latin too: HDG, FOV, BRG, CRS, GS, TRK, V/S, SQUAWK. Local place names from the Bhote Koshi scene (Rasuwagadhi, Bidur, Timure…) stay Latin; well-known cities and countries are written in Arabic (أوستن، طوكيو، نيبال).
@@ -22,7 +22,7 @@ Translation sources: abiyeenzo (#915).
- Latin runs inside Arabic sentences (product names, codes, `{placeholders}` filled with English data) are isolated by the bidi algorithm; write them as plain text, without manual direction marks. - Latin runs inside Arabic sentences (product names, codes, `{placeholders}` filled with English data) are isolated by the bidi algorithm; write them as plain text, without manual direction marks.
- Position counters: a translated counter says «من» («القناة {index} من {total}», «الإطار {index} من {total}»); a spaced slash between two isolated numbers swaps them in right-to-left text. Counters the code builds from bare numbers (`01 / 25`, `1 / 3`) are shown left to right by `rtl.css`. - Position counters: a translated counter says «من» («القناة {index} من {total}», «الإطار {index} من {total}»); a spaced slash between two isolated numbers swaps them in right-to-left text. Counters the code builds from bare numbers (`01 / 25`, `1 / 3`) are shown left to right by `rtl.css`.
- Code stays left to right: the JSON fields of the scene editor are LTR in every language. - Code stays left to right: the JSON fields of the scene editor are LTR in every language.
- Comparison signs `<` and `>` are mirrored glyphs in right-to-left text («< 2%» shows as «> 2%»). Write them as words: «أقل من 2%»، «أكثر من 10%»، «أقل من ساعة». A legend's open upper bound («300+», «-40») is isolated by `formatScaleLabel()`, so its sign stays where it is written. - Comparison signs `<` and `>` are mirrored glyphs in right-to-left text («< 2%» shows as «> 2%»). Write them as words: «أقل من 2%»، «أكثر من 10%»، «أقل من ساعة». A legend's open upper bound («300+», «-40») is isolated by `formatScaleLabel()`, so its sign stays where it is written; `formatScaleRange()` isolates the unit after the range too («°C», not «C°»).
## Units ## Units
@@ -71,12 +71,13 @@ Every form must keep `{count}`. For one and two, Arabic names the quantity with
| installation, site / mapped | منشأة، موقع / مرسوم | | installation, site / mapped | منشأة، موقع / مرسوم |
| camera / CCTV camera | كاميرا / كاميرا CCTV | | camera / CCTV camera | كاميرا / كاميرا CCTV |
| earthquake / active fire / fire perimeter | زلزال / حريق نشط / محيط حريق | | earthquake / active fire / fire perimeter | زلزال / حريق نشط / محيط حريق |
| containment (wildfire) | احتواء (not السيطرة) |
| cyclone advisory | نشرة إعصار | | cyclone advisory | نشرة إعصار |
| scene / shot / scene recipe | مشهد / لقطة / قالب المشهد | | scene / shot / scene recipe | مشهد / لقطة / قالب المشهد |
| briefing / panel / dock | إحاطة / لوحة / الشريط السفلي | | briefing / panel / dock | إحاطة / لوحة / الشريط السفلي |
| preset / visual preset | إعداد مسبق / نمط بصري | | preset / visual preset | إعداد مسبق / نمط بصري |
| Scope (circular viewport mask) / keyhole | المنظار / الفتحة | | Scope (circular viewport mask) / keyhole | المنظار / الفتحة |
| Clean UI | واجهة نظيفة | | Clean UI / EXIT CLEAN VIEW | واجهة نظيفة / الخروج من الواجهة النظيفة (not العرض) |
| cockpit | القمرة (قمرة القيادة) | | cockpit | القمرة (قمرة القيادة) |
| heading / altitude / ground speed | الاتجاه (HDG) / الارتفاع / السرعة الأرضية | | heading / altitude / ground speed | الاتجاه (HDG) / الارتفاع / السرعة الأرضية |
| Street Level / Street View | مستوى الشارع / Street View | | Street Level / Street View | مستوى الشارع / Street View |
@@ -90,7 +91,7 @@ Every form must keep `{count}`. For one and two, Arabic names the quantity with
| forecast / valid (time) / issued | توقع / عند، وقت التوقع (not صالح حتى) / صدر | | forecast / valid (time) / issued | توقع / عند، وقت التوقع (not صالح حتى) / صدر |
| observation / observed | رصد / مرصود | | observation / observed | رصد / مرصود |
| heard (aircraft received by the SDR) | الملتقطة | | heard (aircraft received by the SDR) | الملتقطة |
| upstream (camera feed) | المصدر الأصلي | | upstream (camera feed) | المصدر الأصلي (in every CCTV message, not bare المصدر) |
| field (weather) / model | حقل / نموذج | | field (weather) / model | حقل / نموذج |
| parameters / ellipsoid | المعلمات / المجسّم الإهليلجي | | parameters / ellipsoid | المعلمات / المجسّم الإهليلجي |
| scene cloud | غيوم المشهد | | scene cloud | غيوم المشهد |
@@ -100,6 +101,7 @@ Every form must keep `{count}`. For one and two, Arabic names the quantity with
| share (a view or link) | مشاركة | | share (a view or link) | مشاركة |
| AI agent | وكيل ذكاء اصطناعي | | AI agent | وكيل ذكاء اصطناعي |
| stage (rocket) / downrange | مرحلة / على امتداد المسار | | stage (rocket) / downrange | مرحلة / على امتداد المسار |
| landing position: CONFIRMED / PAD (RTLS) | مؤكَّد / منصة الإطلاق |
| recovery (rocket stage) | استعادة (not استرجاع) | | recovery (rocket stage) | استعادة (not استرجاع) |
| Arrive at your destination (route step) | الوصول إلى وجهتك (a step, not وصلت) | | Arrive at your destination (route step) | الوصول إلى وجهتك (a step, not وصلت) |
| forecast lead hours | مدة التوقع بالساعات (not ساعات المهلة) | | forecast lead hours | مدة التوقع بالساعات (not ساعات المهلة) |
+10 -1
View File
@@ -18,7 +18,11 @@ import {
import { hudQuantity, LOCALIZED_LOCALITY } from '../hudLocalityFormat.js'; import { hudQuantity, LOCALIZED_LOCALITY } from '../hudLocalityFormat.js';
import { formatCockpitDistance } from '../ui/cockpitPresentation.js'; import { formatCockpitDistance } from '../ui/cockpitPresentation.js';
import { READOUT_WORDS } from '../layers/recentImagery/display.js'; import { READOUT_WORDS } from '../layers/recentImagery/display.js';
import { formatQuantity, formatScaleLabel } from '../ui/unitLabels.js'; import {
formatQuantity,
formatScaleLabel,
formatScaleRange,
} from '../ui/unitLabels.js';
test('Arabic resolves from URL, saved preference and browser language', () => { test('Arabic resolves from URL, saved preference and browser language', () => {
assert.equal( assert.equal(
@@ -133,6 +137,11 @@ test('Arabic uses all six plural forms and spaced HUD units', () => {
'\u2068300+\u2069', '\u2068300+\u2069',
'\u20680.1\u2069', '\u20680.1\u2069',
]); ]);
// A Latin unit after the range keeps its order: °C, not C°.
assert.equal(
formatScaleRange('-40', '50+', '°C'),
`${formatScaleLabel('-40')} — \u206850+\u2069\u00a0\u2068°C\u2069`,
);
// Counted minutes agree with the number (3–10 plural, 11+ singular). // Counted minutes agree with the number (3–10 plural, 11+ singular).
assert.deepEqual( assert.deepEqual(
[3, 15].map((n) => [3, 15].map((n) =>
+18 -16
View File
@@ -166,7 +166,7 @@
"READY": "جاهز", "READY": "جاهز",
"ACTIVE STYLE": "النمط النشط", "ACTIVE STYLE": "النمط النشط",
"Return UI controls": "إعادة عناصر التحكم", "Return UI controls": "إعادة عناصر التحكم",
"EXIT CLEAN VIEW": "الخروج من العرض النظيف", "EXIT CLEAN VIEW": "الخروج من الواجهة النظيفة",
"Expand Visual Presets": "توسيع الأنماط البصرية", "Expand Visual Presets": "توسيع الأنماط البصرية",
"VISUAL PRESETS": "الأنماط البصرية", "VISUAL PRESETS": "الأنماط البصرية",
"Pin visual presets": "تثبيت الأنماط البصرية", "Pin visual presets": "تثبيت الأنماط البصرية",
@@ -551,7 +551,7 @@
"PROJECTION ON": "الإسقاط مفعّل", "PROJECTION ON": "الإسقاط مفعّل",
"CALIBRATION": "المعايرة", "CALIBRATION": "المعايرة",
"ADJUST": "ضبط", "ADJUST": "ضبط",
"Drag the camera in the world: rings rotate, arrows move, handles set range/FOV": "اسحب الكاميرا في العالم: تدور الحلقات وتحرّك الأسهم الموضع وتضبط المقابض المدى وزاوية الرؤية", "Drag the camera in the world: rings rotate, arrows move, handles set range/FOV": "اسحب الكاميرا في العالم: الحلقات لتدويرها، والأسهم لتحريكها، والمقابض لضبط المدى وزاوية الرؤية",
"Camera pose — click a value to type": "وضعية الكاميرا — انقر على قيمة لكتابتها", "Camera pose — click a value to type": "وضعية الكاميرا — انقر على قيمة لكتابتها",
"Heading (compass °) — click to type": "الاتجاه (بوصلة °) — انقر للكتابة", "Heading (compass °) — click to type": "الاتجاه (بوصلة °) — انقر للكتابة",
"Pitch (° up/down) — click to type": "الميل (° أعلى/أسفل) — انقر للكتابة", "Pitch (° up/down) — click to type": "الميل (° أعلى/أسفل) — انقر للكتابة",
@@ -814,7 +814,7 @@
"VISUAL": "مرئية", "VISUAL": "مرئية",
"Brightest naked-eye objects — CelesTrak visual group": "ألمع الأجرام المرئية بالعين المجردة — مجموعة CelesTrak المرئية", "Brightest naked-eye objects — CelesTrak visual group": "ألمع الأجرام المرئية بالعين المجردة — مجموعة CelesTrak المرئية",
"COMMS": "اتصالات", "COMMS": "اتصالات",
"Broadband constellation shell — shown only in DENSE mode": "غلاف كوكبة النطاق العريض — يظهر في وضع الكثيف فقط", "Broadband constellation shell — shown only in DENSE mode": "غلاف كوكبة النطاق العريض — يظهر في وضع «كثيف» فقط",
"refreshing...": "جارٍ التحديث…", "refreshing...": "جارٍ التحديث…",
"KEY REQUIRED": "المفتاح مطلوب", "KEY REQUIRED": "المفتاح مطلوب",
"STALE · cached {age}": "متقادم · مخزّن {age}", "STALE · cached {age}": "متقادم · مخزّن {age}",
@@ -899,6 +899,8 @@
"awaiting usable AIS positions…": "بانتظار مواضع AIS صالحة…", "awaiting usable AIS positions…": "بانتظار مواضع AIS صالحة…",
"awaiting first AIS message…": "بانتظار أول رسالة AIS…", "awaiting first AIS message…": "بانتظار أول رسالة AIS…",
"EST. DOWNRANGE": "تقدير على امتداد المسار", "EST. DOWNRANGE": "تقدير على امتداد المسار",
"CONFIRMED": "مؤكَّد",
"PAD / RTLS": "منصة الإطلاق / RTLS",
"Pick a shape, then click the map.": "اختر شكلًا، ثم انقر على الخريطة.", "Pick a shape, then click the map.": "اختر شكلًا، ثم انقر على الخريطة.",
"Enter to place the pin, Esc to cancel.": "Enter لوضع الدبوس، وEsc للإلغاء.", "Enter to place the pin, Esc to cancel.": "Enter لوضع الدبوس، وEsc للإلغاء.",
"Click where the pin goes.": "انقر حيث تريد وضع الدبوس.", "Click where the pin goes.": "انقر حيث تريد وضع الدبوس.",
@@ -938,7 +940,7 @@
"General emergency": "طوارئ عامة", "General emergency": "طوارئ عامة",
"Unlawful interference": "تدخل غير مشروع", "Unlawful interference": "تدخل غير مشروع",
"Radio failure": "عطل في الاتصال اللاسلكي", "Radio failure": "عطل في الاتصال اللاسلكي",
"Downed aircraft": "طائرة سقطت", "Downed aircraft": "طائرة ساقطة",
"Minimum fuel": "الحد الأدنى من الوقود", "Minimum fuel": "الحد الأدنى من الوقود",
"Lifeguard (medical priority)": "إسعاف جوي (أولوية طبية)", "Lifeguard (medical priority)": "إسعاف جوي (أولوية طبية)",
"Rasuwagadhi witness video": "فيديو شاهد عيان في Rasuwagadhi", "Rasuwagadhi witness video": "فيديو شاهد عيان في Rasuwagadhi",
@@ -1599,7 +1601,7 @@
"WebUSB needs desktop Chrome or Edge": "يتطلب WebUSB متصفح Chrome أو Edge على سطح المكتب", "WebUSB needs desktop Chrome or Edge": "يتطلب WebUSB متصفح Chrome أو Edge على سطح المكتب",
"connect a receiver in Radio": "وصّل جهاز استقبال في «الراديو»", "connect a receiver in Radio": "وصّل جهاز استقبال في «الراديو»",
"receiver is in FM mode": "جهاز الاستقبال في وضع FM", "receiver is in FM mode": "جهاز الاستقبال في وضع FM",
"listening": "استماع", "listening": "جارٍ الاستماع",
"USB error": "خطأ USB", "USB error": "خطأ USB",
"${amount}K": "{amount} ألف دولار", "${amount}K": "{amount} ألف دولار",
"${amount}M": "{amount} مليون دولار", "${amount}M": "{amount} مليون دولار",
@@ -1622,8 +1624,8 @@
"many": "{acres} آكر", "many": "{acres} آكر",
"other": "{acres} آكر" "other": "{acres} آكر"
}, },
"{percent}% contained": "تمت السيطرة على {percent}%", "{percent}% contained": "الاحتواء: {percent}%",
"containment unknown": "نسبة السيطرة غير معروفة", "containment unknown": "نسبة الاحتواء غير معروفة",
"{cause} cause": "السبب: {cause}", "{cause} cause": "السبب: {cause}",
"{personnel} personnel": { "{personnel} personnel": {
"zero": "{personnel} فرد", "zero": "{personnel} فرد",
@@ -1643,11 +1645,11 @@
"Unnamed incident": "حادث بلا اسم", "Unnamed incident": "حادث بلا اسم",
"Open {title} on InciWeb": "فتح {title} على InciWeb", "Open {title} on InciWeb": "فتح {title} على InciWeb",
"Perimeter source unavailable": "مصدر محيطات الحرائق غير متاح", "Perimeter source unavailable": "مصدر محيطات الحرائق غير متاح",
"Not contained or unknown": "غير مسيطر عليه أو غير معروف", "Not contained or unknown": "بلا احتواء أو غير معروف",
"Under 50% contained": "السيطرة أقل من 50%", "Under 50% contained": "الاحتواء: أقل من 50%",
"50–99% contained": "السيطرة 50–99%", "50–99% contained": "الاحتواء: 50–99%",
"Fully contained": "مسيطر عليه بالكامل", "Fully contained": "احتواء كامل",
"Colour shows reported containment. Perimeters are simplified to about 100 m.": "يبيّن اللون نسبة السيطرة المُبلَّغ عنها. المحيطات مبسّطة إلى نحو 100 م.", "Colour shows reported containment. Perimeters are simplified to about 100 m.": "يبيّن اللون نسبة الاحتواء المُبلَّغ عنها. المحيطات مبسّطة إلى نحو 100 م.",
"Malformed perimeter snapshot": "لقطة المحيطات مشوّهة", "Malformed perimeter snapshot": "لقطة المحيطات مشوّهة",
"radio tag|All": "الكل", "radio tag|All": "الكل",
"Directory refresh degraded; showing the previous station catalog.": "تحديث الدليل متدهور؛ يُعرض فهرس المحطات السابق.", "Directory refresh degraded; showing the previous station catalog.": "تحديث الدليل متدهور؛ يُعرض فهرس المحطات السابق.",
@@ -2199,14 +2201,14 @@
"Live stream connected": "البث المباشر متصل", "Live stream connected": "البث المباشر متصل",
"Snapshot feed connected": "مصدر اللقطات متصل", "Snapshot feed connected": "مصدر اللقطات متصل",
"Media fetch failed": "فشل جلب الوسائط", "Media fetch failed": "فشل جلب الوسائط",
"Upstream snapshot active": "لقطة المصدر نشطة", "Upstream snapshot active": "لقطة المصدر الأصلي نشطة",
"Fallback Street View frame": "إطار بديل من Street View", "Fallback Street View frame": "إطار بديل من Street View",
"Upstream unavailable": "المصدر غير متاح", "Upstream unavailable": "المصدر الأصلي غير متاح",
"No source configured": "لم يُعَدّ أي مصدر", "No source configured": "لم يُعَدّ أي مصدر",
"Configured source": "المصدر المُعَدّ", "Configured source": "المصدر المُعَدّ",
"Configured Source": "المصدر المُعَدّ", "Configured Source": "المصدر المُعَدّ",
"Synthetic fallback": "بديل محاكى", "Synthetic fallback": "بديل محاكى",
"Upstream HTTP {status}": "المصدر: HTTP {status}", "Upstream HTTP {status}": "المصدر الأصلي: HTTP {status}",
"Unexpected media type {type}": "نوع وسائط غير متوقع: {type}", "Unexpected media type {type}": "نوع وسائط غير متوقع: {type}",
"{value} KM/H": "{value} كم/س", "{value} KM/H": "{value} كم/س",
"{label} · {radius} AIR/SEA WINDOW": "{label} · نطاق جو/بحر {radius}", "{label} · {radius} AIR/SEA WINDOW": "{label} · نطاق جو/بحر {radius}",
@@ -2294,7 +2296,7 @@
"Data source unavailable": "مصدر البيانات غير متاح", "Data source unavailable": "مصدر البيانات غير متاح",
"Local FM stopped: internet radio started": "توقف FM المحلي: بدأ راديو الإنترنت", "Local FM stopped: internet radio started": "توقف FM المحلي: بدأ راديو الإنترنت",
"IDLE": "خامل", "IDLE": "خامل",
"TUNING": "ضبط", "TUNING": "جارٍ الضبط",
"STREAMING": "استقبال", "STREAMING": "استقبال",
"UNSUPPORTED": "غير مدعوم", "UNSUPPORTED": "غير مدعوم",
"idle": "خامل", "idle": "خامل",
+2
View File
@@ -739,6 +739,8 @@
"awaiting usable AIS positions…": "esperando posiciones AIS utilizables…", "awaiting usable AIS positions…": "esperando posiciones AIS utilizables…",
"awaiting first AIS message…": "esperando el primer mensaje AIS…", "awaiting first AIS message…": "esperando el primer mensaje AIS…",
"EST. DOWNRANGE": "DIST. ESTIMADA", "EST. DOWNRANGE": "DIST. ESTIMADA",
"CONFIRMED": "CONFIRMADA",
"PAD / RTLS": "PLATAFORMA / RTLS",
"MISSION CONTROL · FIRST LAUNCH": "CENTRO DE MANDO · PRIMER INICIO", "MISSION CONTROL · FIRST LAUNCH": "CENTRO DE MANDO · PRIMER INICIO",
"Choose your first view": "Elige tu primera vista", "Choose your first view": "Elige tu primera vista",
"LIVE CONTACTS": "CONTACTOS EN VIVO", "LIVE CONTACTS": "CONTACTOS EN VIVO",
+2
View File
@@ -216,6 +216,8 @@
"Pause replay": "Приостановить повтор", "Pause replay": "Приостановить повтор",
"Cancel replay": "Отменить повтор", "Cancel replay": "Отменить повтор",
"EST. DOWNRANGE": "РАСЧЁТ ПО ТРАССЕ", "EST. DOWNRANGE": "РАСЧЁТ ПО ТРАССЕ",
"CONFIRMED": "ПОДТВЕРЖДЕНО",
"PAD / RTLS": "СТАРТОВАЯ ПЛОЩАДКА / RTLS",
"PLANNED · ": "ПЛАН · ", "PLANNED · ": "ПЛАН · ",
"SUPPLIED TRAJECTORY POINTS": "ПРЕДОСТАВЛЕННЫЕ ТОЧКИ ТРАЕКТОРИИ", "SUPPLIED TRAJECTORY POINTS": "ПРЕДОСТАВЛЕННЫЕ ТОЧКИ ТРАЕКТОРИИ",
"RECONSTRUCTED ESTIMATE": "РЕКОНСТРУИРОВАННАЯ ОЦЕНКА", "RECONSTRUCTED ESTIMATE": "РЕКОНСТРУИРОВАННАЯ ОЦЕНКА",
+2
View File
@@ -811,6 +811,8 @@
"Pause replay": "暂停回放", "Pause replay": "暂停回放",
"Cancel replay": "取消回放", "Cancel replay": "取消回放",
"EST. DOWNRANGE": "预估落点距离", "EST. DOWNRANGE": "预估落点距离",
"CONFIRMED": "已确认",
"PAD / RTLS": "发射台 / RTLS",
"PLANNED · ": "计划 · ", "PLANNED · ": "计划 · ",
"SUPPLIED TRAJECTORY POINTS": "提供的轨迹点", "SUPPLIED TRAJECTORY POINTS": "提供的轨迹点",
"RECONSTRUCTED ESTIMATE": "重建估算", "RECONSTRUCTED ESTIMATE": "重建估算",
+2
View File
@@ -806,6 +806,8 @@
"Pause replay": "暫停回放", "Pause replay": "暫停回放",
"Cancel replay": "取消回放", "Cancel replay": "取消回放",
"EST. DOWNRANGE": "預估落點距離", "EST. DOWNRANGE": "預估落點距離",
"CONFIRMED": "已確認",
"PAD / RTLS": "發射臺 / RTLS",
"PLANNED · ": "計劃 · ", "PLANNED · ": "計劃 · ",
"SUPPLIED TRAJECTORY POINTS": "提供的軌跡點", "SUPPLIED TRAJECTORY POINTS": "提供的軌跡點",
"RECONSTRUCTED ESTIMATE": "重建估算", "RECONSTRUCTED ESTIMATE": "重建估算",
+10 -1
View File
@@ -6,6 +6,15 @@ import {
} from './policy.js'; } from './policy.js';
import { UNKNOWN_LAUNCH_STATUS } from './model.js'; import { UNKNOWN_LAUNCH_STATUS } from './model.js';
// Landing endpoints carry an English accuracy code (the panel and the map
// entity compare it); only the text shown in the stage table is translated.
// The downrange estimate arrives already translated.
function accuracyLabel(accuracy) {
if (accuracy === 'CONFIRMED') return t('CONFIRMED');
if (accuracy === 'PAD / RTLS') return t('PAD / RTLS');
return accuracy;
}
export function createPanel({ state: layerState, services, parts, source }) { export function createPanel({ state: layerState, services, parts, source }) {
/** /**
* Preserve the user's globe scale for roster previews while avoiding an * Preserve the user's globe scale for roster previews while avoiding an
@@ -251,7 +260,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
? launch.launchSite ? launch.launchSite
: t('UNAVAILABLE')); : t('UNAVAILABLE'));
const position = endpoint const position = endpoint
? `${endpoint.lat.toFixed(2)}, ${endpoint.lon.toFixed(2)} · ${endpoint.accuracy}` ? `${endpoint.lat.toFixed(2)}, ${endpoint.lon.toFixed(2)} · ${accuracyLabel(endpoint.accuracy)}`
: stage.downrangeKm > 0 : stage.downrangeKm > 0
? t('{value} KM DOWNRANGE', { ? t('{value} KM DOWNRANGE', {
value: stage.downrangeKm.toLocaleString(), value: stage.downrangeKm.toLocaleString(),
+6 -9
View File
@@ -1,7 +1,7 @@
import { t } from '../i18n/index.js'; import { t } from '../i18n/index.js';
import { syncChipGroup } from './chipGroup.js'; import { syncChipGroup } from './chipGroup.js';
import { syncRowList } from './rowList.js'; import { syncRowList } from './rowList.js';
import { formatScaleLabel, unitLabel } from './unitLabels.js'; import { formatScaleRange } from './unitLabels.js';
import { createSlider } from './slider.js'; import { createSlider } from './slider.js';
const set = (node, key, value) => { const set = (node, key, value) => {
@@ -16,13 +16,6 @@ const order = (parent, nodes) =>
parent.insertBefore(node, parent.children[i] || null); parent.insertBefore(node, parent.children[i] || null);
}); });
// "960 — 1050+ гПа": the unit stays with the last number.
const scaleText = (low, high, units) => {
const unit = unitLabel(units);
const range = `${formatScaleLabel(low)} — ${formatScaleLabel(high)}`;
return unit ? `${range}\u00a0${unit}` : range;
};
/** Optional, ordered blocks without disclosure state. All controls are keyed. */ /** Optional, ordered blocks without disclosure state. All controls are keyed. */
export function createRailCardBlocks({ container, cardId, onParams }) { export function createRailCardBlocks({ container, cardId, onParams }) {
const document = container.ownerDocument; const document = container.ownerDocument;
@@ -309,7 +302,11 @@ export function createRailCardBlocks({ container, cardId, onParams }) {
block.scale, block.scale,
'textContent', 'textContent',
visible visible
? scaleText(legend.labels[0], legend.labels.at(-1), legend.units) ? formatScaleRange(
legend.labels[0],
legend.labels.at(-1),
legend.units,
)
: '', : '',
); );
} else if (props.type === 'actions') { } else if (props.type === 'actions') {
+12 -2
View File
@@ -1,4 +1,11 @@
import { formatNumber, msg, t, tn, translateKnown } from '../i18n/index.js'; import {
formatNumber,
isolate,
msg,
t,
tn,
translateKnown,
} from '../i18n/index.js';
import { keyGuidanceText, keyRequirementText } from './keyGuidance.js'; import { keyGuidanceText, keyRequirementText } from './keyGuidance.js';
// Street Level UI state to panel strings and flags. Pure, so it is testable. // Street Level UI state to panel strings and flags. Pure, so it is testable.
@@ -174,7 +181,10 @@ function presentViewer(state) {
if (street.capturedAt) if (street.capturedAt)
right.push(formatDate(street.capturedAt, street.capturedAtPrecision)); right.push(formatDate(street.capturedAt, street.capturedAtPrecision));
const left = []; const left = [];
if (street.title) left.push(street.title); // A provider address mixes Latin and the page language ("602 Congress Ave,
// أوستن"); each part is isolated so a house number stays with its street
// in right-to-left text. A no-op in left-to-right languages.
if (street.title) left.push(street.title.split(', ').map(isolate).join(', '));
if (street.creator) if (street.creator)
left.push(t('Image by {creator}', { creator: street.creator })); left.push(t('Image by {creator}', { creator: street.creator }));
if (street.license) left.push(street.license); if (street.license) left.push(street.license);
+24
View File
@@ -5,6 +5,7 @@ import {
sinceStopIndex, sinceStopIndex,
} from './streetLevelPresentation.js'; } from './streetLevelPresentation.js';
import { providerSnapshot } from '../testSupport/streetLevelFakes.mjs'; import { providerSnapshot } from '../testSupport/streetLevelFakes.mjs';
import { getLocale, setLocale } from '../i18n/index.js';
const provider = providerSnapshot; const provider = providerSnapshot;
@@ -321,6 +322,29 @@ test('a Street View caption names the place, dates it by month, and offers no FI
assert.equal(view.viewer.renderModes, false); assert.equal(view.viewer.renderModes, false);
}); });
test('in Arabic each part of a provider address keeps its own order', () => {
const previous = getLocale();
setLocale('ar');
try {
const view = presentStreetLevelPanel(
snapshot({
enabled: true,
street: {
open: true,
imageId: 'pano-1',
title: '602 Congress Ave, أوستن, تكساس',
},
}),
);
assert.equal(
view.viewer.captionLeft,
'\u2068602 Congress Ave\u2069, \u2068أوستن\u2069, \u2068تكساس\u2069',
);
} finally {
setLocale(previous);
}
});
test('the meta line keeps the sequence count and adds the other providers’ hints', () => { test('the meta line keeps the sequence count and adds the other providers’ hints', () => {
const view = presentStreetLevelPanel( const view = presentStreetLevelPanel(
snapshot({ snapshot({
+4 -4
View File
@@ -26,7 +26,7 @@
0 0 1.2rem rgba(0, 212, 255, 0.08); 0 0 1.2rem rgba(0, 212, 255, 0.08);
backdrop-filter: blur(24px) saturate(1.3); backdrop-filter: blur(24px) saturate(1.3);
-webkit-backdrop-filter: blur(24px) saturate(1.3); -webkit-backdrop-filter: blur(24px) saturate(1.3);
text-align: left; text-align: start;
transform: translateX(-50%); transform: translateX(-50%);
transform-origin: bottom center; transform-origin: bottom center;
animation: gev-voice-card-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1); animation: gev-voice-card-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
@@ -95,7 +95,7 @@
} }
.gev-voice-card-dismiss { .gev-voice-card-dismiss {
margin-left: auto; margin-inline-start: auto;
width: 1.1rem; width: 1.1rem;
height: 1.1rem; height: 1.1rem;
padding: 0; padding: 0;
@@ -415,7 +415,7 @@ body.recording-mode .gev-pointer-reticle {
-webkit-backdrop-filter: blur(24px) saturate(1.3); -webkit-backdrop-filter: blur(24px) saturate(1.3);
color-scheme: dark; color-scheme: dark;
font: 0.75rem/1.45 var(--font-sans); font: 0.75rem/1.45 var(--font-sans);
text-align: left; text-align: start;
transform: translateX(-50%); transform: translateX(-50%);
} }
:root[data-ui-theme='cyber'] #gev-voice-control .gev-voice-panel { :root[data-ui-theme='cyber'] #gev-voice-control .gev-voice-panel {
@@ -443,7 +443,7 @@ body.recording-mode .gev-pointer-reticle {
letter-spacing: 0.14em; letter-spacing: 0.14em;
} }
.gev-voice-panel-tabs { .gev-voice-panel-tabs {
margin-left: auto; margin-inline-start: auto;
} }
#gev-voice-control .gev-voice-panel-header .gev-voice-panel-tab { #gev-voice-control .gev-voice-panel-header .gev-voice-panel-tab {
display: inline-flex; display: inline-flex;
+11
View File
@@ -51,3 +51,14 @@ export function formatScaleLabel(label) {
})}${match[2]}`, })}${match[2]}`,
); );
} }
/**
* A legend range with its unit: "960 — 1050+ hPa". The unit stays with the
* last number. In right-to-left text the unit is isolated as well, so a Latin
* unit keeps its own order ("°C", not "C°").
*/
export function formatScaleRange(low, high, units) {
const unit = unitLabel(units);
const range = `${formatScaleLabel(low)} — ${formatScaleLabel(high)}`;
return unit ? `${range}\u00a0${isolate(unit)}` : range;
}