mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Switch language in place, without a reload
Choosing a language in the picker now applies it at once. The choice is stored and ?lang is kept in step through replaceState, and the picker keeps focus. Code that writes translated text itself re-presents it on a switch: onLocaleChange() lets an owner listen, removed at teardown, and status writers hand over a function of the locale (voice cards, scene director, SDR, draw hints, imagery split, map sources, Bhote Koshi media) so the words are rebuilt rather than re-sent. The HUD, cockpit, radio, CCTV, launch, awareness and credit texts follow the same hook, and applyDocumentTranslations can be applied again without undoing text code wrote since. The unsaved-work registry and persistLocaleAndReload existed only to guard the reload, so they go, with their catalog entries.
This commit is contained in:
1 parent
ba20bc13d8
commit
b20c80e590
87 files changed
+1194
-777
No files matched your search
+51
-26
@@ -7,7 +7,8 @@ import {
|
||||
formatNumber,
|
||||
formatRelativeTime,
|
||||
msg,
|
||||
persistLocaleAndReload,
|
||||
onLocaleChange,
|
||||
persistLocale,
|
||||
setLocale,
|
||||
t,
|
||||
tn,
|
||||
@@ -258,6 +259,40 @@ test('applyDocumentTranslations translates opted-in text and attributes in place
|
||||
assert.equal(label.textContent, t('POWERED UP'));
|
||||
assert.equal(pin.attrs.get('title'), t('POWERED UP'));
|
||||
assert.equal(unknown.textContent, 'No such message');
|
||||
// A later pass starts from the English source, so every switch applies.
|
||||
setLocale('de');
|
||||
applyDocumentTranslations(doc);
|
||||
assert.equal(label.textContent, t('POWERED UP'));
|
||||
setLocale('en');
|
||||
assert.equal(applyDocumentTranslations(doc), 2, 'English restores');
|
||||
assert.equal(label.textContent, 'POWERED UP');
|
||||
assert.equal(pin.attrs.get('title'), 'POWERED UP');
|
||||
// Text that code wrote since is the new source; a pass leaves it alone.
|
||||
label.textContent = 'Error: offline';
|
||||
setLocale('ru');
|
||||
applyDocumentTranslations(doc);
|
||||
assert.equal(label.textContent, 'Error: offline');
|
||||
});
|
||||
|
||||
test('onLocaleChange listeners hear each change until removed', () => {
|
||||
const heard = [];
|
||||
const off = onLocaleChange(() => heard.push(t('POWERED UP')));
|
||||
const broken = onLocaleChange(() => {
|
||||
throw new Error('broken listener');
|
||||
});
|
||||
const error = console.error;
|
||||
console.error = () => {};
|
||||
try {
|
||||
setLocale('ru');
|
||||
setLocale('ru');
|
||||
assert.deepEqual(heard, [t('POWERED UP')], 'once, in the new locale');
|
||||
off();
|
||||
setLocale('en');
|
||||
assert.equal(heard.length, 1);
|
||||
} finally {
|
||||
console.error = error;
|
||||
broken();
|
||||
}
|
||||
});
|
||||
|
||||
test('applyDocumentTranslations reads data-i18n-context for element text', () => {
|
||||
@@ -275,13 +310,9 @@ test('applyDocumentTranslations reads data-i18n-context for element text', () =>
|
||||
assert.notEqual(option.textContent, t('All'));
|
||||
});
|
||||
|
||||
test('persistLocaleAndReload keeps the choice in ?lang when storage is blocked', () => {
|
||||
test('persistLocale keeps the choice in ?lang when storage is blocked', () => {
|
||||
const calls = [];
|
||||
const location = {
|
||||
href: 'https://gev.local/?x=1#scene=a',
|
||||
assign: (url) => calls.push(['assign', url]),
|
||||
reload: () => calls.push(['reload']),
|
||||
};
|
||||
const location = { href: 'https://gev.local/?x=1#scene=a' };
|
||||
const history = {
|
||||
replaceState: (_s, _t, url) => calls.push(['replace', url]),
|
||||
};
|
||||
@@ -290,36 +321,30 @@ test('persistLocaleAndReload keeps the choice in ?lang when storage is blocked',
|
||||
throw new Error('blocked');
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
persistLocaleAndReload('ru', { location, history, storage }),
|
||||
true,
|
||||
);
|
||||
assert.equal(persistLocale('ru', { location, history, storage }), 'ru');
|
||||
assert.deepEqual(calls, [
|
||||
['replace', 'https://gev.local/?x=1&lang=ru#scene=a'],
|
||||
['reload'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('persistLocaleAndReload stores the choice, keeps the hash, strips ?lang', () => {
|
||||
test('persistLocale stores the choice, keeps the hash, strips ?lang', () => {
|
||||
const storage = memoryStorage();
|
||||
const calls = [];
|
||||
const location = {
|
||||
href: 'https://gev.local/?lang=en&x=1#scene=a',
|
||||
assign: (url) => calls.push(['assign', url]),
|
||||
reload: () => calls.push(['reload']),
|
||||
};
|
||||
const location = { href: 'https://gev.local/?lang=en&x=1#scene=a' };
|
||||
const history = {
|
||||
replaceState: (_s, _t, url) => calls.push(['replace', url]),
|
||||
};
|
||||
assert.equal(
|
||||
persistLocaleAndReload('ru', { location, history, storage }),
|
||||
true,
|
||||
);
|
||||
assert.equal(persistLocale('ru', { location, history, storage }), 'ru');
|
||||
assert.equal(storage.data.get(LOCALE_STORAGE_KEY), 'ru');
|
||||
assert.deepEqual(calls, [
|
||||
['replace', 'https://gev.local/?x=1#scene=a'],
|
||||
['reload'],
|
||||
]);
|
||||
assert.deepEqual(calls, [['replace', 'https://gev.local/?x=1#scene=a']]);
|
||||
// An address without ?lang is left as it is.
|
||||
calls.length = 0;
|
||||
persistLocale('de', {
|
||||
location: { href: 'https://gev.local/#scene=a' },
|
||||
history,
|
||||
storage,
|
||||
});
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('tn() shows a caller-formatted count and still picks the form by number', () => {
|
||||
|
||||
+65
-65
@@ -53,6 +53,37 @@ let pageLocale = { value: DEFAULT_LOCALE };
|
||||
export function bindLocale(locale) {
|
||||
locale.value = pageLocale.value;
|
||||
pageLocale = locale;
|
||||
bound = true;
|
||||
}
|
||||
|
||||
let bound = false;
|
||||
const localeListeners = new Set();
|
||||
|
||||
/**
|
||||
* Call `listener` after the active locale changes, so code that shows text it
|
||||
* translated itself (a status line, a count, a time) shows it again. Text a
|
||||
* Vue view translates while rendering follows the locale without this.
|
||||
* @param {() => void} listener
|
||||
* @returns {() => void} Stops listening.
|
||||
*/
|
||||
export function onLocaleChange(listener) {
|
||||
if (typeof listener !== 'function') return () => {};
|
||||
localeListeners.add(listener);
|
||||
return () => localeListeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the listeners the locale changed. setLocale does this for an unbound
|
||||
* locale; the owner of a bound ref calls it when the ref changes.
|
||||
*/
|
||||
export function notifyLocaleChange() {
|
||||
for (const listener of [...localeListeners]) {
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error('[i18n] locale listener failed', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The catalog for a locale; null for English and unknown codes. */
|
||||
@@ -74,8 +105,9 @@ export function getLocale() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the active locale. Pure state change: call applyDocumentLanguage /
|
||||
* applyDocumentTranslations to reflect it in the DOM.
|
||||
* Switch the active locale. Views re-render and onLocaleChange listeners
|
||||
* word their text again; the application keeps <html lang>/<html dir> and
|
||||
* the page's data-i18n markup in step.
|
||||
* @returns {string} The locale actually applied (normalized).
|
||||
*/
|
||||
export function setLocale(locale) {
|
||||
@@ -83,6 +115,7 @@ export function setLocale(locale) {
|
||||
if (normalized !== pageLocale.value) {
|
||||
pageLocale.value = normalized;
|
||||
formatterCache.clear();
|
||||
if (!bound) notifyLocaleChange();
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
@@ -276,75 +309,57 @@ const DOCUMENT_TRANSLATION_TARGETS = Object.freeze([
|
||||
['data-i18n-placeholder', 'placeholder'],
|
||||
]);
|
||||
|
||||
// Per element, the English source a pass read and the text it wrote, so a
|
||||
// later pass in another locale, English included, starts from the source
|
||||
// rather than from that text. Text that code wrote since is the new source.
|
||||
const documentSources = new WeakMap();
|
||||
|
||||
/**
|
||||
* Translate every opted-in element in `doc`. Runs once at boot; switching
|
||||
* locale reloads the page.
|
||||
* Translate every opted-in element in `doc` into the active locale. Runs at
|
||||
* boot and again after every locale change (the page's own markup, such as
|
||||
* a host's, is not rendered by a view).
|
||||
* @param {Document|Element} doc
|
||||
* @returns {number} Count of element/attribute writes performed.
|
||||
*/
|
||||
export function applyDocumentTranslations(doc) {
|
||||
if (typeof doc?.querySelectorAll !== 'function') return 0;
|
||||
if (!CATALOGS[getLocale()]) return 0;
|
||||
let applied = 0;
|
||||
for (const [marker, attribute] of DOCUMENT_TRANSLATION_TARGETS) {
|
||||
for (const element of doc.querySelectorAll(`[${marker}]`)) {
|
||||
const source = attribute
|
||||
? element.getAttribute(attribute)
|
||||
: normalizeMarkupText(element.textContent);
|
||||
const read = () =>
|
||||
attribute
|
||||
? element.getAttribute(attribute)
|
||||
: normalizeMarkupText(element.textContent);
|
||||
let sources = documentSources.get(element);
|
||||
if (!sources) documentSources.set(element, (sources = {}));
|
||||
const shown = read();
|
||||
const known = sources[marker];
|
||||
const source = known && shown === known.written ? known.source : shown;
|
||||
sources[marker] = { source, written: shown };
|
||||
if (!source) continue;
|
||||
const context = attribute
|
||||
? null
|
||||
: element.getAttribute?.('data-i18n-context');
|
||||
const text = context ? t(source, { context }) : t(source);
|
||||
if (text === source) continue;
|
||||
if (text === shown) continue;
|
||||
if (attribute) element.setAttribute(attribute, text);
|
||||
else element.textContent = text;
|
||||
sources[marker].written = read();
|
||||
applied += 1;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
const unsavedWorkChecks = new Set();
|
||||
|
||||
/**
|
||||
* Name work that switching language would lose: the switch reloads the page,
|
||||
* and the language switch asks first while any check reports something.
|
||||
* @param {() => string|null|undefined} check Returns a msg() label (e.g.
|
||||
* msg('unsaved CCTV calibration')) while there is such work, else nothing.
|
||||
* @returns {() => void} Removes the check.
|
||||
* Store a locale choice for the next visit and keep the address in step
|
||||
* without reloading: ?lang is dropped once the choice is stored, or carries
|
||||
* the choice where storage is blocked (share links drop it:
|
||||
* ShareLinkManager.copyLink). The hash is kept exactly. setLocale applies
|
||||
* the choice to the page.
|
||||
* @returns {string} The normalized locale.
|
||||
*/
|
||||
export function registerUnsavedWork(check) {
|
||||
if (typeof check !== 'function') return () => {};
|
||||
unsavedWorkChecks.add(check);
|
||||
return () => unsavedWorkChecks.delete(check);
|
||||
}
|
||||
|
||||
/**
|
||||
* What a reload would lose right now, translated, without repeats.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function describeUnsavedWork() {
|
||||
const labels = new Set();
|
||||
for (const check of unsavedWorkChecks) {
|
||||
try {
|
||||
const label = check();
|
||||
if (typeof label === 'string' && label) labels.add(t(label));
|
||||
} catch {
|
||||
// A failing check must not block the switch.
|
||||
}
|
||||
}
|
||||
return [...labels];
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a locale choice, then reload preserving window.location.hash exactly
|
||||
* and stripping ?lang from the search string, so the stored choice applies.
|
||||
* Where storage is blocked, ?lang carries the choice instead (share links
|
||||
* drop it: ShareLinkManager.copyLink).
|
||||
* @returns {boolean} true when a reload was triggered.
|
||||
*/
|
||||
export function persistLocaleAndReload(
|
||||
export function persistLocale(
|
||||
locale,
|
||||
{
|
||||
location = globalThis.location,
|
||||
@@ -354,28 +369,13 @@ export function persistLocaleAndReload(
|
||||
) {
|
||||
const normalized = normalizeLocale(locale) || DEFAULT_LOCALE;
|
||||
const stored = writeStoredLocale(normalized, storage);
|
||||
if (!location || typeof location.assign !== 'function') return false;
|
||||
let target = location.href;
|
||||
try {
|
||||
// Without storage the choice only survives in the URL, so keep it there.
|
||||
const url = new URL(location.href);
|
||||
if (stored) url.searchParams.delete('lang');
|
||||
else url.searchParams.set('lang', normalized);
|
||||
target = url.href;
|
||||
if (url.href !== location.href) history?.replaceState?.(null, '', url.href);
|
||||
} catch {
|
||||
// Keep the current href.
|
||||
// Without a location or history the stored choice still applies.
|
||||
}
|
||||
// location.assign() to the identical URL does not navigate, so strip ?lang
|
||||
// in place and force a real reload instead.
|
||||
try {
|
||||
history?.replaceState?.(null, '', target);
|
||||
} catch {
|
||||
// replaceState is best-effort; the reload below still applies.
|
||||
}
|
||||
if (typeof location.reload === 'function') {
|
||||
location.reload();
|
||||
} else {
|
||||
location.assign(target);
|
||||
}
|
||||
return true;
|
||||
return normalized;
|
||||
}
|
||||
@@ -872,7 +872,6 @@
|
||||
"That line has no length — click somewhere further away.": "Diese Linie hat keine Länge – weiter entfernt klicken.",
|
||||
" · {max}-point limit reached": " · Limit von {max} Punkten erreicht",
|
||||
"{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · Doppelklick oder Enter beendet, Rücktaste macht rückgängig, Esc bricht ab.{full}",
|
||||
"drawings on the map": "Zeichnungen auf der Karte",
|
||||
"That shape already has {count} points — finish it or press Backspace.": "Diese Form hat bereits {count} Punkte – abschließen oder Rücktaste drücken.",
|
||||
"That point is off the globe — click on the world.": "Dieser Punkt liegt außerhalb des Globus – auf die Erde klicken.",
|
||||
"That shape could not be placed.": "Diese Form konnte nicht platziert werden.",
|
||||
@@ -1173,7 +1172,6 @@
|
||||
"GET KEY ↗": "SCHLÜSSEL HOLEN ↗",
|
||||
"{envVar} saved — paste to replace": "{envVar} gespeichert – zum Ersetzen einfügen",
|
||||
"paste {envVar}": "{envVar} einfügen",
|
||||
"unsaved provider keys": "ungespeicherte Anbieter-Schlüssel",
|
||||
"Project keys: {status}": "Projektschlüssel: {status}",
|
||||
"Cloud voice will use OPENAI_API_KEY on the next session.": "Die Cloud-Sprachsteuerung verwendet ab der nächsten Sitzung OPENAI_API_KEY.",
|
||||
"Checking local ChatGPT OAuth sign-in…": "Lokale ChatGPT-OAuth-Anmeldung wird geprüft…",
|
||||
@@ -2049,9 +2047,6 @@
|
||||
"Sensitivity": "Empfindlichkeit",
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"unsaved CCTV calibration": "ungespeicherte CCTV-Kalibrierung",
|
||||
"the open cockpit": "das geöffnete Cockpit",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Ein Sprachwechsel lädt die Seite neu. Dabei geht Folgendes verloren: {items}. Trotzdem wechseln?",
|
||||
"Link shared": "Link geteilt",
|
||||
"Return map to straight-down view": "Karte wieder senkrecht von oben zeigen",
|
||||
"Tilt map to oblique view": "Karte in Schrägansicht kippen",
|
||||
@@ -2098,7 +2093,6 @@
|
||||
"{aircraftClass} · {emergency} · {feedState}": "{aircraftClass} · {emergency} · {feedState}",
|
||||
"{source} · LOCATION QUERY · RECENT": "{source} · ORTSABFRAGE · AKTUELL",
|
||||
"Rendered globe frames per second · toggle with `": "Gerenderte Globus-Frames pro Sekunde · umschalten mit `",
|
||||
"imported map files": "importierte Kartendateien",
|
||||
"Ground heights could not be refreshed. Try the file again when the map has loaded.": "Geländehöhen konnten nicht aktualisiert werden. Die Datei erneut laden, wenn die Karte geladen ist.",
|
||||
"{count} map objects": {
|
||||
"one": "{count} Kartenobjekt",
|
||||
@@ -2678,7 +2672,6 @@
|
||||
"Activate to toggle voice": "Aktivieren, um die Sprachsteuerung umzuschalten",
|
||||
"Voice active": "Sprachsteuerung aktiv",
|
||||
"Voice could not be started.": "Sprachsteuerung konnte nicht gestartet werden.",
|
||||
"the active voice session": "die aktive Sprachsitzung",
|
||||
"{value} feet": "{value} Fuß",
|
||||
"Fire · FRP {frp} MW": "Brand · FRP {frp} MW",
|
||||
"The flights layer is not enabled": "Die Ebene „Live-Flüge“ ist nicht aktiviert",
|
||||
|
||||
@@ -2466,9 +2466,6 @@
|
||||
"ChatGPT sign-in timed out. Click {button} to try again.": "Se agotó el tiempo para iniciar sesión en ChatGPT. Haz clic en \"{button}\" para intentarlo de nuevo.",
|
||||
"ChatGPT sign-in complete. OAuth will be used for the next cloud voice session.": "Sesión de ChatGPT iniciada. Se usará OAuth en la próxima sesión de voz en la nube.",
|
||||
"OAuth check failed: {detail}": "Error al comprobar OAuth: {detail}",
|
||||
"unsaved CCTV calibration": "la calibración de CCTV sin guardar",
|
||||
"the open cockpit": "la cabina abierta",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Cambiar de idioma recarga la página. Se perderá lo siguiente: {items}. ¿Cambiar de todos modos?",
|
||||
"Wind upstream unavailable": "Origen de datos de viento no disponible",
|
||||
"WebUSB needs desktop Chrome or Edge": "WebUSB requiere Chrome o Edge de escritorio",
|
||||
"connect a receiver in Radio": "conecta un receptor en Radio",
|
||||
@@ -2654,9 +2651,6 @@
|
||||
"KartaView answered HTTP {status}": "KartaView respondió HTTP {status}",
|
||||
"KartaView refused the request (code {code})": "KartaView rechazó la petición (código {code})",
|
||||
"Stopped by voice": "Detenido por voz",
|
||||
"drawings on the map": "los dibujos del mapa",
|
||||
"imported map files": "los archivos importados en el mapa",
|
||||
"the active voice session": "la sesión de voz activa",
|
||||
"Light aircraft": "Avioneta",
|
||||
"Glider": "Planeador",
|
||||
"Turboprop": "Turbohélice",
|
||||
@@ -2857,7 +2851,6 @@
|
||||
"many": "fuentes {names} no válidas",
|
||||
"other": "fuentes {names} no válidas"
|
||||
},
|
||||
"unsaved provider keys": "las claves de proveedores sin guardar",
|
||||
"Exit cockpit to fly to an aircraft": "Sal de la cabina para volar a una aeronave",
|
||||
"Exit cockpit to fly to a satellite": "Sal de la cabina para volar a un satélite",
|
||||
"Exit cockpit to frame contacts": "Sal de la cabina para encuadrar contactos",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"That line has no length — click somewhere further away.": "Cette ligne n'a pas de longueur — cliquez plus loin.",
|
||||
" · {max}-point limit reached": " · limite de {max} points atteinte",
|
||||
"{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · double-clic ou Enter pour terminer, Backspace annule le dernier point, Esc annule.{full}",
|
||||
"drawings on the map": "les dessins sur la carte",
|
||||
"That shape already has {count} points — finish it or press Backspace.": "Cette forme a déjà {count} points — terminez-la ou appuyez sur Backspace.",
|
||||
"That point is off the globe — click on the world.": "Ce point est hors du globe — cliquez sur la Terre.",
|
||||
"That shape could not be placed.": "Impossible de placer cette forme.",
|
||||
@@ -402,7 +401,6 @@
|
||||
"paste {envVar}": "collez {envVar}",
|
||||
"REMOVE": "SUPPRIMER",
|
||||
"Remove {title} from this app's saved keys": "Supprimer {title} des clés enregistrées de l'application",
|
||||
"unsaved provider keys": "les clés de fournisseurs non enregistrées",
|
||||
"Project keys: {status}": "Clés du projet : {status}",
|
||||
"your app configuration": "la configuration de votre application",
|
||||
"your local .env": "votre .env local",
|
||||
@@ -1540,9 +1538,6 @@
|
||||
"Sensitivity": "Sensibilité",
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"unsaved CCTV calibration": "la calibration CCTV non enregistrée",
|
||||
"the open cockpit": "le cockpit ouvert",
|
||||
"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 et sites les plus proches",
|
||||
@@ -1767,7 +1762,6 @@
|
||||
"Space Missions": "Missions spatiales",
|
||||
"Context": "Contexte",
|
||||
"Rendered globe frames per second · toggle with `": "Images du globe rendues par seconde · basculer avec `",
|
||||
"imported map files": "les fichiers importés sur la carte",
|
||||
"Ground heights could not be refreshed. Try the file again when the map has loaded.": "Impossible d'actualiser les altitudes du sol. Réessayez avec le fichier une fois la carte chargée.",
|
||||
"{count} map objects": {
|
||||
"one": "{count} objet cartographique",
|
||||
@@ -2547,7 +2541,6 @@
|
||||
"Activate to toggle voice": "Activer/désactiver la commande vocale",
|
||||
"Voice active": "Voix active",
|
||||
"Voice could not be started.": "Impossible de démarrer la voix.",
|
||||
"the active voice session": "la session vocale active",
|
||||
"{value} feet": "{value} pieds",
|
||||
"Fire · FRP {frp} MW": "Feu · FRP {frp} MW",
|
||||
"The flights layer is not enabled": "La couche des vols n'est pas activée",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"That line has no length — click somewhere further away.": "Quella linea non ha lunghezza: fai clic più lontano.",
|
||||
" · {max}-point limit reached": " · raggiunto il limite di {max} punti",
|
||||
"{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · doppio clic o Enter per finire, Backspace torna indietro, Esc annulla.{full}",
|
||||
"drawings on the map": "disegni sulla mappa",
|
||||
"That shape already has {count} points — finish it or press Backspace.": "Quella forma ha già {count} punti: finiscila o premi Backspace.",
|
||||
"That point is off the globe — click on the world.": "Quel punto è fuori dal globo: fai clic sul mondo.",
|
||||
"That shape could not be placed.": "Impossibile posizionare quella forma.",
|
||||
@@ -402,7 +401,6 @@
|
||||
"paste {envVar}": "incolla {envVar}",
|
||||
"REMOVE": "RIMUOVI",
|
||||
"Remove {title} from this app's saved keys": "Rimuovi {title} dalle chiavi salvate di questa app",
|
||||
"unsaved provider keys": "chiavi provider non salvate",
|
||||
"Project keys: {status}": "Chiavi del progetto: {status}",
|
||||
"your app configuration": "la configurazione della tua app",
|
||||
"your local .env": "il tuo .env locale",
|
||||
@@ -1540,9 +1538,6 @@
|
||||
"Sensitivity": "Sensibilità",
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"unsaved CCTV calibration": "calibrazione CCTV non salvata",
|
||||
"the open cockpit": "il cockpit aperto",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Cambiare lingua ricarica la pagina. Perderai: {items}. Cambiare comunque?",
|
||||
"VISUAL PRESETS": "PRESET VISIVI",
|
||||
"LOCATION": "POSIZIONE",
|
||||
"CONTACTS — nearest planes · vessels · sites": "CONTATTI — aerei · navi · siti più vicini",
|
||||
@@ -1767,7 +1762,6 @@
|
||||
"Space Missions": "Missioni spaziali",
|
||||
"Context": "Contesto",
|
||||
"Rendered globe frames per second · toggle with `": "Fotogrammi al secondo del globo · attiva/disattiva con `",
|
||||
"imported map files": "file di mappa importati",
|
||||
"Ground heights could not be refreshed. Try the file again when the map has loaded.": "Impossibile aggiornare le quote del terreno. Riprova con il file quando la mappa è caricata.",
|
||||
"{count} map objects": {
|
||||
"one": "{count} oggetto sulla mappa",
|
||||
@@ -2547,7 +2541,6 @@
|
||||
"Activate to toggle voice": "Attiva per abilitare o disabilitare la voce",
|
||||
"Voice active": "Voce attiva",
|
||||
"Voice could not be started.": "Impossibile avviare la voce.",
|
||||
"the active voice session": "la sessione vocale attiva",
|
||||
"{value} feet": "{value} piedi",
|
||||
"Fire · FRP {frp} MW": "Incendio · FRP {frp} MW",
|
||||
"The flights layer is not enabled": "Il livello voli non è attivo",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"That line has no length — click somewhere further away.": "線の長さがゼロです。もっと離れた場所をクリックしてください。",
|
||||
" · {max}-point limit reached": " · 上限の{max}点に達しました",
|
||||
"{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · ダブルクリックまたはEnterで完了、Backspaceで元に戻す、Escでキャンセル。{full}",
|
||||
"drawings on the map": "地図上の描画",
|
||||
"That shape already has {count} points — finish it or press Backspace.": "この図形はすでに{count}点あります。完了するか、Backspaceを押してください。",
|
||||
"That point is off the globe — click on the world.": "その点は地球の外です。地表をクリックしてください。",
|
||||
"That shape could not be placed.": "図形を配置できませんでした。",
|
||||
@@ -394,7 +393,6 @@
|
||||
"paste {envVar}": "{envVar}を貼り付け",
|
||||
"REMOVE": "削除",
|
||||
"Remove {title} from this app's saved keys": "{title}をこのアプリの保存済みキーから削除",
|
||||
"unsaved provider keys": "未保存のプロバイダーキー",
|
||||
"Project keys: {status}": "プロジェクトのキー:{status}",
|
||||
"your app configuration": "アプリの設定",
|
||||
"your local .env": "ローカルの .env",
|
||||
@@ -1498,9 +1496,6 @@
|
||||
"Sensitivity": "感度",
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"unsaved CCTV calibration": "未保存のCCTVキャリブレーション",
|
||||
"the open cockpit": "表示中のコックピット",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "言語を切り替えるとページが再読み込みされます。次の内容は失われます:{items}。切り替えますか?",
|
||||
"VISUAL PRESETS": "ビジュアルプリセット",
|
||||
"LOCATION": "位置",
|
||||
"CONTACTS — nearest planes · vessels · sites": "コンタクト:最寄りの航空機 · 船舶 · 施設",
|
||||
@@ -1711,7 +1706,6 @@
|
||||
"Space Missions": "宇宙ミッション",
|
||||
"Context": "コンテキスト",
|
||||
"Rendered globe frames per second · toggle with `": "地球儀の描画フレームレート(毎秒) · `キーで切り替え",
|
||||
"imported map files": "インポートした地図ファイル",
|
||||
"Ground heights could not be refreshed. Try the file again when the map has loaded.": "地表高を更新できませんでした。地図の読み込み後に、もう一度このファイルをお試しください。",
|
||||
"{count} map objects": {
|
||||
"other": "地図オブジェクト{count}個"
|
||||
@@ -2417,7 +2411,6 @@
|
||||
"Activate to toggle voice": "押すと音声のオン/オフを切り替え",
|
||||
"Voice active": "音声 オン",
|
||||
"Voice could not be started.": "音声を開始できませんでした。",
|
||||
"the active voice session": "進行中の音声セッション",
|
||||
"{value} feet": "{value}フィート",
|
||||
"Fire · FRP {frp} MW": "火災 · FRP {frp} MW",
|
||||
"The flights layer is not enabled": "フライトレイヤーが有効になっていません",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"That line has no length — click somewhere further away.": "Essa linha não tem comprimento — clique em um ponto mais distante.",
|
||||
" · {max}-point limit reached": " · limite de {max} pontos atingido",
|
||||
"{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · clique duplo ou Enter para concluir, Backspace desfaz, Esc cancela.{full}",
|
||||
"drawings on the map": "os desenhos no mapa",
|
||||
"That shape already has {count} points — finish it or press Backspace.": "Essa forma já tem {count} pontos — conclua-a ou pressione Backspace.",
|
||||
"That point is off the globe — click on the world.": "Esse ponto está fora do globo — clique sobre o mundo.",
|
||||
"That shape could not be placed.": "Não foi possível colocar essa forma.",
|
||||
@@ -402,7 +401,6 @@
|
||||
"paste {envVar}": "cole {envVar}",
|
||||
"REMOVE": "REMOVER",
|
||||
"Remove {title} from this app's saved keys": "Remover {title} das chaves salvas deste app",
|
||||
"unsaved provider keys": "chaves de provedores não salvas",
|
||||
"Project keys: {status}": "Chaves do projeto: {status}",
|
||||
"your app configuration": "sua configuração do app",
|
||||
"your local .env": "seu .env local",
|
||||
@@ -1540,9 +1538,6 @@
|
||||
"Sensitivity": "Sensibilidade",
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"unsaved CCTV calibration": "a calibração de CCTV não salva",
|
||||
"the open cockpit": "a cabine aberta",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Mudar de idioma recarrega a página. Isto será perdido: {items}. Mudar mesmo assim?",
|
||||
"VISUAL PRESETS": "PREDEF. VISUAIS",
|
||||
"LOCATION": "LOCAL",
|
||||
"CONTACTS — nearest planes · vessels · sites": "CONTATOS — aviões · embarcações · locais mais próximos",
|
||||
@@ -1767,7 +1762,6 @@
|
||||
"Space Missions": "Missões espaciais",
|
||||
"Context": "Contexto",
|
||||
"Rendered globe frames per second · toggle with `": "Quadros por segundo renderizados do globo · alterne com `",
|
||||
"imported map files": "os arquivos importados no mapa",
|
||||
"Ground heights could not be refreshed. Try the file again when the map has loaded.": "Não foi possível atualizar as altitudes do terreno. Tente o arquivo de novo quando o mapa tiver carregado.",
|
||||
"{count} map objects": {
|
||||
"one": "{count} objeto no mapa",
|
||||
@@ -2549,7 +2543,6 @@
|
||||
"Activate to toggle voice": "Ative para ligar ou desligar a voz",
|
||||
"Voice active": "Voz ativa",
|
||||
"Voice could not be started.": "Não foi possível iniciar a voz.",
|
||||
"the active voice session": "a sessão de voz ativa",
|
||||
"{value} feet": "{value} pés",
|
||||
"Fire · FRP {frp} MW": "Incêndio · FRP {frp} MW",
|
||||
"The flights layer is not enabled": "A camada de voos não está ativada",
|
||||
|
||||
@@ -2508,9 +2508,6 @@
|
||||
"ChatGPT sign-in timed out. Click {button} to try again.": "Время на вход в ChatGPT истекло. Нажмите «{button}», чтобы попробовать ещё раз.",
|
||||
"ChatGPT sign-in complete. OAuth will be used for the next cloud voice session.": "Вход в ChatGPT выполнен. В следующей облачной голосовой сессии будет использоваться OAuth.",
|
||||
"OAuth check failed: {detail}": "Не удалось проверить OAuth: {detail}",
|
||||
"unsaved CCTV calibration": "несохранённая калибровка CCTV",
|
||||
"the open cockpit": "открытая кабина",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Смена языка перезагрузит страницу. Будет потеряно: {items}. Всё равно сменить язык?",
|
||||
"Wind upstream unavailable": "Источник данных о ветре недоступен",
|
||||
"WebUSB needs desktop Chrome or Edge": "Для WebUSB нужен настольный Chrome или Edge",
|
||||
"connect a receiver in Radio": "подключите приёмник в разделе «Радио»",
|
||||
@@ -2712,9 +2709,6 @@
|
||||
"KartaView answered HTTP {status}": "KartaView ответил: HTTP {status}",
|
||||
"KartaView refused the request (code {code})": "KartaView отклонил запрос (код {code})",
|
||||
"Stopped by voice": "Остановлено голосовой командой",
|
||||
"drawings on the map": "рисунки на карте",
|
||||
"imported map files": "импортированные файлы на карте",
|
||||
"the active voice session": "активная голосовая сессия",
|
||||
"Light aircraft": "Лёгкий самолёт",
|
||||
"Glider": "Планёр",
|
||||
"Turboprop": "Турбовинтовой самолёт",
|
||||
@@ -2925,7 +2919,6 @@
|
||||
"many": "потоки {names} некорректны",
|
||||
"other": "потоки {names} некорректны"
|
||||
},
|
||||
"unsaved provider keys": "несохранённые ключи провайдеров",
|
||||
"Exit cockpit to fly to an aircraft": "Выйдите из кабины, чтобы перелететь к самолёту",
|
||||
"Exit cockpit to fly to a satellite": "Выйдите из кабины, чтобы перелететь к спутнику",
|
||||
"Exit cockpit to frame contacts": "Выйдите из кабины, чтобы показать контакты в кадре",
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"That line has no length — click somewhere further away.": "Лінія не має довжини — клацніть далі.",
|
||||
" · {max}-point limit reached": " · досягнуто ліміту точок: {max}",
|
||||
"{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · подвійне клацання або Enter — завершити, Backspace — скасувати точку, Esc — скасувати.{full}",
|
||||
"drawings on the map": "малюнки на карті",
|
||||
"That shape already has {count} points — finish it or press Backspace.": "Фігура вже має максимальну кількість точок ({count}) — завершіть її або натисніть Backspace.",
|
||||
"That point is off the globe — click on the world.": "Ця точка поза глобусом — клацніть на Землі.",
|
||||
"That shape could not be placed.": "Не вдалося розмістити фігуру.",
|
||||
@@ -406,7 +405,6 @@
|
||||
"paste {envVar}": "вставте {envVar}",
|
||||
"REMOVE": "ВИДАЛИТИ",
|
||||
"Remove {title} from this app's saved keys": "Видалити {title} зі збережених ключів застосунку",
|
||||
"unsaved provider keys": "незбережені ключі постачальників",
|
||||
"Project keys: {status}": "Ключі проєкту: {status}",
|
||||
"your app configuration": "вашої конфігурації застосунку",
|
||||
"your local .env": "вашого локального .env",
|
||||
@@ -1560,9 +1558,6 @@
|
||||
"Sensitivity": "Чутливість",
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"unsaved CCTV calibration": "незбережене калібрування CCTV",
|
||||
"the open cockpit": "відкрита кабіна",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "Зміна мови перезавантажить сторінку. Буде втрачено: {items}. Усе одно змінити мову?",
|
||||
"VISUAL PRESETS": "ВІЗУАЛЬНІ ПРЕСЕТИ",
|
||||
"LOCATION": "РОЗТАШУВАННЯ",
|
||||
"CONTACTS — nearest planes · vessels · sites": "КОНТАКТИ — найближчі літаки · судна · обʼєкти",
|
||||
@@ -1794,7 +1789,6 @@
|
||||
"Space Missions": "Космічні місії",
|
||||
"Context": "Контекст",
|
||||
"Rendered globe frames per second · toggle with `": "Кадрів глобуса за секунду · перемикання клавішею `",
|
||||
"imported map files": "імпортовані файли на карті",
|
||||
"Ground heights could not be refreshed. Try the file again when the map has loaded.": "Не вдалося оновити висоти рельєфу. Спробуйте імпортувати файл ще раз, коли карта завантажиться.",
|
||||
"{count} map objects": {
|
||||
"one": "{count} обʼєкт на карті",
|
||||
@@ -2611,7 +2605,6 @@
|
||||
"Activate to toggle voice": "Активуйте, щоб увімкнути або вимкнути голос",
|
||||
"Voice active": "Голос активний",
|
||||
"Voice could not be started.": "Не вдалося запустити голос.",
|
||||
"the active voice session": "активний голосовий сеанс",
|
||||
"{value} feet": "{value} фт",
|
||||
"Fire · FRP {frp} MW": "Пожежа · FRP {frp} МВт",
|
||||
"The flights layer is not enabled": "Шар рейсів не ввімкнено",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"That line has no length — click somewhere further away.": "这条线没有长度——请在更远处点击。",
|
||||
" · {max}-point limit reached": " · 已达 {max} 个点上限",
|
||||
"{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · 双击或按 Enter 完成,按 Backspace 撤销,按 Esc 取消。{full}",
|
||||
"drawings on the map": "地图上的绘图",
|
||||
"That shape already has {count} points — finish it or press Backspace.": "该形状已有 {count} 个点——请完成绘制或按 Backspace。",
|
||||
"That point is off the globe — click on the world.": "该点在地球之外——请点击地球上的位置。",
|
||||
"That shape could not be placed.": "该形状无法放置。",
|
||||
@@ -394,7 +393,6 @@
|
||||
"paste {envVar}": "粘贴 {envVar}",
|
||||
"REMOVE": "移除",
|
||||
"Remove {title} from this app's saved keys": "从本应用已保存的密钥中移除 {title}",
|
||||
"unsaved provider keys": "未保存的服务商密钥",
|
||||
"Project keys: {status}": "项目密钥:{status}",
|
||||
"your app configuration": "你的应用配置",
|
||||
"your local .env": "你的本地 .env",
|
||||
@@ -1470,9 +1468,6 @@
|
||||
"Sensitivity": "灵敏度",
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"unsaved CCTV calibration": "未保存的 CCTV 校准",
|
||||
"the open cockpit": "当前打开的座舱",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "切换语言会重新加载页面。以下内容将会丢失:{items}。仍要切换吗?",
|
||||
"VISUAL PRESETS": "视觉预设",
|
||||
"LOCATION": "位置",
|
||||
"CONTACTS — nearest planes · vessels · sites": "目标:最近的飞机 · 船舶 · 设施",
|
||||
@@ -1686,7 +1681,6 @@
|
||||
"Space Missions": "太空任务",
|
||||
"Context": "态势",
|
||||
"Rendered globe frames per second · toggle with `": "每秒渲染的地球帧数 · 按 ` 键切换",
|
||||
"imported map files": "已导入的地图文件",
|
||||
"Ground heights could not be refreshed. Try the file again when the map has loaded.": "无法刷新地面高度。请在地图加载完成后重新尝试该文件。",
|
||||
"{count} map objects": {
|
||||
"other": "{count} 个地图对象"
|
||||
@@ -2315,7 +2309,6 @@
|
||||
"Activate to toggle voice": "点击切换语音",
|
||||
"Voice active": "语音已启用",
|
||||
"Voice could not be started.": "无法启动语音。",
|
||||
"the active voice session": "当前语音会话",
|
||||
"{value} feet": "{value} 英尺",
|
||||
"The flights layer is not enabled": "航班图层未开启",
|
||||
"The military layer is not enabled": "军用航班图层未开启",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"That line has no length — click somewhere further away.": "這條線沒有長度——請在更遠處點擊。",
|
||||
" · {max}-point limit reached": " · 已達 {max} 個點上限",
|
||||
"{measure} · double-click or Enter to finish, Backspace undoes, Esc cancels.{full}": "{measure} · 雙擊或按 Enter 完成,按 Backspace 撤銷,按 Esc 取消。{full}",
|
||||
"drawings on the map": "地圖上的繪圖",
|
||||
"That shape already has {count} points — finish it or press Backspace.": "該形狀已有 {count} 個點——請完成繪製或按 Backspace。",
|
||||
"That point is off the globe — click on the world.": "該點在地球之外——請點擊地球上的位置。",
|
||||
"That shape could not be placed.": "該形狀無法放置。",
|
||||
@@ -394,7 +393,6 @@
|
||||
"paste {envVar}": "粘貼 {envVar}",
|
||||
"REMOVE": "移除",
|
||||
"Remove {title} from this app's saved keys": "從本應用已保存的密鑰中移除 {title}",
|
||||
"unsaved provider keys": "未保存的服務商密鑰",
|
||||
"Project keys: {status}": "項目密鑰:{status}",
|
||||
"your app configuration": "你的應用配置",
|
||||
"your local .env": "你的本地 .env",
|
||||
@@ -1465,9 +1463,6 @@
|
||||
"Sensitivity": "靈敏度",
|
||||
"WHOT/BHOT": "WHOT/BHOT",
|
||||
"Ironbow": "Ironbow",
|
||||
"unsaved CCTV calibration": "未保存的 CCTV 校準",
|
||||
"the open cockpit": "當前打開的座艙",
|
||||
"Switching language reloads the page. This will be lost: {items}. Switch anyway?": "切換語言會重新加載頁面。以下內容將會丟失:{items}。仍要切換嗎?",
|
||||
"VISUAL PRESETS": "視覺預設",
|
||||
"LOCATION": "位置",
|
||||
"CONTACTS — nearest planes · vessels · sites": "目標:最近的飛機 · 船舶 · 設施",
|
||||
@@ -1676,7 +1671,6 @@
|
||||
"Space Missions": "太空任務",
|
||||
"Context": "態勢",
|
||||
"Rendered globe frames per second · toggle with `": "每秒渲染的地球幀數 · 按 ` 鍵切換",
|
||||
"imported map files": "已導入的地圖文件",
|
||||
"Ground heights could not be refreshed. Try the file again when the map has loaded.": "無法刷新地面高度。請在地圖加載完成後重新嘗試該文件。",
|
||||
"{count} map objects": {
|
||||
"other": "{count} 個地圖對象"
|
||||
@@ -2303,7 +2297,6 @@
|
||||
"Activate to toggle voice": "點擊切換語音",
|
||||
"Voice active": "語音已啟用",
|
||||
"Voice could not be started.": "無法啟動語音。",
|
||||
"the active voice session": "當前語音會話",
|
||||
"{value} feet": "{value} 英尺",
|
||||
"The flights layer is not enabled": "航班圖層未開啟",
|
||||
"The military layer is not enabled": "軍用航班圖層未開啟",
|
||||
|
||||
Reference in new issue
Block a user