i18n(pt): apply review round 2

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 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-28 17:57:51 +04:00
1 parent 3c0c10d3b1
commit 858ce827e5
13 files changed
+79 -46

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ A module the server, tools or other portable code also imports must not load the
<input placeholder="Search any location..." data-i18n-placeholder />
```
`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
+9 -5
View File
@@ -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 |
+5 -2
View File
@@ -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,
)) {
+15
View File
@@ -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 = {
+7 -2
View File
@@ -228,7 +228,9 @@ function normalizeMarkupText(text) {
* Markup opt-in: the element's own English text or attribute is the source.
* <span data-i18n>Normal</span>
* <button title="Pin" data-i18n-title>…</button>
* 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:
* <option data-i18n data-i18n-context="radio tag">All</option>
*/
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;
+1
View File
@@ -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}",
+33 -32
View File
@@ -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",
+1
View File
@@ -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}",
+1
View File
@@ -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": "风场上游数据源不可用",
+1
View File
@@ -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": "風場上游數據源不可用",
+1
View File
@@ -225,6 +225,7 @@
"Techno",
"Trance",
"USB {rate} msg/s",
"V/S {rate}",
"VISUAL",
"VOLUME",
"VS",
+3 -3
View File
@@ -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' ||
+1 -1
View File
@@ -122,7 +122,7 @@
<button id="radio-enable-btn" class="panel-layer-toggle" type="button" aria-pressed="false" data-i18n>ENABLE</button>
<label class="radio-filter-label" for="radio-filter" data-i18n>STATION TAG</label>
<select class="gev-field" id="radio-filter" aria-label="Filter stations by station tag" disabled data-i18n-aria-label>
<option value="all" data-i18n>All</option>
<option value="all" data-i18n data-i18n-context="radio tag">All</option>
</select>
</div>
<div id="radio-station-card" class="radio-station-card" aria-live="polite">