diff --git a/docs/translations/fr.md b/docs/translations/fr.md index 8a10804b0..34ba9c9cb 100644 --- a/docs/translations/fr.md +++ b/docs/translations/fr.md @@ -9,7 +9,7 @@ Translation source: abiyeenzo (#907); the French part of meirm's #312 was used f - Standard French that reads naturally in France, Belgium, Switzerland and Canada; avoid regionalisms. - Address the user with "vous" imperatives: « Cliquez », « Activez », « Réessayez ». Tooltips that name an action use the infinitive: « Réduire le panneau », « Réinitialiser la caméra ». UPPERCASE HUD instructions also use the infinitive: « CLIQUER SUR UNE CAMÉRA POUR L'ACTIVER ». - Sentence case, not Title Case: "Live Flights" → « Vols en direct ». UPPERCASE English stays UPPERCASE, accents included: « ÉTAT », « PRÉC. ». -- Typography: a narrow no-break space (U+202F) goes before `:` `;` `?` `!` and `%`, and inside « » quotes: `Supprimer le plan « {shot} » ?`. It keeps the sign on the same line as the word before it. Do not type a plain space there. Ellipsis is `…`, not `...`. Dashes keep their spaces: « flux indisponible — réessayez ». +- Typography: a narrow no-break space (U+202F) goes before `:` `;` `?` `!` and `%`, and inside « » quotes: `Supprimer le plan « {shot} » ?`. It keeps the sign on the same line as the word before it. Do not type a plain space there. The same narrow no-break space separates thousands in a number written into the text: « 1 000 km ». Ellipsis is `…`, not `...`. Dashes keep their spaces: « flux indisponible — réessayez ». - Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: PRÉC., SUIV., RÉINIT., ENREG., INDISP., DIST. - The HUD codes HDG, BRG, CRS, FOV, DEST, SPD and ALT stay in English everywhere, as in cockpit avionics. Words are translated: RANGE → « PORTÉE », heading in a sentence → « cap », ground-track course → « route » (« ROUTE 270° »). - ON and OFF stay as they are on HUD toggles and chips (« CCTV ON », « PROJECTION OFF »); in a sentence use « activé » / « désactivé ». @@ -28,7 +28,7 @@ A `tn()` entry needs three forms. `Intl.PluralRules('fr')` picks: | `many` | 1 000 000, 2 000 000… (exact millions) | 1 000 000 de caméras | | `other` | everything else: 2–999 999, 2,5… | 3 caméras | -The `many` form is the `other` text with « de » (« d' » before a vowel) after the number: « {count} de couches effacées », « {count} D'ENTRÉES INCONNUES ». When the words after the number cannot agree (an UPPERCASE readout, a list), put the number after a colon in every form: « CAPTÉS : {count} ». The same goes for a layer name whose gender and number vary: « Couche activée : {label} », not « {label} activé ». +The `many` form is the `other` text with « de » (« d' » before a vowel) after the number: « {count} de couches effacées », « {count} D'ENTRÉES INCONNUES ». When the words after the number cannot agree (an UPPERCASE readout, a list), put the number after a colon in every form: « CAPTÉS : {count} ». The same goes for a layer name whose gender and number vary: « Couche activée : {label} », not « {label} activé », or a tool name: « Outil actif : {name} », not « {name} est actif ». ## Glossary @@ -66,6 +66,7 @@ The `many` form is the `other` text with « de » (« d' » before a vowel) afte | route (directions) / turn (maneuver) | itinéraire / manœuvre | | scene / shot / scene recipe | scène / plan / recette de scène | | bundle / data pack / anchor | archive / paquet de données / ancre | +| capture camera as anchor / set move start (scene editor) | ancre à la position de la caméra / vue actuelle comme départ | | story beat | étape du récit | | briefing / panel / dock | briefing / panneau / dock | | cockpit | cockpit | @@ -75,6 +76,7 @@ The `many` form is the `other` text with « de » (« d' » before a vowel) afte | Clean UI | vue épurée | | Street Level (layer) / Street View (Google) | Niveau rue / Street View | | Recent Imagery / box | imagerie récente / zone | +| Daily overview / overview (Recent Imagery VIIRS product) | vue quotidienne (one name for the product, not « vue d'ensemble ») | | pin (Draw tool) / pin (Recent Imagery) | épingle / épingler | | Cyber (HUD layout, effect) | Cyber | | heading / course (ground track) / altitude / ground speed | cap / route / altitude / vitesse sol | diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index f90c1d50e..b63ee45f6 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -140,7 +140,7 @@ "◉ CINEMATIC": "◉ CINÉMATIQUE", "↺ FULL STORY": "↺ RÉCIT COMPLET", "↗ OPEN ORIGINAL": "↗ OUVRIR L'ORIGINAL", - "⌖ LOWER GORGE": "⌖ GORGE INFÉRIEURE", + "⌖ LOWER GORGE": "⌖ GORGE AVAL", "FIELD REPORTS · {count}": "RAPPORTS DE TERRAIN · {count}", "GEOLOCATIONS · {credit}": "GÉOLOCALISATIONS · {credit}", "↗ OPEN PUBLIC MAP": "↗ OUVRIR LA CARTE PUBLIQUE", @@ -184,7 +184,7 @@ "USGS INITIAL REPORT": "RAPPORT INITIAL DE L'USGS", "26 AUG 2026 · 08:37 NPT": "26 AOÛT 2026 · 08:37 NPT", "28.2710° N · 85.5150° E": "28,2710° N · 85,5150° E", - "INITIAL M4.4 · RECLASSIFIED M5.2 LANDSLIDE": "M4,4 INITIALE · RECLASSÉ EN GLISSEMENT DE TERRAIN M5,2", + "INITIAL M4.4 · RECLASSIFIED M5.2 LANDSLIDE": "INITIALEMENT M4,4 · RECLASSÉ EN GLISSEMENT DE TERRAIN M5,2", "USGS EVENT us7000tbwb · OPEN SOURCE": "ÉVÉNEMENT USGS us7000tbwb · OUVRIR LA SOURCE", "Open the USGS initial report for the 26 August 2026 Nepal landslide": "Ouvrir le rapport initial de l'USGS sur le glissement de terrain du 26 août 2026 au Népal", "Show the Nepal context callout without a border or country fill": "Afficher l'encart de contexte du Népal sans frontière ni remplissage du pays", @@ -271,7 +271,7 @@ "NAV": "NAV", "GNSS navigation — GPS, GLONASS, Galileo": "Navigation GNSS — GPS, GLONASS, Galileo", "GEO": "GEO", - "Geostationary belt — comms and weather, fixed over the equator": "Ceinture géostationnaire — télécoms et météo, fixes au-dessus de l'équateur", + "Geostationary belt — comms and weather, fixed over the equator": "Ceinture géostationnaire — télécoms et météo, en position fixe au-dessus de l'équateur", "VISUAL": "VISUEL", "Brightest naked-eye objects — CelesTrak visual group": "Objets les plus brillants à l'œil nu — groupe visuel CelesTrak", "COMMS": "TÉLÉCOM", @@ -668,7 +668,7 @@ "Use the {direction} lane": "Prenez la voie {direction}", "Continue onto {road}": "Continuez sur {road}", "maneuver|Continue": "Continuez", - "{mode} — reroute for {modeLower}": "{mode} — recalculer l'itinéraire : {modeLower}", + "{mode} — reroute for {modeLower}": "{mode} — recalculer l'itinéraire {modeLower}", "CLICK MAP": "CLIQUER SUR LA CARTE", "SET A": "PLACER A", "Click a spot on the globe to place A (click again to cancel)": "Cliquez sur un point du globe pour placer A (cliquez à nouveau pour annuler)", @@ -678,14 +678,14 @@ "Then click the globe to place the destination": "Puis cliquez sur le globe pour placer la destination", "Swap A and B": "Inverser A et B", "FLY ···": "SURVOL ···", - "FLYING": "SURVOL EN COURS", + "FLYING": "EN SURVOL", "FLY": "SURVOLER", "Fly the camera along the route": "Faire voler la caméra le long de l'itinéraire", "Place A and B first": "Placez d'abord A et B", "action|CLEAR": "EFFACER", "Remove the route and both markers": "Supprimer l'itinéraire et les deux marqueurs", "press Escape twice to leave Draw": "appuyez deux fois sur Esc pour quitter Dessin", - "{name} is active — turn {name} off, then set {endpoint}": "{name} est actif — désactivez {name}, puis placez {endpoint}", + "{name} is active — turn {name} off, then set {endpoint}": "Outil actif : {name} — désactivez {name}, puis placez {endpoint}", "{name} is active — {leave}, then set {endpoint}": "{name} est actif — {leave}, puis placez {endpoint}", "Only the first {count} turns of this route are shown": "Seules les {count} premières manœuvres de cet itinéraire sont affichées", "Turn-by-turn directions": "Instructions pas à pas", @@ -994,7 +994,7 @@ "Techno": "Techno", "Trance": "Trance", "World": "Musiques du monde", - "overview": "vue d'ensemble", + "overview": "vue quotidienne", "cloud unknown": "nébulosité inconnue", "{percent}% scene cloud": "{percent} % de nuages sur la scène", "{min}–{max}% scene cloud": "{min}–{max} % de nuages sur la scène", @@ -1198,7 +1198,7 @@ "Observation unavailable": "Observation indisponible", "Waiting for observation": "En attente d'observation", "{detail} · synced": "{detail} · synchro", - "{detail} · nearest": "{detail} · la plus proche", + "{detail} · nearest": "{detail} · plus proche", "RADAR REFLECTIVITY · dBZ": "RÉFLECTIVITÉ RADAR · dBZ", "LIGHTNING DENSITY · 15 min accumulation": "DENSITÉ DE FOUDRE · cumul sur 15 min", "GLOBAL INFRARED · hourly": "INFRAROUGE MONDIAL · horaire", @@ -1471,9 +1471,9 @@ "Anchors and data packs": "Ancres et paquets de données", "Shot camera, timing, packs and actions": "Caméra, minutage, paquets et actions du plan", "New anchor ID": "ID de la nouvelle ancre", - "Capture camera as anchor": "Capturer la caméra comme ancre", + "Capture camera as anchor": "Créer une ancre à la position de la caméra", "Anchor added to draft": "Ancre ajoutée au brouillon", - "Set move start to current camera": "Définir le début du mouvement sur la caméra actuelle", + "Set move start to current camera": "Utiliser la vue actuelle comme départ du mouvement", "Move added to draft; destination remains the shot camera": "Mouvement ajouté au brouillon ; la destination reste la caméra du plan", "Use ordinary flight": "Utiliser un vol normal", "Invalid shot JSON": "JSON de plan non valide", @@ -1542,7 +1542,7 @@ "Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Changer de langue recharge la page. Ceci sera perdu : {items}. Changer quand même ?", "VISUAL PRESETS": "PRÉRÉGLAGES VISUELS", "LOCATION": "LIEU", - "CONTACTS — nearest planes · vessels · sites": "CONTACTS — avions · navires · sites les plus proches", + "CONTACTS — nearest planes · vessels · sites": "CONTACTS — avions, navires et sites les plus proches", "SPACE MISSIONS — launches & orbital assets": "MISSIONS SPATIALES — lancements et objets orbitaux", "LIVE SIGNALS": "SIGNAUX EN DIRECT", "Contacts could not complete the requested transition; try again": "Les contacts n'ont pas pu effectuer la transition demandée ; réessayez", @@ -1844,7 +1844,7 @@ "Other layers": "Autres couches", "{state} · {source}": "{state} · {source}", "UNCERTAIN · {source} · lifecycle state requires reconciliation": "INCERTAIN · {source} · état à rétablir", - "{state} · {source} · {detail} · retry {seconds}s": "{state} · {source} · {detail} · nouvel essai {seconds} s", + "{state} · {source} · {detail} · retry {seconds}s": "{state} · {source} · {detail} · nouvel essai dans {seconds} s", "{state} · {source} · {detail}": "{state} · {source} · {detail}", "never": "jamais", "{source} · {loadingLabel}": "{source} · {loadingLabel}", @@ -1898,7 +1898,7 @@ "LOCATING…": "LOCALISATION…", "LOCATED": "LOCALISÉ", "LOCATE": "LOCALISER", - " · read while Local ADS-B is on": " · lus tant que ADS-B local est activé", + " · read while Local ADS-B is on": " · lus tant qu'ADS-B local est activé", "Decoder feeds: {feeds}": "Flux du décodeur : {feeds}", "Language: {language}": "Langue : {language}", "Change the interface language": "Changer la langue de l'interface", @@ -2055,7 +2055,7 @@ "S on another day replaces it · × unpins": "S sur un autre jour le remplace · × le désépingle", "← → preview · S shows the focused day": "← → aperçu · S affiche le jour ciblé", "ZOOM IN": "ZOOMER", - "Fly in until the view fits the 1,000 km limit": "Rapprochez-vous jusqu'à ce que la vue tienne dans la limite de 1 000 km", + "Fly in until the view fits the 1,000 km limit": "Rapprochez-vous jusqu'à ce que la vue tienne dans la limite de 1 000 km", "Imagery days, newest first · arrows move, S or A and B pin": "Jours d'imagerie, du plus récent au plus ancien · les flèches déplacent, S ou A et B épinglent", "On the map": "Sur la carte", "Mode": "Mode", @@ -2660,7 +2660,7 @@ "Next ISS pass": "Prochain passage de l'ISS", "Next satellite pass": "Prochain passage du satellite", "Mark the map": "Marquer la carte", - "Mark {targets} +{more}": "Marquer {targets} +{more}", + "Mark {targets} +{more}": "Marquer : {targets} +{more}", "Mark {targets}": "Marquer : {targets}", "Fly to {place}": "Rejoindre {place}", "Fly to Location": "Rejoindre le lieu", @@ -2822,7 +2822,7 @@ "All": "Tous", "Enable Radio, then choose a globe marker or use next.": "Activez la Radio, puis choisissez un marqueur sur le globe ou utilisez SUIV.", "DRAG TO TUNE": "GLISSER POUR RÉGLER", - "Tune available internet radio stations": "Écouter les stations de radio Internet disponibles", + "Tune available internet radio stations": "Régler sur une station de radio Internet disponible", "ALL · DRAG THE NEEDLE": "TOUTES · DÉPLACER L'AIGUILLE", "SNAPS TO AVAILABLE STATIONS": "SE CALE SUR LES STATIONS DISPONIBLES", "Radio playback": "Lecture radio", @@ -2964,7 +2964,7 @@ "IMAGERY": "IMAGES", "Panorama filter": "Filtre de panoramas", "ALL": "TOUTES", - "FLAT": "PLATES", + "FLAT": "CLASSIQUES", "SINCE": "DEPUIS", "SCENES": "SCÈNES", "Scene recipe": "Recette de scène", diff --git a/src/layers/cctv/presentation.i18n.test.mjs b/src/layers/cctv/presentation.i18n.test.mjs new file mode 100644 index 000000000..4bfa5186b --- /dev/null +++ b/src/layers/cctv/presentation.i18n.test.mjs @@ -0,0 +1,58 @@ +// The CCTV HUD summary shows the CAL badge, source kind and view band in the +// UI language; English keeps its codes, and unknown source codes stay codes. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { setLocale } from '../../i18n/index.js'; +import { createPresentation } from './presentation.js'; + +function summary(sourceKind) { + const camera = { + id: 'c1', + city: 'Austin', + name: 'Cam', + rangeM: 100, + fovDeg: 60, + feedType: 'image', + }; + const record = { camera }; + const presentation = createPresentation({ + state: { + _records: [record], + _healthById: new Map(sourceKind ? [['c1', { sourceKind }]] : []), + _showProjection: false, + _coverageMode: 'sector', + }, + services: {}, + parts: { + selection: { getActiveRecord: () => record }, + model: { + sectorAreaKm2: () => 1, + currentViewContext: () => 'street:1:2', + }, + geometry: { coverageNeighborCount: () => 0 }, + calibration: { deriveCalBadge: () => 'raw-prior' }, + }, + source: {}, + }); + return presentation.buildSummaryText(); +} + +test('en: CAL, SRC and CONTEXT keep their English codes', () => { + setLocale('en'); + assert.match(summary('upstream'), /CAL RAW PRIOR · SRC UPSTREAM/); + assert.match(summary(null), /SRC IMAGE · STREET CONTEXT/); + assert.match(summary('upstream-image'), /SRC UPSTREAM-IMAGE/); +}); + +test('fr: CAL, SRC and CONTEXT values are translated', () => { + setLocale('fr'); + try { + assert.match( + summary('upstream'), + /CAL ESTIM\. INITIALE · SRC AMONT · CONTEXTE RUE/, + ); + assert.match(summary('upstream-image'), /SRC UPSTREAM-IMAGE/); + } finally { + setLocale('en'); + } +}); diff --git a/src/layers/cctv/presentation.js b/src/layers/cctv/presentation.js index e47e08194..898e4605c 100644 --- a/src/layers/cctv/presentation.js +++ b/src/layers/cctv/presentation.js @@ -1,5 +1,5 @@ import { CCTV_AMBIENT_CARD_MAX } from '../../data/cctvLod.js'; -import { t } from '../../i18n/index.js'; +import { msg, t } from '../../i18n/index.js'; import { ACTIVE_FRAME_REFRESH_MS, IDLE_FRAME_REFRESH_MS } from './policy.js'; import { headingHudToken, isHeadingEstimated } from './headingConfidence.js'; import { @@ -8,6 +8,59 @@ import { isDatasheetFov, } from './modelSpecs.js'; +// The summary line names the CAL badge, the source kind and the view band in +// the page language; the codes underneath stay English. +const CAL_BADGE_LABELS = Object.freeze({ + calibrated: msg('CALIBRATED'), + curated: msg('CURATED'), + 'raw-prior': msg('RAW PRIOR'), +}); + +// The same source-kind words as the CCTV panel (src/ui/cctvFrames.js). +const SOURCE_KIND_LABELS = Object.freeze({ + snapshot: msg('SNAPSHOT'), + live: msg('LIVE'), + fallback: msg('FALLBACK'), + upstream: msg('UPSTREAM'), + synthetic: msg('SYNTHETIC'), + configured: msg('CONFIGURED'), + seed: msg('SEED'), + image: msg('IMAGE'), + unknown: msg('UNKNOWN'), +}); + +/** CAL badge id ('raw-prior') as the summary shows it; unknown ids stay codes. */ +export function summaryCalLabel(badge) { + const key = String(badge || ''); + return CAL_BADGE_LABELS[key] + ? t(CAL_BADGE_LABELS[key]) + : key.replace('-', ' ').toUpperCase(); +} + +/** Source kind ('image', 'snapshot') for the summary; HLS, MJPEG… stay codes. */ +export function summarySourceLabel(kind) { + const key = String(kind || 'image').toLowerCase(); + return SOURCE_KIND_LABELS[key] + ? t(SOURCE_KIND_LABELS[key]) + : key.toUpperCase(); +} + +/** View band id ('street', 'city'…) as the HUD names it. */ +export function summaryViewBandLabel(band) { + switch (band) { + case 'street': + return t('STREET', { context: 'view band' }); + case 'city': + return t('CITY', { context: 'view band' }); + case 'regional': + return t('REGIONAL', { context: 'view band' }); + case 'global': + return t('GLOBAL', { context: 'view band' }); + default: + return String(band || '').toUpperCase(); + } +} + export function createPresentation({ state: layerState, services, @@ -72,17 +125,13 @@ export function createPresentation({ : t('ISOLATED VIEW'), layerState._showProjection ? t('PROJ MONITOR') : t('PROJ OFF'), layerState._coverageMode === 'viewshed' ? t('VIEWSHED') : null, - t('CAL {value}', { - value: calBadge.replace('-', ' ').toUpperCase(), + t('CAL {value}', { value: summaryCalLabel(calBadge) }), + t('SRC {value}', { + value: summarySourceLabel( + health?.sourceKind || active.camera.feedType || 'image', + ), }), - health?.sourceKind - ? t('SRC {value}', { - value: String(health.sourceKind).toUpperCase(), - }) - : t('SRC {value}', { - value: String(active.camera.feedType || 'image').toUpperCase(), - }), - t('{value} CONTEXT', { value: viewBand.toUpperCase() }), + t('{value} CONTEXT', { value: summaryViewBandLabel(viewBand) }), // Identified hardware: append the model's datasheet specs + attribution. active.camera.spec ? t('{spec} · SPECS: CCTV-DATABASE.COM', {