i18n(ar): apply review round 1

Findings from the astra, sol and fable reviews of the Arabic screenshots:

- Wording: forecast valid time is «عند», not an expiry; acres are آكر, not
  feddans; live layers are «تتبّع … المباشر» (not "direct flights"); the
  straight-ahead lane, turboprop, call sign, reconcile, proximity, retrying,
  count agreement (colon form, fixed counts, ي for compact day ages) and
  smaller fixes. Guide and glossary updated.
- Right to left: the scene editor's JSON fields are LTR; canvas map cards
  draw with LTR geometry and an Arabic face; whiteboard callouts anchor
  inside their cards; HDG/FOV keep their values; the cyclone timestamp no
  longer splits its date; bare-number counters read 01 / 25, translated
  ones say «من».
- Built-in shot titles of the Nepal shot pack's bootstrap shots localize
  (the applied pack vouches for them), in every language.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-28 17:42:24 +04:00
1 parent 91f1f80e60
commit 4f44a17b44
12 files changed
+226 -103

No files matched your search

+8 -3
View File
@@ -19,10 +19,12 @@ Translation sources: abiyeenzo (#915).
- **Western digits (0–9)**, as in the catalog. The HUD reads like an instrument panel, and mixing ٠–٩ with Latin codes and coordinates misreads. `intlLocale('ar')` in `src/i18n/locale.js` passes `ar-u-nu-latn` to every Intl formatter, so browsers that default to Arabic-Indic digits for an Arabic region still show 0–9.
- **Right to left.** `ar` is in `RTL_LOCALES`, so the page gets `<html dir="rtl">`. The title, style indicator and both rails swap sides; the dock and the corner row reverse. The intel HUD (classification block, MGRS, coordinates, GSD) stays a left-to-right instrument overlay; its translated lines read right to left inside it. Media transport (previous / play / next) and the radio dial stay left to right, as on any player. Layout rules: `src/ui/styles/rtl.css`.
- 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`.
- Code stays left to right: the JSON fields of the scene editor are LTR in every language.
## Units
Distances and speeds take Arabic abbreviations after the number, with a space: كم، م، كم/س، كم/ث، م/ث، ميل/س، عقدة (knots)، قدم (feet)، مم. Time: ث (seconds)، د (minutes)، س (hours)، يوم. Scientific and radio units stay Latin: MHz، dB، dBFS، dBZ، hPa، MW، MiB. `°` and `%` stay. Hemisphere letters in coordinates: ش (N)، ج (S)، ق (E)، غ (W), as in `21.33ش`.
Distances and speeds take Arabic abbreviations after the number, with a space: كم، م، كم/س، كم/ث، م/ث، ميل/س، عقدة (knots)، قدم (feet)، مم. Time: ث (seconds)، د (minutes)، س (hours)، يوم; compact age labels («3d», «5D AGO») use ي: «{count} ي»، «قبل {count} ي», since a plain string cannot make يوم agree with the count. Area: آكر (acres, the value is not converted; فدان is a different unit). Scientific and radio units stay Latin: MHz، dB، dBFS، dBZ، hPa، MW، MiB. `°` and `%` stay. Hemisphere letters in coordinates: ش (N)، ج (S)، ق (E)، غ (W), as in `21.33ش`.
## Plurals
@@ -55,6 +57,8 @@ Every form must keep `{count}`. For one and two, Arabic names the quantity with
| globe / map / map source / basemap | الكرة الأرضية / الخريطة / مصدر الخريطة / الخريطة الأساسية |
| tiles | بلاطات |
| aircraft / flight / military flights | طائرة / رحلة / الرحلات العسكرية |
| Live Flights / Live Vessels (layers) | تتبّع الرحلات / السفن المباشر (not الرحلات المباشرة) |
| call sign | رمز النداء |
| vessel, ship | سفينة |
| satellite | قمر صناعي |
| launch (rocket) / space mission | إطلاق / مهمة فضائية |
@@ -78,12 +82,13 @@ Every form must keep `{count}`. For one and two, Arabic names the quantity with
| transit / bike share | النقل العام / الدراجات المشتركة |
| Previous / Next | السابق / التالي |
| compass points (N, NNE, …) | شمال، شمال شمال شرق، … |
| forecast / valid / issued | توقع / صالح حتى / صدر |
| forecast / valid (time) / issued | توقع / عند، وقت التوقع (not صالح حتى) / صدر |
| observation / observed | رصد / مرصود |
| field (weather) / model | حقل / نموذج |
| parameters / ellipsoid | المعلمات / المجسّم الإهليلجي |
| scene cloud | غيوم المشهد |
| box (Recent Imagery area) / pin | منطقة / تثبيت |
| THIS (voice pointer) | المُشار إليه |
| THIS (voice pointer) | هذا |
| outline / mark (map annotation) | مخطط / علامة، تعليم |
| share (a view or link) | مشاركة |
| AI agent | وكيل ذكاء اصطناعي |
+21 -1
View File
@@ -1,6 +1,14 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { formatDate, formatNumber, getLocale, setLocale, t, tn } from './index.js';
import {
formatDate,
formatNumber,
getLocale,
keepCodesWithValues,
setLocale,
t,
tn,
} from './index.js';
import {
applyDocumentLanguage,
intlLocale,
@@ -109,7 +117,19 @@ test('Arabic uses all six plural forms and spaced HUD units', () => {
t('Issued: {time}', { time: '10/28 00:00 UTC' }),
'صدر: \u206810/28 00:00 UTC\u2069',
);
// A Latin code stays beside its own value: HDG 68° · FOV 44°.
assert.equal(
keepCodesWithValues(
t('{city} · HDG {heading} · FOV {fov}', {
city: 'Austin',
heading: '68°',
fov: '44°',
}),
),
'\u2068Austin\u2069 · \u2068HDG \u206868°\u2069\u2069 · \u2068FOV \u206844°\u2069\u2069',
);
setLocale('ru');
assert.equal(keepCodesWithValues('HDG 68°'), 'HDG 68°');
assert.equal(formatCockpitDistance(14000), '14 КМ');
} finally {
setLocale(previous);
+14
View File
@@ -53,6 +53,20 @@ export function isolate(text) {
return isRtlLocale(currentLocale) ? `\u2068${text}\u2069` : String(text);
}
const CODE_BEFORE_VALUE =
/(?<![\p{L}\p{N}])([A-Z][A-Z0-9/]+) (\u2068[^\u2068\u2069]*\u2069)/gu;
/**
* A Latin code and the isolated value after it ("HDG 68\u00b0") as one unit in
* right-to-left text. Two codes in a row (HDG 68\u00b0 \u00b7 FOV 44\u00b0) otherwise form a
* single left-to-right run and the second code ends up beside the first
* value. Applies to t() output; a no-op outside RTL locales.
*/
export function keepCodesWithValues(text) {
if (!isRtlLocale(currentLocale)) return text;
return String(text).replace(CODE_BEFORE_VALUE, '\u2068$1 $2\u2069');
}
/**
* Switch the active locale. Pure state change: call applyDocumentLanguage /
* applyDocumentTranslations to reflect it in the DOM.
+68 -68
View File
@@ -27,21 +27,21 @@
"Initializing systems...": "جارٍ تهيئة الأنظمة...",
"LIVE DATA OFF": "البيانات المباشرة متوقفة",
"MAPPED SITES LOADED": "تم تحميل المواقع المرسومة",
"RETRYING MAPPED SITES": "إعادة محاولة المواقع المرسومة",
"RETRYING MAPPED SITES": "جارٍ إعادة محاولة تحميل المواقع المرسومة",
"FETCHING MAPPED SITES": "جارٍ جلب المواقع المرسومة",
"TURNING OFF LIVE DATA": "جارٍ إيقاف البيانات المباشرة",
"REFRESHING LIVE DATA": "جارٍ تحديث البيانات المباشرة",
"Close data attribution": "إغلاق إسناد البيانات",
"MISSION CONTROL · FIRST LAUNCH": "مركز التحكم · أول تشغيل",
"Choose your first view": "اختر عرضك الأول",
"LIVE CONTACTS": "الأهداف المباشرة",
"LIVE CONTACTS": "تتبّع الأهداف المباشر",
"Don't show this again": "لا تعرض هذا مرة أخرى",
"POWER UP": "تشغيل",
"GROUND STATION · PROVIDER SETTINGS": "المحطة الأرضية · إعدادات المزوّدين",
"Power up the globe": "شغّل الكرة الأرضية",
"SAVE KEYS": "حفظ المفاتيح",
"Saving…": "جارٍ الحفظ…",
"It feels like a forbidden cockpit—then you realize the sources are public and the data is real.": "يبدو الأمر كقمرة قيادة محظورة—ثم تدرك أن المصادر عامة والبيانات حقيقية.",
"It feels like a forbidden cockpit—then you realize the sources are public and the data is real.": "يبدو الأمر كقمرة قيادة محظورة — ثم تدرك أن المصادر عامة والبيانات حقيقية.",
"Aircraft, vessels and nearby intelligence": "طائرات وسفن ومعلومات استخباراتية قريبة",
"SPACE MISSIONS": "المهام الفضائية",
"Launches, spacecraft and orbital context": "عمليات إطلاق ومركبات وسياق مداري",
@@ -66,7 +66,7 @@
"IMPORT": "استيراد",
"RUN LOG": "سجل التشغيل",
"Ready": "جاهز",
"Starting live contacts…": "جارٍ تشغيل الأهداف المباشرة…",
"Starting live contacts…": "جارٍ بدء تتبّع الأهداف المباشر…",
"Opening space missions…": "جارٍ فتح المهام الفضائية…",
"Scanning active events…": "جارٍ مسح الأحداث النشطة…",
"Working…": "جارٍ العمل…",
@@ -105,7 +105,7 @@
"CONNECTING": "جارٍ الاتصال",
"ERROR": "خطأ",
"Session ended — cost cap {cost}": "انتهت الجلسة — بلغت حد التكلفة {cost}",
"VOICE STANDBY": "الصوت في وضع الاستعداد",
"VOICE STANDBY": "الصوت: استعداد",
"VOICE ACTIVE": "الصوت نشط",
"VOICE UNAVAILABLE": "الصوت غير متاح",
"WebRTC microphone support unavailable": "دعم ميكروفون WebRTC غير متاح",
@@ -136,7 +136,7 @@
"Loaded: {scene} / {shot}": "تم التحميل: {scene} / {shot}",
"Camera unavailable — exit cockpit first": "الكاميرا غير متاحة — اخرج من قمرة القيادة أولًا",
"No shots to run": "لا توجد لقطات للتشغيل",
"Running {index}/{total}: {scene} / {shot}": "جارٍ التشغيل {index}/{total}: {scene} / {shot}",
"Running {index}/{total}: {scene} / {shot}": "جارٍ التشغيل {index} من {total}: {scene} / {shot}",
"Scene run complete": "اكتمل تشغيل المشهد",
"Error: {message}": "خطأ: {message}",
"Could not exit {mode} — scene layers may be refused": "تعذّر الخروج من {mode} — قد تُرفض طبقات المشهد",
@@ -210,7 +210,7 @@
"Outside": "الخارج",
"Detection opacity outside the keyhole": "عتامة الكشف خارج الفتحة",
"World-overlay label and card opacity beyond the fade distance": "عتامة التسميات والبطاقات في طبقة العالم بعد مسافة التلاشي",
"PARAMETERS": "المعاملات",
"PARAMETERS": "المعلمات",
"3D aircraft — flat icons zoomed out, 3D models up close": "طائرات ثلاثية الأبعاد — أيقونات مسطحة عند التصغير ونماذج ثلاثية الأبعاد عن قرب",
"Models": "النماذج",
"3D model coverage": "تغطية النماذج ثلاثية الأبعاد",
@@ -235,11 +235,11 @@
"Landmark: --": "المَعلم: --",
"Search any location": "البحث عن أي موقع",
"Search any location...": "ابحث عن أي موقع...",
"LEVEL": "المستوى",
"LEVEL": "أفقي",
"Estimated destination direction": "اتجاه الوجهة التقديري",
"DEST ---°": "الوجهة ---°",
"OPTICAL PLANE · 01": "المستوى البصري · 01",
"VISOR LOCK · ACTIVE": "قفل الحاجب · نشط",
"VISOR LOCK · ACTIVE": "قفل واقي الخوذة · نشط",
"FIRST PERSON": "منظور الشخص الأول",
"LIVE TRACK · COURSE ALIGNED": "تتبع مباشر · المسار متوافق",
"Cockpit vision style": "نمط رؤية القمرة",
@@ -249,8 +249,8 @@
"PREV": "السابق",
"Previous cockpit vision style": "نمط رؤية القمرة السابق",
"Previous vision style": "نمط الرؤية السابق",
"Current cockpit vision style: NORMAL. Activate for next style.": "نمط رؤية القمرة الحالي: NORMAL. فعّل للانتقال إلى النمط التالي.",
"Current style: NORMAL — click for next": "النمط الحالي: NORMAL — انقر للتالي",
"Current cockpit vision style: NORMAL. Activate for next style.": "نمط رؤية القمرة الحالي: عادي. فعّل للانتقال إلى النمط التالي.",
"Current style: NORMAL — click for next": "النمط الحالي: عادي — انقر للتالي",
"Next cockpit vision style": "نمط رؤية القمرة التالي",
"Next vision style": "نمط الرؤية التالي",
"View switcher": "مبدّل العرض",
@@ -308,7 +308,7 @@
"Reclassify tracked contact as TR-3B": "إعادة تصنيف الهدف المتتبَّع إلى TR-3B",
"Reclassify as TR-3B": "إعادة التصنيف إلى TR-3B",
"CONTACTS CONTEXT OFF": "سياق الأهداف متوقف",
"SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY": "اختر الأهداف لتحميل القرب المرصود / المرسوم",
"SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY": "اختر الأهداف لتحميل العناصر القريبة المرصودة / المرسومة",
"Available Space Missions": "المهام الفضائية المتاحة",
"AVAILABLE MISSIONS": "المهام المتاحة",
"SELECT A MISSION TO INSPECT": "اختر مهمة لفحصها",
@@ -356,7 +356,7 @@
"SUBJECT LEFT ITS FEED · READOUT HOLDING LAST KNOWN": "غادر الهدف مصدر بياناته · القراءة تحتفظ بآخر قيمة معروفة",
"MILITARY FLIGHT": "رحلة عسكرية",
"COMMERCIAL FLIGHT": "رحلة تجارية",
"{aircraftClass} · CURRENT": "{aircraftClass} · الحالي",
"{aircraftClass} · CURRENT": "{aircraftClass} · الحالية",
"{aircraftClass} · {distance}": "{aircraftClass} · {distance}",
"DISTANCE UNKNOWN": "المسافة غير معروفة",
"{count} INPUTS UNKNOWN": {
@@ -401,7 +401,7 @@
"Expand briefing panel": "توسيع لوحة الإحاطة",
"CYCLE ON": "التدوير مفعّل",
"CYCLE OFF": "التدوير متوقف",
"Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "إيقاف التدوير التلقائي للصفحات. تبقى أزرار السابق والتالي وتبويبات SIG/NEWS/LOCAL متاحة.",
"Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "إيقاف التدوير التلقائي للصفحات. تبقى أزرار السابق والتالي وتبويبات «إشارات» و«أخبار» و«محلي» متاحة.",
"REGIONAL NEWS": "أخبار إقليمية",
"LOCAL INFO": "معلومات محلية",
"LATEST LOCATION-MATCHED REPORTING": "أحدث التقارير المطابقة للموقع",
@@ -416,7 +416,7 @@
"TIME UNKNOWN": "الوقت غير معروف",
"{count}M AGO": "قبل {count} د",
"{count}H AGO": "قبل {count} س",
"{count}D AGO": "قبل {count} يوم",
"{count}D AGO": "قبل {count} ي",
"DIR UNKNOWN": "الاتجاه غير معروف",
"CLOUD {pct}%": "الغيوم {pct}%",
"CLOUD UNKNOWN": "الغيوم غير معروفة",
@@ -432,7 +432,7 @@
"entity": "عنصر",
"SHARED {subject}": "مشاركة: {subject}",
"Shared {subject} follow expired": "انتهت متابعة المشاركة: {subject}",
"Shared {subject} could not be restored — feed unavailable": "تعذّرت استعادة المشاركة — المصدر غير متاح: {subject}",
"Shared {subject} could not be restored — feed unavailable": "تعذّرت استعادة المشاركة ({subject}) — المصدر غير متاح",
"Shared {subject} is unavailable": "المشاركة غير متاحة: {subject}",
"Context": "السياق",
"Space Missions": "المهام الفضائية",
@@ -469,7 +469,7 @@
"Resume": "استئناف",
"selected": "المحددة",
"nearest": "الأقرب",
"{action} {target} radio station": "{action} محطة الراديو {target}",
"{action} {target} radio station": "{action}: محطة الراديو {target}",
"RADIO STATE UNCERTAIN": "حالة الراديو غير مؤكدة",
"SYNCING DIRECTORY": "جارٍ مزامنة الدليل",
"SYNCING": "جارٍ المزامنة",
@@ -519,7 +519,7 @@
"Search location by name or coordinates": "البحث عن موقع بالاسم أو الإحداثيات",
"Expand LOCATION": "توسيع الموقع",
"UNAVAILABLE": "غير متاح",
"Live Flights": "الرحلات المباشرة",
"Live Flights": "تتبّع الرحلات المباشر",
"Earthquakes (24h)": "الزلازل (24 ساعة)",
"No selected data layers": "لا توجد طبقات بيانات محددة",
"Cleared {count} data layers": {
@@ -552,7 +552,7 @@
"CALIBRATION": "المعايرة",
"ADJUST": "ضبط",
"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": "الاتجاه (بوصلة °) — انقر للكتابة",
"Pitch (° up/down) — click to type": "الميل (° أعلى/أسفل) — انقر للكتابة",
"Horizontal FOV (°) — click to type": "زاوية الرؤية الأفقية (°) — انقر للكتابة",
@@ -599,7 +599,7 @@
"Street Traffic": "حركة مرور الشوارع",
"Radio": "الراديو",
"Bikeshare": "الدراجات المشتركة",
"Live AIS Vessels": "سفن AIS المباشرة",
"Live AIS Vessels": "تتبّع سفن AIS المباشر",
"Mapped Installations": "المنشآت المرسومة",
"Global Context": "السياق العالمي",
"Datacenters": "مراكز البيانات",
@@ -612,7 +612,7 @@
"{count}h ago": "قبل {count} س",
"never": "لم يُحدَّث",
"loading...": "جارٍ التحميل...",
"UNCERTAIN · {source} · lifecycle state requires reconciliation": "غير مؤكد · {source} · حالة دورة التشغيل تحتاج إلى مطابقة",
"UNCERTAIN · {source} · lifecycle state requires reconciliation": "غير مؤكد · {source} · حالة التشغيل تحتاج إلى تصحيح",
"{state} · {source}": "{state} · {source}",
"{state} · {source} · {detail} · retry {seconds}s": "{state} · {source} · {detail} · إعادة المحاولة {seconds} ث",
"{state} · {source} · {detail}": "{state} · {source} · {detail}",
@@ -653,19 +653,19 @@
"AIS: {name}": "AIS: {name}",
"{type} SPD: {speed} HDG: {heading}": "{type} السرعة: {speed} HDG: {heading}",
"awaiting first AIS position…": "بانتظار أول موضع AIS…",
"CCTV ON": "CCTV يعمل",
"CCTV ON": "CCTV مفعّل",
"COVERAGE ON": "التغطية مفعّلة",
"AUTO HOP ON": "التنقل التلقائي مفعّل",
"PROJECTION OFF": "الإسقاط متوقف",
"ADJUST ON": "الضبط مفعّل",
"{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE": "{count} كاميرا في وضع الاستعداد · لم تُحدَّد كاميرا · انقر على كاميرا لتفعيلها",
"{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE": "الكاميرات في وضع الاستعداد: {count} · لم تُحدَّد كاميرا · انقر على كاميرا لتفعيلها",
"No cameras available in catalog.": "لا توجد كاميرات متاحة في الفهرس.",
"{city} CCTV": "CCTV {city}",
"HDG {value}°": "HDG {value}°",
"HDG {value}° (ESTIMATED)": "HDG {value}° (تقديري)",
"FOV {value}°": "FOV {value}°",
"COVERAGE {value}km²": "التغطية {value} كم²",
"OVERLAP {count} cams": "تداخل {count} كاميرا",
"OVERLAP {count} cams": "الكاميرات المتداخلة: {count}",
"ISOLATED VIEW": "منظور معزول",
"PROJ MONITOR": "إسقاط على شاشة",
"PROJ OFF": "إسقاط متوقف",
@@ -681,7 +681,7 @@
"Traffic / Transit": "المرور / النقل",
"Music": "موسيقى",
"Other": "أخرى",
"Alternative": "بديل",
"Alternative": "بديلة",
"Ambient": "أمبيانت",
"Blues": "بلوز",
"Classical": "كلاسيكية",
@@ -710,10 +710,10 @@
"WEATHER": "طقس",
"NO STATION AVAILABLE": "لا توجد محطة متاحة",
"DISABLE": "إيقاف",
"RECONCILE": "مطابقة",
"RECONCILE": "تصحيح الحالة",
"Enable Radio": "تفعيل الراديو",
"Disable Radio": "إيقاف الراديو",
"Reconcile Radio — lifecycle uncertain": "مطابقة الراديو — حالة دورة التشغيل غير مؤكدة",
"Reconcile Radio — lifecycle uncertain": "تصحيح حالة الراديو — حالة التشغيل غير مؤكدة",
"Loading station directory…": "جارٍ تحميل دليل المحطات…",
"Directory metadata only": "بيانات وصفية للدليل فقط",
"Choose a globe marker or use next.": "اختر علامة على الكرة الأرضية أو استخدم التالي.",
@@ -735,7 +735,7 @@
"Station unavailable after directory refresh — choose another channel": "المحطة غير متاحة بعد تحديث الدليل — اختر قناة أخرى",
"Radio is enabling…": "جارٍ تفعيل الراديو…",
"Radio is disabling…": "جارٍ إيقاف الراديو…",
"Radio lifecycle is uncertain — use Enable or Disable to reconcile": "حالة دورة تشغيل الراديو غير مؤكدة — استخدم تفعيل أو إيقاف للمطابقة",
"Radio lifecycle is uncertain — use Enable or Disable to reconcile": "حالة تشغيل الراديو غير مؤكدة — استخدم «تفعيل» أو «إيقاف» لتصحيحها",
"LAUNCH SITE": "موقع الإطلاق",
"LAUNCH SITE · {site}": "موقع الإطلاق · {site}",
"SELECTED SPACE MISSION": "المهمة الفضائية المحددة",
@@ -856,7 +856,7 @@
"Overpass timed out": "انتهت مهلة Overpass",
"Overpass could not complete the query": "تعذّر على Overpass إكمال الاستعلام",
"Overpass temporarily unavailable": "Overpass غير متاح مؤقتًا",
"Retrying mapped sites…": "إعادة محاولة المواقع المرسومة…",
"Retrying mapped sites…": "جارٍ إعادة محاولة تحميل المواقع المرسومة…",
"Fetching mapped sites…": "جارٍ جلب المواقع المرسومة…",
"{reason} — retrying in {seconds}s": "{reason} — إعادة المحاولة خلال {seconds} ث",
"{reason} — retry pending": "{reason} — إعادة المحاولة معلّقة",
@@ -866,7 +866,7 @@
"CONTEXT READY": "السياق جاهز",
"GLOBAL CONTEXT OFF": "السياق العالمي متوقف",
"SELECT A FLIGHT, VESSEL, OR MAPPED INSTALLATION": "اختر رحلة أو سفينة أو منشأة مرسومة",
"ENABLE TO LOAD OBSERVED / MAPPED PROXIMITY": "فعّل لتحميل القرب المرصود / المرسوم",
"ENABLE TO LOAD OBSERVED / MAPPED PROXIMITY": "فعّل لتحميل العناصر القريبة المرصودة / المرسومة",
"Global Context navigation": "التنقل في السياق العالمي",
"PREVIOUS": "السابق",
"Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.": "سياق مرسوم/مرصود من مصادر مفتوحة. غياب البث أو عدم تحميل مناطق الخريطة أو وجود مواقع غير مرسومة لا يدل على عدم الوجود.",
@@ -928,7 +928,7 @@
"Data provided by:": "البيانات مقدَّمة من:",
"Light aircraft": "طائرة خفيفة",
"Glider": "طائرة شراعية",
"Turboprop": "طائرة مروحية توربينية",
"Turboprop": "طائرة بمحرك توربيني مروحي",
"Airliner": "طائرة ركاب",
"Widebody": "طائرة عريضة البدن",
"Four-engine jet": "نفاثة رباعية المحركات",
@@ -1110,7 +1110,7 @@
"That file creates more than {limit} map objects (the limit).": "ينشئ هذا الملف عناصر على الخريطة تتجاوز الحد المسموح ({limit}).",
"That file has more than {limit} style definitions and image links after cleanup (the limit).": "بعد التنظيف، يحتوي هذا الملف على تعريفات أنماط وروابط صور تتجاوز الحد المسموح ({limit}).",
"That file has more than {limit} features (the limit).": "يحتوي هذا الملف على عناصر تتجاوز الحد المسموح ({limit}).",
"That file has more than {limit} points (the limit) across {features} features.": "يحتوي هذا الملف على نقاط تتجاوز الحد المسموح ({limit}) موزّعة على {features} عنصر.",
"That file has more than {limit} points (the limit) across {features} features.": "يحتوي هذا الملف على نقاط تتجاوز الحد المسموح ({limit})؛ عدد العناصر: {features}.",
"That file has more than {limit} points (the limit).": "يحتوي هذا الملف على نقاط تتجاوز الحد المسموح ({limit}).",
"That KMZ unpacks to more than the import limit.": "حجم ملف KMZ هذا بعد فك ضغطه يتجاوز حد الاستيراد.",
"That KMZ is damaged and could not be unpacked.": "ملف KMZ هذا تالف وتعذّر فك ضغطه.",
@@ -1200,7 +1200,7 @@
"{name} is empty": "{name}: القيمة فارغة",
"{name} is longer than any real key ({limit} max)": "{name}: القيمة أطول من أي مفتاح حقيقي (الحد الأقصى {limit})",
"{name} may only contain printable characters with no spaces": "{name}: يُسمح فقط بالأحرف القابلة للطباعة دون مسافات",
"{name} contains a character that is not valid in a key (#, quotes, $, \\, or backtick)": "{name}: يحتوي على حرف غير صالح في المفتاح (#، أو علامات الاقتباس، أو $، أو \\، أو العلامة المائلة الخلفية `)",
"{name} contains a character that is not valid in a key (#, quotes, $, \\, or backtick)": "{name}: يحتوي على حرف غير صالح في المفتاح (#، أو علامات الاقتباس، أو $، أو \\، أو علامة الاقتباس الخلفية `)",
"{name} is configured outside Provider Settings and can only be changed where it was set": "{name}: مُعَدّ خارج «إعدادات المزوّدين» ولا يمكن تغييره إلا حيث ضُبط",
"The key was not saved: could not restrict the credential file to your account; nothing was saved": "لم يُحفظ المفتاح: تعذّر قصر الوصول إلى ملف بيانات الاعتماد على حسابك؛ لم يُحفظ شيء",
"The key was not saved: the existing configuration could not be read, so nothing was changed": "لم يُحفظ المفتاح: تعذّرت قراءة الإعدادات الحالية، لذا لم يتغيّر شيء",
@@ -1290,7 +1290,7 @@
"many": "القريبة: {count}",
"other": "القريبة: {count}"
},
"retrying mapped ALPR cameras": "إعادة محاولة تحميل كاميرات ALPR المرسومة",
"retrying mapped ALPR cameras": "جارٍ إعادة محاولة تحميل كاميرات ALPR المرسومة",
"loading mapped ALPR cameras": "جارٍ تحميل كاميرات ALPR المرسومة",
"No ALPR data for this area — US and Canada only": "لا توجد بيانات ALPR لهذه المنطقة — الولايات المتحدة وكندا فقط",
"Zoom in to load mapped cameras": "قرّب الخريطة لتحميل الكاميرات المرسومة",
@@ -1336,7 +1336,7 @@
"many": "{count} عاصفة نشطة",
"other": "{count} عاصفة نشطة"
},
"Position as of {time}": "الموضع حتى {time}",
"Position as of {time}": "الموضع عند {time}",
"Maximum sustained wind: {wind} · Pressure: {pressure}": "أقصى رياح مستمرة: {wind} · الضغط: {pressure}",
"{name} · {classification} · Advisory {number} · issued {time}": "{name} · {classification} · النشرة رقم {number} · صدرت {time}",
"No active NHC/CPHC systems": "لا توجد أنظمة NHC/CPHC نشطة",
@@ -1385,7 +1385,7 @@
"lane direction|slightly right": "اليمنى",
"lane direction|sharply left": "الأقصى يسارًا",
"lane direction|sharply right": "الأقصى يمينًا",
"lane direction|straight": "الوسطى",
"lane direction|straight": "المستقيمة",
"ordinal|{number}st": "رقم {number}",
"ordinal|{number}nd": "رقم {number}",
"ordinal|{number}rd": "رقم {number}",
@@ -1453,7 +1453,7 @@
"Then click the globe to place the destination": "ثم انقر على الكرة الأرضية لوضع الوجهة",
"Swap A and B": "تبديل A وB",
"FLY ···": "تحليق ···",
"FLYING": "يحلّق",
"FLYING": "جارٍ التحليق",
"FLY": "تحليق",
"Fly the camera along the route": "تحليق الكاميرا على امتداد المسار",
"Place A and B first": "ضع A وB أولًا",
@@ -1462,12 +1462,12 @@
"press Escape twice to leave Draw": "اضغط Esc مرتين للخروج من «الرسم»",
"{name} is active — turn {name} off, then set {endpoint}": "«{name}» نشط — أوقف «{name}»، ثم عيّن {endpoint}",
"{name} is active — {leave}, then set {endpoint}": "«{name}» نشط — {leave}، ثم عيّن {endpoint}",
"Only the first {count} turns of this route are shown": "لا يُعرض من هذا المسار سوى أول {count} انعطافات",
"Only the first {count} turns of this route are shown": "لا يُعرض من هذا المسار سوى أول {count} من الانعطافات",
"Turn-by-turn directions": "إرشادات خطوة بخطوة",
"Another map tool is using clicks — close it, then SET A again": "أداة خريطة أخرى تستخدم النقرات — أغلقها، ثم اضغط «تعيين A» مجددًا",
"Routing…": "جارٍ حساب المسار…",
"No route found": "لم يُعثر على مسار",
"{distance} · {duration} · {mode} · first {count} turns": "{distance} · {duration} · {mode} · أول {count} انعطافات",
"{distance} · {duration} · {mode} · first {count} turns": "{distance} · {duration} · {mode} · أول {count} من الانعطافات",
"{distance} · {duration} · {mode}": "{distance} · {duration} · {mode}",
"Click the globe to place {endpoint}": "انقر على الكرة الأرضية لوضع {endpoint}",
"SET B, then click the globe": "اضغط «تعيين B»، ثم انقر على الكرة الأرضية",
@@ -1480,7 +1480,7 @@
"Routing timed out": "انتهت مهلة حساب المسار",
"Routing unavailable — no response from the routing service": "التوجيه غير متاح — لا استجابة من خدمة التوجيه",
"Earthquake source unavailable": "مصدر بيانات الزلازل غير متاح",
"live feed unavailable": "البث المباشر للبيانات غير متاح",
"live feed unavailable": "المصدر المباشر غير متاح",
"{value}°N": "{value}° ش",
"{value}°S": "{value}° ج",
"{value}°E": "{value}° ق",
@@ -1490,7 +1490,7 @@
"fire confidence|low": "منخفضة",
"age|<1h": "<1 س",
"age|{count}h": "{count} س",
"age|{count}d": "{count} يوم",
"age|{count}d": "{count} ي",
"Source snapshot time unavailable": "وقت لقطة المصدر غير متاح",
"Live data unavailable": "البيانات المباشرة غير متاحة",
"Intel Picture": "الصورة الاستخباراتية",
@@ -1534,7 +1534,7 @@
"Retrying automatically at {time} UTC": "إعادة المحاولة تلقائيًا عند {time} UTC",
"Retrying automatically after provider cooldown": "إعادة المحاولة تلقائيًا بعد انتهاء مهلة المزوّد",
"Updates automatically after pan or zoom and once a minute. The public feed covers a 250-nautical-mile circle around the view centre; zooming out does not make it a worldwide snapshot. Blank cells are outside sampled coverage or have insufficient observations.": "يتحدّث تلقائيًا بعد التحريك أو التكبير/التصغير ومرة كل دقيقة. يغطي المصدر العام دائرة نصف قطرها 250 ميلًا بحريًا حول مركز العرض؛ والتصغير لا يجعله لقطة عالمية. الخلايا الفارغة خارج نطاق العيّنة أو أرصادها غير كافية.",
"Low-accuracy score = 100 × max(0, bad − 1) / total: one aircraft is discounted, so this is not the raw share. Cells need {aircraft} aircraft over {minutes} minutes. Navigation-integrity anomalies are not a detection of jamming or spoofing.": "مؤشر ضعف الدقة = 100 × max(0, الضعيفة − 1) / الإجمالي: تُستبعد طائرة واحدة، لذا فهو ليس النسبة الخام. تحتاج كل خلية إلى {aircraft} طائرة خلال {minutes} دقيقة. شذوذ سلامة الملاحة ليس كشفًا للتشويش أو الانتحال.",
"Low-accuracy score = 100 × max(0, bad − 1) / total: one aircraft is discounted, so this is not the raw share. Cells need {aircraft} aircraft over {minutes} minutes. Navigation-integrity anomalies are not a detection of jamming or spoofing.": "مؤشر ضعف الدقة = 100 × max(0, الضعيفة − 1) / الإجمالي: تُستبعد طائرة واحدة، لذا فهو ليس النسبة الخام. تحتاج كل خلية إلى {aircraft} طائرات خلال {minutes} دقيقة. شذوذ سلامة الملاحة ليس كشفًا للتشويش أو الانتحال.",
"GNSS snapshot needs a view anchor": "لقطة GNSS تحتاج إلى نقطة ارتكاز للعرض",
"GNSS feed rate limited (HTTP 429)": "مصدر GNSS تجاوز حد الطلبات (HTTP 429)",
"Malformed GNSS integrity snapshot": "لقطة سلامة GNSS مشوّهة",
@@ -1579,7 +1579,7 @@
"Your RTL-SDR receiver and decoder feed": "جهاز استقبال RTL-SDR لديك ومصدر المفكّك",
"Your decoder feed": "مصدر المفكّك لديك",
"Your RTL-SDR receiver": "جهاز استقبال RTL-SDR لديك",
"NO CALLSIGN": "بلا نداء",
"NO CALLSIGN": "بلا رمز نداء",
"ALT {altitude} FT · GS {speed} KT · TRK {track}": "الارتفاع {altitude} قدم · GS {speed} عقدة · TRK {track}",
"V/S {rate}": "V/S {rate}",
"V/S {rate} FPM": "V/S {rate} قدم/د",
@@ -1616,20 +1616,20 @@
"Type {level} Incident": "حادث من النوع {level}",
"age|{count}m": "{count} د",
"{acres} ac": {
"zero": "{acres} فدان",
"one": "{acres} فدان",
"two": "{acres} فدان",
"few": "{acres} فدادين",
"many": "{acres} فدانًا",
"other": "{acres} فدان"
"zero": "{acres} آكر",
"one": "{acres} آكر",
"two": "{acres} آكر",
"few": "{acres} آكر",
"many": "{acres} آكر",
"other": "{acres} آكر"
},
"{percent}% contained": "تمت السيطرة على {percent}%",
"containment unknown": "نسبة السيطرة غير معروفة",
"{cause} cause": "السبب: {cause}",
"{personnel} personnel": {
"zero": "{personnel} فرد",
"one": "{personnel} فرد",
"two": "{personnel} فرد",
"one": "فرد واحد ({personnel})",
"two": "فردان ({personnel})",
"few": "{personnel} أفراد",
"many": "{personnel} فردًا",
"other": "{personnel} فرد"
@@ -1813,7 +1813,7 @@
"{heading}DEG": "{heading}°",
"AIS live load failed": "فشل تحميل AIS المباشر",
"AISSTREAM_API_KEY not set": "AISSTREAM_API_KEY غير مضبوط",
"live feed unsupported": "البث المباشر للبيانات غير مدعوم",
"live feed unsupported": "المصدر المباشر غير مدعوم",
"connecting to feed…": "جارٍ الاتصال بالمصدر…",
"feed disconnected": "المصدر منقطع",
"feed down": "المصدر متعطل",
@@ -1850,7 +1850,7 @@
"GOES INFRARED · ~5 min": "الأشعة تحت الحمراء GOES · ~5 د",
"{state}: {time}": "{state}: {time}",
"Latest observation": "أحدث رصد",
"frame {index}/{total}": "الإطار {index}/{total}",
"frame {index}/{total}": "الإطار {index} من {total}",
"loading": "جارٍ التحميل",
"Observation: unavailable": "الرصد: غير متاح",
"Contiguous US · gaps ≠ no rain": "الولايات المتحدة المتجاورة · الفجوات ≠ انعدام المطر",
@@ -1876,7 +1876,7 @@
"Hourly global mosaic; usually 2–3 hours delayed": "فسيفساء عالمية كل ساعة؛ بتأخير 2–3 ساعات عادةً",
"GOES regional clouds; approximately 5-minute updates": "غيوم إقليمية من GOES؛ تحديث كل 5 دقائق تقريبًا",
"Clouds only": "الغيوم فقط",
"Apply the full cloud brightness key; 100% filter strength": "تطبيق مفتاح سطوع الغيوم كاملًا؛ قوة المرشح 100%",
"Apply the full cloud brightness key; 100% filter strength": "تطبيق مرشح سطوع الغيوم بكامل شدته؛ قوة المرشح 100%",
"Full": "كامل",
"Show the complete infrared image; 0% filter strength": "عرض صورة الأشعة تحت الحمراء كاملة؛ قوة المرشح 0%",
"Soft": "ناعم",
@@ -1936,7 +1936,7 @@
"Read wind at map center": "قراءة الرياح في مركز الخريطة",
"Read the forecast at the center of the map without changing selection": "قراءة التوقع في مركز الخريطة دون تغيير التحديد",
"{model} forecast · {field} ({unit})": "توقع {model} · {field} ({unit})",
"Valid: {time}": "صالح حتى: {time}",
"Valid: {time}": "وقت التوقع: {time}",
"preparing": "جارٍ التجهيز",
"Issued: {time}": "صدر: {time}",
"Selected field unavailable · wind remains visible": "الحقل المحدد غير متاح · تبقى الرياح ظاهرة",
@@ -1966,11 +1966,11 @@
"No surface reading": "لا قراءة سطحية",
"Aim the map center at Earth": "وجّه مركز الخريطة نحو الأرض",
"A location reading needs a loaded forecast and the Earth at the center of the view.": "تتطلب قراءة الموقع توقعًا محمّلًا ووجود الأرض في مركز العرض.",
"Interpolated model forecast on an approximately 1° grid. Broad weather patterns, not a street-level measurement.": "توقع نموذجي مستكمَل على شبكة بدقة 1° تقريبًا. أنماط طقس عامة، لا قياس على مستوى الشارع.",
"Interpolated model forecast on an approximately 1° grid. Broad weather patterns, not a street-level measurement.": "توقع نموذج عددي محسوب بالاستيفاء على شبكة بدقة 1° تقريبًا. أنماط طقس عامة، لا قياس على مستوى الشارع.",
" · calm": " · سكون",
" from {direction}": " من {direction}",
"WIND AT {coordinates}": "الرياح عند {coordinates}",
"{model} · valid {time}": "{model} · صالح حتى {time}",
"{model} · valid {time}": "{model} · التوقع عند {time}",
"Wind speed field unavailable": "حقل سرعة الرياح غير متاح",
"Globe imagery unavailable": "صور الكرة الأرضية غير متاحة",
"Globe field image unavailable": "صورة الحقل على الكرة الأرضية غير متاحة",
@@ -2096,12 +2096,12 @@
"Review scene import": "مراجعة استيراد المشهد",
"Nothing is loaded or changed until you apply this file.": "لا يُحمَّل أو يتغيّر شيء حتى تطبّق هذا الملف.",
"Ready to import": "جاهز للاستيراد",
"Apply replaces the current project. Export your current project first if you want to keep both.": "التطبيق يستبدل المشروع الحالي. صدّر مشروعك الحالي أولًا إن أردت الاحتفاظ بكليهما.",
"Apply replaces the current project. Export your current project first if you want to keep both.": "يؤدي تطبيق الاستيراد إلى استبدال المشروع الحالي. صدّر مشروعك الحالي أولًا إن أردت الاحتفاظ بكليهما.",
"Apply import": "تطبيق الاستيراد",
"Import was not applied. The current project may have changed.": "لم يُطبَّق الاستيراد. ربما تغيّر المشروع الحالي.",
"Select a scene and shot first": "اختر مشهدًا ولقطة أولًا",
"Edit scene details": "تعديل تفاصيل المشهد",
"Camera positions use degrees and meters above the ellipsoid. Drafts are validated before they replace your saved scene.": "تُحدَّد مواضع الكاميرا بالدرجات وبالأمتار فوق الإهليلج. يُتحقّق من المسودات قبل أن تحل محل مشهدك المحفوظ.",
"Camera positions use degrees and meters above the ellipsoid. Drafts are validated before they replace your saved scene.": "تُحدَّد مواضع الكاميرا بالدرجات وبالأمتار فوق المجسّم الإهليلجي. يُتحقّق من المسودات قبل أن تحل محل مشهدك المحفوظ.",
"Anchors and data packs": "نقاط الارتكاز وحزم البيانات",
"Shot camera, timing, packs and actions": "كاميرا اللقطة والتوقيت والحزم والإجراءات",
"New anchor ID": "معرّف نقطة الارتكاز الجديدة",
@@ -2233,7 +2233,7 @@
"Show {name}": "إظهار {name}",
"Fly to {name}": "الانتقال إلى {name}",
"Remove {name}": "إزالة {name}",
"At most {count} files at once — remove one first.": "الحد الأقصى {count} ملفات في آن واحد — أزل ملفًا أولًا.",
"At most {count} files at once — remove one first.": "الحد الأقصى {count} ملفًا في آن واحد — أزل ملفًا أولًا.",
"Reading {name}…": "جارٍ قراءة {name}…",
"That file could not be read.": "تعذّرت قراءة هذا الملف.",
"Added {count} files.": {
@@ -2269,7 +2269,7 @@
"Lightning density": "كثافة البرق",
"Cyclone advisories": "نشرات الأعاصير",
"ALPR Cameras": "كاميرات ALPR",
"Live Vessels": "السفن المباشرة",
"Live Vessels": "تتبّع السفن المباشر",
"Bike Share": "الدراجات المشتركة",
"Cameras": "الكاميرات",
"Street Level": "مستوى الشارع",
@@ -2364,7 +2364,7 @@
"Washington DC": "واشنطن العاصمة",
"Tallinn": "تالين",
"Los Angeles": "لوس أنجلوس",
"CH {index} / {total}": "القناة {index} / {total}",
"CH {index} / {total}": "القناة {index} من {total}",
"Station unavailable after directory refresh": "المحطة غير متاحة بعد تحديث الدليل",
"Go to expanded Radio section": "الانتقال إلى قسم الراديو الموسَّع",
"Expand Radio section in Context": "توسيع قسم الراديو في «السياق»",
@@ -2514,7 +2514,7 @@
"This share link can't be opened": "لا يمكن فتح رابط المشاركة هذا",
"Shared vessel is unavailable": "السفينة المشتركة غير متاحة",
"subject": "الهدف",
"RETRYING ALPR CAMERAS": "إعادة محاولة كاميرات ALPR",
"RETRYING ALPR CAMERAS": "جارٍ إعادة محاولة تحميل كاميرات ALPR",
"FETCHING ALPR CAMERAS": "جارٍ جلب كاميرات ALPR",
"OVERPASS TEMPORARILY UNAVAILABLE": "OVERPASS غير متاح مؤقتًا",
"OVERPASS RATE-LIMITED": "OVERPASS قيّد معدل الطلبات",
@@ -2681,13 +2681,13 @@
"{time} · {minutes} min ago": "{time} · قبل {minutes} د",
"Active weather": "الطقس النشط",
"LATEST · newest per product": "الأحدث · آخر إطار لكل منتج",
"Forecast · valid {valid} · issued {issued}": "توقع · صالح حتى {valid} · صدر {issued}",
"Forecast · valid {valid} · issued {issued}": "توقع عند {valid} · صدر {issued}",
"{detail} · Does not follow history": "{detail} · لا يتبع السجل",
"Forecast · valid {valid}": "توقع · صالح حتى {valid}",
"Forecast · valid {valid}": "توقع عند {valid}",
"voice|OFF": "متوقف",
"voice|CONNECTING": "جارٍ الاتصال",
"voice|LISTENING": "يستمع",
"voice|EXECUTING": "ينفّذ",
"voice|LISTENING": "جارٍ الاستماع",
"voice|EXECUTING": "جارٍ التنفيذ",
"voice|ERROR": "خطأ",
"Voice provider": "مزوّد الصوت",
"Changing provider reconnects voice if it is on.": "تغيير المزوّد يعيد اتصال الصوت إن كان مفعّلًا.",
@@ -2956,7 +2956,7 @@
"The flights layer is not enabled": "طبقة الرحلات غير مفعّلة",
"The military layer is not enabled": "طبقة الرحلات العسكرية غير مفعّلة",
"The satellites layer is not enabled": "طبقة الأقمار الصناعية غير مفعّلة",
"The ais-live-vessels layer is not enabled": "طبقة سفن AIS المباشرة غير مفعّلة",
"The ais-live-vessels layer is not enabled": "طبقة تتبّع سفن AIS المباشر غير مفعّلة",
"Could not enable the requested layer": "تعذّر تفعيل الطبقة المطلوبة",
"Could not disable the requested layer": "تعذّر إيقاف الطبقة المطلوبة",
"Radio layer unavailable": "طبقة الراديو غير متاحة",
+4 -1
View File
@@ -16,6 +16,7 @@ import {
} from './labels.js';
import {
formatUtcDayMonth,
isolate,
t,
tn,
msg,
@@ -25,9 +26,11 @@ import { formatQuantity } from '../../ui/unitLabels.js';
// "10-12 15:00 UTC" never breaks across lines: no-break spaces, and a word
// joiner after the hyphen (JetBrains Mono has no U+2011 non-breaking hyphen).
// The clock is isolated (a no-op outside RTL): a no-break space between two
// numbers joins them into one run, which in Arabic split the date around it.
const utc = (value) =>
value
? `${formatUtcDayMonth(value).replace('-', '-\u2060')}\u00a0${value.slice(11, 16)}\u00a0UTC`
? `${formatUtcDayMonth(value).replace('-', '-\u2060')}\u00a0${isolate(value.slice(11, 16))}\u00a0UTC`
: t('Unavailable');
const COVERAGE = msg(
'Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.',
+2 -1
View File
@@ -365,8 +365,9 @@ export function createStreetLevelLayer({
/** The hint a ground-click provider shows, or the provider's own. */
function hintFor(entry, stats) {
if (entry.def.groundClick && stats.hint && !state.groundClickReady)
// No-break spaces keep a provider name (Google Street View) on one line.
return t('zoom in to a street to open {name}', {
name: translateKnown(entry.def.name),
name: translateKnown(entry.def.name).replace(/ /g, '\u00a0'),
});
return stats.hint || '';
}
+10 -7
View File
@@ -7,6 +7,9 @@
*/
/** Shared visual tokens used by every world-overlay source. */
// Map-card fonts: JetBrains Mono has no Arabic glyphs and the generic
// monospace fallback is a fixed-pitch Arabic face that stretches every word;
// Arabic text falls through to Noto Sans Arabic (loaded with the page fonts).
export const WORLD_OVERLAY_STYLE = Object.freeze({
background: 'rgba(4, 12, 16, 0.82)',
selectedBackground: 'rgba(5, 18, 24, 0.94)',
@@ -16,13 +19,13 @@ export const WORLD_OVERLAY_STYLE = Object.freeze({
detail: 'rgba(147, 161, 173, 0.92)',
leader: 'rgba(147, 213, 228, 0.58)',
accent: '#6be8ff',
fontLabel: '500 10px "JetBrains Mono", monospace',
fontTrack: '600 10px "JetBrains Mono", monospace',
fontTitle: '600 12px "JetBrains Mono", monospace',
fontDetail: '500 10.5px "JetBrains Mono", monospace',
fontSelected: '600 13px "JetBrains Mono", monospace',
fontTrackedTitle: '600 13px "JetBrains Mono", monospace',
fontTrackedDetail: '500 11px "JetBrains Mono", monospace',
fontLabel: '500 10px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontTrack: '600 10px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontTitle: '600 12px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontDetail: '500 10.5px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontSelected: '600 13px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontTrackedTitle: '600 13px "JetBrains Mono", "Noto Sans Arabic", monospace',
fontTrackedDetail: '500 11px "JetBrains Mono", "Noto Sans Arabic", monospace',
radius: 4,
anchorDotRadius: 3.2,
anchorDotStrokeWidth: 1,
+19 -6
View File
@@ -1069,17 +1069,30 @@ export function displaySceneTitle(scene) {
export function displayShotTitle(title, shot = undefined, scene = undefined) {
const text = typeof title === 'string' ? title : '';
const fromRecipe = shot
? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene?.id, text)
? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene, text)
: true;
if (fromRecipe && BUILTIN_SHOT_TITLES.has(text)) return t(text);
const numbered = /^Shot ([1-9]\d*)$/.exec(text);
return numbered ? t('Shot {n}', { n: numbered[1] }) : text;
}
/** Whether the built-in recipe behind a scene declares a shot with this title. */
function recipeDeclaresShotTitle(sceneId, title) {
const recipe = sceneId ? getSceneRecipeById(sceneId) : null;
return Boolean(
recipe?.cameraPath?.some((keyframe) => keyframe?.title === title),
/**
* Whether the built-in recipe behind a scene declares a shot with this title:
* its own keyframes, or the bootstrap shots of a shot pack applied to the
* scene (those shots carry no sourcePackId; the scene's pack marker vouches
* for them).
*/
function recipeDeclaresShotTitle(scene, title) {
const declares = (cameraPath) =>
Boolean(cameraPath?.some((keyframe) => keyframe?.title === title));
const recipe = scene?.id ? getSceneRecipeById(scene.id) : null;
if (declares(recipe?.cameraPath)) return true;
const packs = Array.isArray(scene?.appliedShotPacks)
? scene.appliedShotPacks
: [];
return packs.some((marker) =>
declares(
getSceneAppendRecipeById(marker?.id)?.legacySceneBootstrap?.cameraPath,
),
);
}
+19
View File
@@ -87,6 +87,25 @@ test('a built-in shot title localizes only for a recipe-sourced shot', () => {
}
});
test('bootstrap shots of an applied shot pack localize their built-in titles', () => {
setLocale('ru');
const title = 'Global Incident Context';
const shot = { id: 'shot-1', title };
// A legacy scene upgraded in place: its bootstrap shots have no
// sourcePackId, but the scene carries the pack marker.
const upgraded = {
id: 'scene-legacy',
appliedShotPacks: [{ id: 'bhote-koshi-nepal-evidence-pack', version: 12 }],
};
assert.equal(
displayShotTitle(title, shot, upgraded),
displayShotTitle(title),
);
assert.notEqual(displayShotTitle(title, shot, upgraded), title);
// The same typed title in a scene without the pack stays as typed.
assert.equal(displayShotTitle(title, shot, { id: 'scene-user' }), title);
});
test('the Scene status line shows an operator-typed shot title as typed', async () => {
const { createScenePlaybackAdapter } = await import('./playbackAdapter.js');
const statuses = [];
+15 -13
View File
@@ -1,5 +1,5 @@
import { updateStatusBadge } from './statusBadge.js';
import { t, tn } from '../i18n/index.js';
import { keepCodesWithValues, t, tn } from '../i18n/index.js';
import { cctvSourceLabelText, cctvSourceMessageText } from './cctvFrames.js';
import { createCctvVideoSurface } from './cctvVideo.js';
@@ -167,18 +167,20 @@ export function _renderCctvState(state) {
: '';
const projLabel =
state?.showProjection !== false ? t('MONITOR') : t('OFF');
this._cctvMeta.textContent = t(
'{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}',
{
city: activeCamera.city,
heading: `${Math.round(activeCamera.headingDeg)}°`,
fov: `${Math.round(activeCamera.fovDeg)}°`,
range: Math.round(activeCamera.rangeM),
projection: projLabel,
calBadge: calBadge ? ` · ${calBadge}` : '',
provider,
status: `${credit}${statusMsg}`,
},
this._cctvMeta.textContent = keepCodesWithValues(
t(
'{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}',
{
city: activeCamera.city,
heading: `${Math.round(activeCamera.headingDeg)}°`,
fov: `${Math.round(activeCamera.fovDeg)}°`,
range: Math.round(activeCamera.rangeM),
projection: projLabel,
calBadge: calBadge ? ` · ${calBadge}` : '',
provider,
status: `${credit}${statusMsg}`,
},
),
);
} else if (cameras.length > 0) {
this._cctvMeta.textContent = enabled
+7 -1
View File
@@ -80,7 +80,13 @@ export function createSceneDialog(title, onClose) {
border: '1px solid #397080',
font: multiline ? '12px monospace' : 'inherit',
});
if (multiline) node.rows = 9;
if (multiline) {
node.rows = 9;
// The multiline fields hold JSON: code reads left to right in every
// language, or braces, quotes and minus signs move in Arabic.
node.dir = 'ltr';
node.spellcheck = false;
}
wrapper.append(node);
if (type === 'file') fileChooser(wrapper, node);
body.append(wrapper);
+39 -2
View File
@@ -37,8 +37,8 @@ html:lang(ar) {
'Noto Sans Arabic', 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Geeza Pro',
Tahoma, monospace;
--font-sans:
'Inter', 'Noto Sans Arabic', -apple-system, BlinkMacSystemFont,
'Geeza Pro', Tahoma, sans-serif;
'Inter', 'Noto Sans Arabic', -apple-system, BlinkMacSystemFont, 'Geeza Pro',
Tahoma, sans-serif;
}
/* The Global Context lines and their buttons set ui-monospace through the
`font` shorthand; route them through the Arabic-aware stack too. */
@@ -139,3 +139,40 @@ html[dir='rtl']
:is(.context-radio-mini-transport, .sdr-seek-row, .radio-tuner) {
direction: ltr;
}
/* Canvas labels (installations, map cards, detections) are measured and drawn
with left-to-right geometry: a card's text starts at its left padding. A
canvas inherits the page direction, which turned that start into the right
edge and pushed every title out of its card. */
html[dir='rtl'] canvas {
direction: ltr;
}
/* Scene callouts on the screen whiteboard: the card grows to the right of the
text's x, so right-to-left text ends there instead of starting there. The
injected stack (JetBrains Mono) has no Arabic glyphs; use the Arabic-aware
one. */
html[dir='rtl'] .gev-anno-text {
text-anchor: end;
font-family: var(--font-mono);
letter-spacing: 0;
}
/* Position counters built from bare numbers (01 / 25, 1 / 3) read as a number
pair, left to right, as an unspaced 1/3 does; otherwise the slash swaps
them and the current item looks like the total. Translated counters say
«من» instead. */
html[dir='rtl']
:is(
#bhote-koshi-event-panel [data-role='beat-index'],
#cockpit-brief-position
) {
direction: ltr;
unicode-bidi: isolate;
}
/* The radio tuner header is a left-to-right instrument row; its channel
readout («القناة 01 من 750») still reads in its own language. */
html[dir='rtl'] #radio-tuner-value {
unicode-bidi: plaintext;
}