From 858ce827e50a7e9659e35a1dffc7c1a964ceebaf Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 28 Oct 2026 17:57:51 +0400 Subject: [PATCH] i18n(pt): apply review round 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-model review (astra, sol, fable), round 2: 29 of 30 findings applied, one of them in part. - pt: FIRMS «mais recente há», CCTV «cruzamentos», degraded radio directory, «{layers}: ativação recente» for any layer list, «Sem imagem», NIVELADO, AJUSTA-SE, quoted Provider Settings, V/S kept on the ADS-B card, no-break space in «(24 h)» / «(30 d)», and more (see docs/translations/pt.md). - Malformed cyclone snapshot is now a catalog message (all locales). - Markup supports data-i18n-context; the radio STATION TAG option uses "radio tag|All", so it no longer flips gender when the list loads. Co-Authored-By: Claude Opus 5.5 --- docs/I18N.md | 2 +- docs/translations/pt.md | 14 +++++--- scripts/i18n.mjs | 7 ++-- src/i18n/i18n.test.mjs | 15 ++++++++ src/i18n/index.js | 9 +++-- src/i18n/locales/es.json | 1 + src/i18n/locales/pt.json | 65 ++++++++++++++++++----------------- src/i18n/locales/ru.json | 1 + src/i18n/locales/zh-CN.json | 1 + src/i18n/locales/zh-Hant.json | 1 + src/i18n/same-as-english.json | 1 + src/layers/cyclones/source.js | 6 ++-- src/ui/templates/context.html | 2 +- 13 files changed, 79 insertions(+), 46 deletions(-) diff --git a/docs/I18N.md b/docs/I18N.md index 9b5220917..105fd5fb3 100644 --- a/docs/I18N.md +++ b/docs/I18N.md @@ -35,7 +35,7 @@ A module the server, tools or other portable code also imports must not load the ``` -`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. Markup is translated once at boot, and switching language reloads the page. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading. +`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. `data-i18n-context="radio tag"` gives the element's text a context, the catalog entry `radio tag|All`, as `t('All', { context: 'radio tag' })` does in code. Markup is translated once at boot, and switching language reloads the page. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading. ## Rules diff --git a/docs/translations/pt.md b/docs/translations/pt.md index bf1208dce..15d6291b5 100644 --- a/docs/translations/pt.md +++ b/docs/translations/pt.md @@ -9,15 +9,15 @@ Translation source: abiyeenzo (#909). - Brazilian Portuguese (pt-BR): câmera, tela, compartilhar, arquivo, baixar, ônibus, metrô. Every `pt` tag, `pt-PT` included, gets this catalog, so avoid slang and regionalisms. - Address the user with «você» imperatives: «Clique no mapa», «Ative o CCTV», «Tente novamente». Tooltips that name an action use the infinitive: «Expandir painel», «Redefinir a câmera». - Sentence case, not Title Case: "Live Flights" → «Voos ao vivo». UPPERCASE English stays UPPERCASE: «AO VIVO», «SEM DADOS». -- Quotes inside Portuguese text are straight double quotes: «Excluir o plano "{shot}"?». A sentence that names a panel uses «o painel "Configurações de provedores"», so the verb agrees. +- Quotes inside Portuguese text are straight double quotes: «Excluir o plano "{shot}"?». A sentence that names a panel quotes it: «o painel "Configurações de provedores"» as the subject, so the verb agrees, and «adicione em "Configurações de provedores"» after a preposition. - A dash between clauses is a spaced em dash « — », even where the English runs it in. -- A date or time after a participle takes a preposition: «emitido em {time}», «válida para {valid}», «Posição em {time}». +- A date or time after a participle takes a preposition: «emitido em {time}», «válida para {valid}», «Posição em {time}», «onde estava em {time}». Use «em», not «às», wherever {time} can include a date. - Short labels, chips and HUD readouts stay short. A standard abbreviation with a period beats a wrapped line: ANT., PRÓX., DESLIG., EXCL., INCL., ALT., VEL., câm. -- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD, RANGE, ALT, GS and TRK stay in English, as in cockpit avionics («ALT {altitude} FT · GS {speed} KT · TRK {track}»), and so does the HUD recording mark REC. Words used in a sentence are translated: heading → «rumo», range → «alcance». +- The HUD codes HDG, BRG, CRS, FOV, DEST, SPD, RANGE, ALT, GS, TRK and V/S stay in English, as in cockpit avionics («ALT {altitude} FT · GS {speed} KT · TRK {track}», «V/S {rate} FT/MIN»), and so does the HUD recording mark REC. Words used in a sentence are translated: heading → «rumo», range → «alcance». - Keyboard keys stay in English: Space, Esc, Enter, Tab, Shift, Backspace, H, 1–9 («Segure Space para falar»). - Common English tech words used in Brazil stay: feed, stream, snapshot, briefing, dock, layout, bloom, status, app, link, token. -- Units: km, m, km/h, m/s, kt (knots), ft (feet), h, min, s, always spaced from the number («Terremotos (24 h)», «Missões espaciais (30 d)»); in UPPERCASE text KM, KM/S, KT, FT, MIN, H. "m" is always metres, so minutes are «min» («há 5 min»). MW stays MW. `°` and `%` stay. Decimal comma in written numbers («M4,4»). -- "ago" goes before the time: «há {count} min». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «há» again. +- Units: km, m, km/h, m/s, kt (knots), ft (feet), h, min, s, always spaced from the number («Terremotos (24 h)», «Missões espaciais (30 d)»; in the layer names the space is a no-break space, U+00A0, so the unit never wraps away from the number); in UPPERCASE text KM, KM/S, KT, FT, MIN, H. "m" is always metres, so minutes are «min» («há 5 min»). MW stays MW. `°` and `%` stay. Decimal comma in written numbers («M4,4»). +- "ago" goes before the time: «há {count} min», «mais recente há {value}». When a placeholder already holds a localized "ago" text (`{age}`, `{ago}`), do not add «há» again. - Money: «US$ {amount} mil / mi / bi» for thousands, millions and billions. - Compass points use Leste/Oeste: N, NNE, NE, ENE, L, ESE, SE, SSE, S, SSO, SO, OSO, O, ONO, NO, NNO; hemisphere letters N, S, L, O. @@ -84,6 +84,7 @@ The `many` form is the `other` text with «de» after the number: «{count} de c | Cyber (HUD layout, effect) | Ciber | | heading / course / altitude / ground speed | rumo / curso / altitude / velocidade solo (VELOC. SOLO) | | HDG / BRG / CRS / FOV / DEST / SPD / RANGE | HDG / BRG / CRS / FOV / DEST / SPD / RANGE (unchanged) | +| V/S (vertical speed code) | V/S (unchanged) | | PREV / NEXT | ANT. / PRÓX. | | Previous / Next (button text) | Anterior / Próximo (Próxima) | | L / R (relative bearing) | E / D | @@ -91,4 +92,7 @@ The `many` form is the `other` text with «de» after the number: «{count} de c | REGIONAL NEWS (fits the 16-character kicker) | NOTÍCIAS DA ÁREA | | PITCH / HGT (CCTV calibration chips) | INCL. / ALT. | | NOT AN ALL-CLEAR | NÃO GARANTE SEGURANÇA | +| Last report (transit vehicle position) | Último registro | +| intersection (CCTV) | cruzamento | +| {layers} just turned on (any list of layers) | «{layers}: ativação recente» (no agreement needed) | | turn-by-turn: turn left / keep left / lane | vire à esquerda / mantenha-se à esquerda / faixa da esquerda | diff --git a/scripts/i18n.mjs b/scripts/i18n.mjs index 0ee5d60ec..c035c6bb2 100644 --- a/scripts/i18n.mjs +++ b/scripts/i18n.mjs @@ -125,8 +125,11 @@ export function extractMessages() { const html = readFileSync(path.join(ROOT, file), 'utf8'); for (const [, tag, attrs, text] of html.matchAll(TAG)) { const where = `${file} <${tag}>`; - if (/\sdata-i18n(\s|=|$)/.test(attrs)) - add(normalize(decodeHtml(text)), where); + if (/\sdata-i18n(\s|=|$)/.test(attrs)) { + const context = attrs.match(/\sdata-i18n-context="([^"]*)"/); + const message = normalize(decodeHtml(text)); + add(context ? `${decodeHtml(context[1])}|${message}` : message, where); + } for (const [, name] of attrs.matchAll( /\sdata-i18n-(title|aria-label|placeholder)\b/g, )) { diff --git a/src/i18n/i18n.test.mjs b/src/i18n/i18n.test.mjs index df6b4d000..8f9d50158 100644 --- a/src/i18n/i18n.test.mjs +++ b/src/i18n/i18n.test.mjs @@ -216,6 +216,21 @@ test('applyDocumentTranslations translates opted-in text and attributes in place assert.equal(unknown.textContent, 'No such message'); }); +test('applyDocumentTranslations reads data-i18n-context for element text', () => { + const option = fakeElement({ + text: 'All', + attributes: { 'data-i18n-context': 'radio tag' }, + }); + const doc = { + querySelectorAll: (selector) => + selector === '[data-i18n]' ? [option] : [], + }; + setLocale('pt'); + applyDocumentTranslations(doc); + assert.equal(option.textContent, t('All', { context: 'radio tag' })); + assert.notEqual(option.textContent, t('All')); +}); + test('persistLocaleAndReload keeps the choice in ?lang when storage is blocked', () => { const calls = []; const location = { diff --git a/src/i18n/index.js b/src/i18n/index.js index a6076bbd5..7d17afa03 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -228,7 +228,9 @@ function normalizeMarkupText(text) { * Markup opt-in: the element's own English text or attribute is the source. * Normal * - * Also data-i18n-aria-label and data-i18n-placeholder. + * Also data-i18n-aria-label and data-i18n-placeholder. data-i18n-context + * gives the element's text a context, as t(text, { context }) does: + * */ const DOCUMENT_TRANSLATION_TARGETS = Object.freeze([ ['data-i18n', null], @@ -253,7 +255,10 @@ export function applyDocumentTranslations(doc) { ? element.getAttribute(attribute) : normalizeMarkupText(element.textContent); if (!source) continue; - const text = t(source); + const context = attribute + ? null + : element.getAttribute?.('data-i18n-context'); + const text = context ? t(source, { context }) : t(source); if (text === source) continue; if (attribute) element.setAttribute(attribute, text); else element.textContent = text; diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 3db1ff129..c322665fa 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -2227,6 +2227,7 @@ "The Google Maps JavaScript API could not load": "No se pudo cargar la API de Maps JavaScript de Google", "Weather request timed out": "La petición del clima agotó el tiempo de espera", "Cyclone request timed out": "La petición de ciclones agotó el tiempo de espera", + "Malformed cyclone snapshot": "Instantánea de ciclones mal formada", "Malformed weather manifest": "Manifiesto del clima mal formado", "Weather HTTP {status}": "Clima: HTTP {status}", "Weather image HTTP {status}": "Imagen del clima: HTTP {status}", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 9f2bbe765..fee5d1274 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -300,12 +300,12 @@ "Source configured; file checked when loaded": "Fonte configurada; o arquivo será verificado ao carregar", "Source unavailable": "Fonte indisponível", "ENVIRONMENTAL": "AMBIENTAL", - "EARTH WATCH": "VIGIA DA TERRA", + "EARTH WATCH": "VIGILÂNCIA DA TERRA", "ACTIVE EVENTS": "EVENTOS ATIVOS", "Starting live contacts…": "Iniciando contatos ao vivo…", "Opening space missions…": "Abrindo missões espaciais…", "Scanning active events…": "Varrendo eventos ativos…", - "Live earthquakes and active fires, from USGS and NASA": "Terremotos e incêndios ativos ao vivo, de USGS e NASA", + "Live earthquakes and active fires, from USGS and NASA": "Terremotos e incêndios ativos ao vivo, do USGS e da NASA", "It feels like a forbidden cockpit—then you realize the sources are public and the data is real.": "Parece uma cabine proibida — até você perceber que as fontes são públicas e os dados são reais.", "Working…": "Processando…", "Could not open that mission{detail}. Retry or explore manually.": "Não foi possível abrir essa missão{detail}. Tente novamente ou explore manualmente.", @@ -376,7 +376,7 @@ "{name} is longer than any real key ({limit} max)": "{name}: o valor é mais longo que qualquer chave real (máximo {limit})", "{name} may only contain printable characters with no spaces": "{name}: só são aceitos caracteres imprimíveis, sem espaços", "{name} contains a character that is not valid in a key (#, quotes, $, \\, or backtick)": "{name}: há um caractere inválido para uma chave (#, aspas, $, \\ ou crase)", - "{name} is configured outside Provider Settings and can only be changed where it was set": "{name} foi configurada fora de Configurações de provedores e só pode ser alterada onde foi definida", + "{name} is configured outside Provider Settings and can only be changed where it was set": "{name} foi configurada fora de \"Configurações de provedores\" e só pode ser alterada onde foi definida", "The key was not saved: could not restrict the credential file to your account; nothing was saved": "A chave não foi salva: não foi possível restringir o arquivo de credenciais à sua conta; nada foi salvo", "The key was not saved: the existing configuration could not be read, so nothing was changed": "A chave não foi salva: não foi possível ler a configuração existente, então nada foi alterado", "Could not write the {store} store": "Não foi possível gravar no armazenamento {store}", @@ -430,7 +430,7 @@ "Live active-fire detections": "Detecções de incêndios ativos ao vivo", "Real live traffic (keyless runs a simulation)": "Trânsito real ao vivo (sem chave, roda uma simulação)", "Bing imagery map stacks + world terrain": "Pilhas de mapas com imagens do Bing + relevo mundial", - "More flight-polling credits (anonymous works without)": "Mais créditos de consulta de voos (o acesso anônimo funciona sem)", + "More flight-polling credits (anonymous works without)": "Mais créditos de consulta de voos (opcional: o acesso anônimo também funciona)", "Street-level imagery and coverage in the Street Level layer. Free: register an app in the Mapillary developer dashboard and paste its Client Token": "Imagens e cobertura no nível da rua na camada \"Nível da rua\". Grátis: registre um app no painel de desenvolvedores do Mapillary e cole o token de cliente (Client Token)", "Public space-missions access needs no key. Leave blank for standard use; paid tokens only raise request limits.": "O acesso público às missões espaciais não precisa de chave. Deixe em branco para o uso padrão; tokens pagos só aumentam os limites de requisições.", "Air quality request failed": "Falha na requisição de qualidade do ar", @@ -716,7 +716,7 @@ "FIRE": "INCÊNDIO", "FIRES": "INCÊNDIOS", "max {value} MW": "máx {value} MW", - "new {value}": "novo {value}", + "new {value}": "mais recente há {value}", "{value}°N": "{value}°N", "{value}°S": "{value}°S", "{value}°E": "{value}°L", @@ -822,7 +822,7 @@ "FLIGHT {count}": "VOO {count}", "REUSED": "REUTILIZADO", "NO STAGE RE-ENTRY / RECOVERY DATA": "SEM DADOS DE REENTRADA / RECUPERAÇÃO DE ESTÁGIO", - "{count} / 30D": "{count} / 30D", + "{count} / 30D": "{count} / 30 D", "NO MISSIONS AVAILABLE IN THE CURRENT 30-DAY WINDOW": "NENHUMA MISSÃO DISPONÍVEL NA JANELA ATUAL DE 30 DIAS", "DATE UNAVAILABLE": "DATA INDISPONÍVEL", "UNSPECIFIED OPERATOR": "OPERADOR NÃO ESPECIFICADO", @@ -881,8 +881,8 @@ "Your RTL-SDR receiver": "Seu receptor RTL-SDR", "NO CALLSIGN": "SEM INDICATIVO", "ALT {altitude} FT · GS {speed} KT · TRK {track}": "ALT {altitude} FT · GS {speed} KT · TRK {track}", - "V/S {rate}": "VEL. VERT. {rate}", - "V/S {rate} FPM": "VEL. VERT. {rate} FT/MIN", + "V/S {rate}": "V/S {rate}", + "V/S {rate} FPM": "V/S {rate} FT/MIN", "POSITION {age}": "POSIÇÃO {age}", "POSITION {seconds} S AGO": "POSIÇÃO HÁ {seconds} S", "{count} MSGS": { @@ -954,8 +954,8 @@ "Traffic / Transit": "Trânsito / Transporte", "Music": "Música", "Other": "Outras", - "Directory refresh degraded; showing the previous station catalog.": "A atualização do diretório foi parcial; mostrando o catálogo de estações anterior.", - "Radio directory coverage is degraded.": "O diretório de rádio está funcionando com falhas.", + "Directory refresh degraded; showing the previous station catalog.": "A atualização do diretório retornou dados degradados; mostrando o catálogo de estações anterior.", + "Radio directory coverage is degraded.": "A cobertura do diretório de rádio está degradada.", "Directory refresh failed; showing the previous station catalog.": "A atualização do diretório falhou; mostrando o catálogo de estações anterior.", "Radio directory is temporarily unavailable.": "O diretório de rádio está temporariamente indisponível.", "radio|NEWS": "NOTÍCIAS", @@ -1069,7 +1069,7 @@ "KartaView sent too much data": "O KartaView enviou dados demais", "KartaView is rate-limiting requests — try again in a moment": "O KartaView está limitando as requisições — tente novamente em instantes", "KartaView has no such photo": "O KartaView não tem essa foto", - "Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings": "O Mapillary recusou o MAPILLARY_CLIENT_TOKEN — substitua-o em Configurações de provedores", + "Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings": "O Mapillary recusou o MAPILLARY_CLIENT_TOKEN — substitua-o em \"Configurações de provedores\"", "Coverage requests are being rate-limited — they resume on their own": "As requisições de cobertura estão sendo limitadas — elas são retomadas sozinhas", "The Panoramax photo could not be loaded": "Não foi possível carregar a foto do Panoramax", "Panoramax is busy — coverage resumes on its own": "O Panoramax está sobrecarregado — a cobertura volta sozinha", @@ -1120,7 +1120,7 @@ "LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov": "AO VIVO · Vias: OpenStreetMap · Fluxo: TomTom · {percent}% cob", "SIMULATED · Roads: OpenStreetMap · Flow: TomTom (no matches)": "SIMULADO · Vias: OpenStreetMap · Fluxo: TomTom (sem correspondências)", "SIMULATED — traffic service unreachable": "SIMULADO — serviço de trânsito inacessível", - "SIMULATED — add TomTom key for live": "SIMULADO — adicione a chave TomTom para dados ao vivo", + "SIMULATED — add TomTom key for live": "SIMULADO — adicione a chave do TomTom para dados ao vivo", "OpenFreeMap tiles rate-limited": "Blocos do OpenFreeMap com limite de requisições", "OpenFreeMap tiles timed out": "Os blocos do OpenFreeMap excederam o tempo limite", "OpenFreeMap tiles unavailable": "Blocos do OpenFreeMap indisponíveis", @@ -1140,9 +1140,9 @@ "Moving": "Em movimento", "reported {speed} km/h": "velocidade informada: {speed} km/h", "reported hdg {heading}°": "rumo informado: {heading}°", - "Last report: stopped at stop {stop}": "Último relato: parado no ponto {stop}", - "Last report: arriving at stop {stop}": "Último relato: chegando ao ponto {stop}", - "Last report: next stop {stop}": "Último relato: próximo ponto {stop}", + "Last report: stopped at stop {stop}": "Último registro: parado no ponto {stop}", + "Last report: arriving at stop {stop}": "Último registro: chegando ao ponto {stop}", + "Last report: next stop {stop}": "Último registro: próximo ponto {stop}", "Shown at street level · depth not in the feed": "Exibido no nível da rua · o feed não informa a profundidade", "Reported {seconds} s ago": "Informado há {seconds} s", "Reported {minutes} min ago": "Informado há {minutes} min", @@ -1184,7 +1184,7 @@ "Hidden by this map source · choose a globe map": "Oculto com esta fonte do mapa · escolha um mapa de globo", "Weather image HTTP {status}": "Imagem do clima: HTTP {status}", "Weather HTTP {status}": "Clima: HTTP {status}", - "Frame unavailable; previous observation retained": "Quadro indisponível; a observação anterior foi mantida", + "Frame unavailable; previous observation retained": "Quadro indisponível; mantido o anterior", "Weather imagery unavailable": "Imagens meteorológicas indisponíveis", "Weather source unavailable; previous observation retained": "Fonte meteorológica indisponível; a observação anterior foi mantida", "Weather unavailable": "Clima indisponível", @@ -1259,6 +1259,7 @@ "Malformed weather manifest": "Manifesto do clima malformado", "Weather request timed out": "A requisição do clima excedeu o tempo limite", "Cyclone request timed out": "A requisição de ciclones excedeu o tempo limite", + "Malformed cyclone snapshot": "Snapshot de ciclones malformado", "Temperature field unavailable": "Campo de temperatura indisponível", "Mean sea level pressure field unavailable": "Campo de pressão ao nível do mar indisponível", "Wind upstream unavailable": "Origem dos dados de vento indisponível", @@ -1338,12 +1339,12 @@ "Malformed wind manifest": "Manifesto de vento malformado", "Malformed wind grid": "Grade de vento malformada", "Error: {message}": "Erro: {message}", - "Needs {envVars} — add it in Provider Settings": "Requer {envVars} — adicione em Configurações de provedores", + "Needs {envVars} — add it in Provider Settings": "Requer {envVars} — adicione em \"Configurações de provedores\"", "{label} is unavailable": "{label} indisponível", "This map stack": "Esta pilha de mapas", "{label} unavailable: {hint}": "{label} indisponível: {hint}", "Google 3D tiles unavailable — check the key's API restrictions, quota, or network": "Google 3D Tiles indisponível — verifique as restrições de API da chave, a cota ou a rede", - "Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Requer {envVars} — adicione em Configurações de provedores — ou um token do Cesium ion para a rota hospedada no ion", + "Needs {envVars} — add it in Provider Settings — or a Cesium ion token for the ion-hosted route": "Requer {envVars} — adicione em \"Configurações de provedores\" — ou um token do Cesium ion para a rota hospedada no ion", "Exit cockpit to fly to a camera": "Saia da cabine para voar até uma câmera", "Exit cockpit to fly to a location": "Saia da cabine para voar até um local", "Exit cockpit to fly to a photo": "Saia da cabine para voar até uma foto", @@ -1384,7 +1385,7 @@ "many": "{count} de planos adicionados: {scene}", "other": "{count} planos adicionados: {scene}" }, - "Cannot capture shot: camera not ready": "Não foi possível capturar o plano: câmera não está pronta", + "Cannot capture shot: camera not ready": "Não foi possível capturar o plano: a câmera não está pronta", "Captured: {scene} / {shot}": "Capturado: {scene} / {shot}", "Select a shot first": "Selecione um plano primeiro", "Updated: {scene} / {shot}": "Atualizado: {scene} / {shot}", @@ -1623,7 +1624,7 @@ "many": "{count} de câmeras carregadas · ative o CCTV para usar", "other": "{count} câmeras carregadas · ative o CCTV para usar" }, - "Enable CCTV to load camera intersections": "Ative o CCTV para carregar interseções de câmeras", + "Enable CCTV to load camera intersections": "Ative o CCTV para carregar os cruzamentos com câmeras", "Enable CCTV to start camera-linked intelligence summaries.": "Ative o CCTV para iniciar resumos de inteligência vinculados às câmeras.", "No summary available.": "Nenhum resumo disponível.", "loading frames": "carregando quadros", @@ -1675,12 +1676,12 @@ "many": "{count} DE ENTRADAS DESCONHECIDAS · NÃO GARANTE SEGURANÇA", "other": "{count} ENTRADAS DESCONHECIDAS · NÃO GARANTE SEGURANÇA" }, - "AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR": "ENTRADAS DISPONÍVEIS ATUAIS · NÃO GARANTE SEGURANÇA", + "AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR": "ENTRADAS DISPONÍVEIS ATUALIZADAS · NÃO GARANTE SEGURANÇA", "SQUAWK {code} · {status}": "SQUAWK {code} · {status}", "ADS-B PRIORITY · {status}": "PRIORIDADE ADS-B · {status}", "ADS-B EMERGENCY · {status}": "EMERGÊNCIA ADS-B · {status}", "AIRCRAFT": "AERONAVE", - "ACQUIRING SURFACE": "ADQUIRINDO SUPERFÍCIE", + "ACQUIRING SURFACE": "CARREGANDO SUPERFÍCIE", "SURFACE FALLBACK": "SUPERFÍCIE ALTERNATIVA", "STALE FEED": "FEED DESATUALIZADO", "LIVE TRACK": "RASTRO AO VIVO", @@ -1795,9 +1796,9 @@ "{requirement}, or {offer}": "{requirement}, ou {offer}", "Live Flights": "Voos ao vivo", "Military Flights": "Voos militares", - "Earthquakes (24h)": "Terremotos (24 h)", + "Earthquakes (24h)": "Terremotos (24 h)", "Satellites": "Satélites", - "Space Missions (30d)": "Missões espaciais (30 d)", + "Space Missions (30d)": "Missões espaciais (30 d)", "Street Traffic": "Trânsito urbano", "CCTV": "CCTV", "Radio": "Rádio", @@ -1909,7 +1910,7 @@ "📍 Location: --": "📍 Local: --", "Landmark: --": "Ponto de referência: --", "Searched location": "Local pesquisado", - "ORBIT": "ÓRBITA", + "ORBIT": "ORBITANDO", "Location not found": "Local não encontrado", "Check the spelling and press Enter to search again.": "Verifique a grafia e pressione Enter para buscar de novo.", "Search failed": "Falha na busca", @@ -2084,7 +2085,7 @@ "Every day is empty here": "Todos os dias estão vazios aqui", "Sources off": "Fontes desativadas", "VS": "VS", - "Not set": "Não definido", + "Not set": "Sem imagem", "Basemap": "Mapa base", "Source off": "Fonte desativada", "Unpin {slot}": "Desafixar {slot}", @@ -2149,7 +2150,7 @@ "Shared {subject} could not be restored — feed unavailable": "Não foi possível restaurar o compartilhamento de {subject} — feed indisponível", "Shared {subject} is unavailable": "O compartilhamento de {subject} está indisponível", "{id} isn't reporting now — showing where it was last seen": "{id} não está transmitindo agora — mostrando onde foi visto pela última vez", - "{id} isn't reporting now — showing where it was at {time}": "{id} não está transmitindo agora — mostrando onde estava às {time}", + "{id} isn't reporting now — showing where it was at {time}": "{id} não está transmitindo agora — mostrando onde estava em {time}", "This share link can't be opened": "Não é possível abrir este link de compartilhamento", "Shared vessel is unavailable": "A embarcação compartilhada está indisponível", "ACQUIRING": "ADQUIRINDO", @@ -2358,7 +2359,7 @@ "Invalid Gemini connection endpoint": "Endpoint de conexão do Gemini inválido", "Gemini voice could not connect. Try again.": "Não foi possível conectar a voz do Gemini. Tente novamente.", "Gemini voice requires a same-origin app request": "A voz do Gemini exige uma requisição da mesma origem do app", - "Add a Gemini API key in Provider Settings": "Adicione uma chave de API do Gemini em Configurações de provedores", + "Add a Gemini API key in Provider Settings": "Adicione uma chave de API do Gemini em \"Configurações de provedores\"", "The Gemini Live model configuration is invalid": "A configuração do modelo do Gemini Live é inválida", "Gemini voice is starting too often. Try again shortly.": "A voz do Gemini está sendo iniciada com frequência demais. Tente novamente em breve.", "Gemini quota is unavailable. Try again later.": "A cota do Gemini está indisponível. Tente novamente mais tarde.", @@ -2529,7 +2530,7 @@ "{label} error": "{label}: erro", "counted the first {returned} loaded records": "foram contados os primeiros {returned} registros carregados", "counted {returned} of {total} loaded records": "foram contados {returned} de {total} registros carregados", - "{layers} just turned on — still loading, counts will rise": "{layers}: acabou de ser ativado — ainda carregando, as contagens vão subir", + "{layers} just turned on — still loading, counts will rise": "{layers}: ativação recente — ainda carregando, as contagens vão subir", "Contacts panel snapshot at {time} — the same retained cohort supplies the card, speech, and follow-up memory": "Snapshot do painel Contatos ({time}) — o mesmo grupo retido alimenta o cartão, a fala e a memória de acompanhamento", "Contacts panel snapshot — the same retained cohort supplies the card, speech, and follow-up memory": "Snapshot do painel Contatos — o mesmo grupo retido alimenta o cartão, a fala e a memória de acompanhamento", "Fly below {km} km to a covered region": "Desça abaixo de {km} km sobre uma região com cobertura", @@ -2662,7 +2663,7 @@ "Next satellite pass": "Próxima passagem do satélite", "Mark the map": "Marcar o mapa", "Mark {targets} +{more}": "Marcar {targets} +{more}", - "Mark {targets}": "Marcar: {targets}", + "Mark {targets}": "Marcar {targets}", "Fly to {place}": "Voar até {place}", "Fly to Location": "Voar até o local", "Nearest aircraft to {place}": "Aeronave mais próxima de {place}", @@ -2694,7 +2695,7 @@ "Recent speech": "Fala recente", "HERE": "AQUI", "Aircraft cockpit view": "Vista da cabine da aeronave", - "LEVEL": "NÍVEL", + "LEVEL": "NIVELADO", "Estimated destination direction": "Direção estimada do destino", "DEST ---°": "DEST ---°", "OPTICAL PLANE · 01": "PLANO ÓPTICO · 01", @@ -2825,7 +2826,7 @@ "DRAG TO TUNE": "ARRASTE PARA SINTONIZAR", "Tune available internet radio stations": "Sintonizar as estações de rádio pela internet disponíveis", "ALL · DRAG THE NEEDLE": "TODAS · ARRASTE A AGULHA", - "SNAPS TO AVAILABLE STATIONS": "AJUSTA ÀS ESTAÇÕES DISPONÍVEIS", + "SNAPS TO AVAILABLE STATIONS": "AJUSTA-SE ÀS ESTAÇÕES DISPONÍVEIS", "Radio playback": "Reprodução do rádio", "Previous filtered station": "Estação filtrada anterior", "Play selected station": "Reproduzir a estação selecionada", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 93e2b871d..13182cec1 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -2263,6 +2263,7 @@ "The Google Maps JavaScript API could not load": "Не удалось загрузить Google Maps JavaScript API", "Weather request timed out": "Истекло время ожидания запроса погоды", "Cyclone request timed out": "Истекло время ожидания запроса циклонов", + "Malformed cyclone snapshot": "Некорректный снимок циклонов", "Malformed weather manifest": "Некорректный манифест погоды", "Weather HTTP {status}": "Погода: HTTP {status}", "Weather image HTTP {status}": "Снимок погоды: HTTP {status}", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index aca16f65a..3d83437b7 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1203,6 +1203,7 @@ "Malformed weather manifest": "天气清单格式错误", "Weather request timed out": "天气请求超时", "Cyclone request timed out": "气旋请求超时", + "Malformed cyclone snapshot": "气旋快照格式错误", "Temperature field unavailable": "温度场不可用", "Mean sea level pressure field unavailable": "平均海平面气压场不可用", "Wind upstream unavailable": "风场上游数据源不可用", diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index 9fe469f6e..9fcab471b 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -1198,6 +1198,7 @@ "Malformed weather manifest": "天氣清單格式錯誤", "Weather request timed out": "天氣請求超時", "Cyclone request timed out": "氣旋請求超時", + "Malformed cyclone snapshot": "氣旋快照格式錯誤", "Temperature field unavailable": "溫度場不可用", "Mean sea level pressure field unavailable": "平均海平面氣壓場不可用", "Wind upstream unavailable": "風場上游數據源不可用", diff --git a/src/i18n/same-as-english.json b/src/i18n/same-as-english.json index 90abbc6a8..678b9a7b6 100644 --- a/src/i18n/same-as-english.json +++ b/src/i18n/same-as-english.json @@ -225,6 +225,7 @@ "Techno", "Trance", "USB {rate} msg/s", + "V/S {rate}", "VISUAL", "VOLUME", "VS", diff --git a/src/layers/cyclones/source.js b/src/layers/cyclones/source.js index e0039a20e..50aa24ad2 100644 --- a/src/layers/cyclones/source.js +++ b/src/layers/cyclones/source.js @@ -1,11 +1,11 @@ import { readResponseJsonCapped } from '../../sources/httpBody.js'; -// Shared with the AI tools, so no i18n import: the timeout stays English here -// and the cyclones layer translates it where it shows it (translateKnown). +// Shared with the AI tools, so no i18n import: these errors stay English here +// and the cyclones layer translates them where it shows them (translateKnown). const msg = (text) => text; export const CYCLONE_RESPONSE_LIMIT = 4 * 1024 * 1024; -const malformed = () => new Error('Malformed cyclone snapshot'); +const malformed = () => new Error(msg('Malformed cyclone snapshot')); const text = (value, max = 160) => { if ( typeof value !== 'string' || diff --git a/src/ui/templates/context.html b/src/ui/templates/context.html index 9e88d20f4..bb1947008 100644 --- a/src/ui/templates/context.html +++ b/src/ui/templates/context.html @@ -122,7 +122,7 @@