From 40485359215c086b1de0fae45820c3023c5bb33b Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 28 Oct 2026 22:40:38 +0400 Subject: [PATCH] i18n(pt): apply review round 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-model review (astra, sol, fable), round 3: 17 of 18 findings applied (some in own wording, one in part); "Missões espac." rejected. - pt: layer cards «{label}: camada ativada/desativada», pointer and Directions toasts name «A ferramenta "{owner}"», counters after a colon where the count can be 1, traffic «sem dados de fluxo» and «Fluxo: {percent}% cob», «limite de requisições atingido», FECHAR, ANT., PREDEF. VISUAIS, quoted "Nível da rua", infinitive tooltips, and more (see docs/translations/pt.md). - CCTV summary line localizes its CAL, SRC and CONTEXT values through existing catalog keys (all locales); codes stay underneath. Co-Authored-By: Claude Opus 5.5 --- docs/translations/pt.md | 8 +++ src/i18n/locales/pt.json | 92 +++++++++++++-------------- src/layers/cctv/presentation.js | 71 +++++++++++++++++---- src/layers/cctv/presentation.test.mjs | 38 +++++++++++ 4 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 src/layers/cctv/presentation.test.mjs diff --git a/docs/translations/pt.md b/docs/translations/pt.md index 15d6291b5..e71023b31 100644 --- a/docs/translations/pt.md +++ b/docs/translations/pt.md @@ -12,6 +12,8 @@ Translation source: abiyeenzo (#909). - 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}», «onde estava em {time}». Use «em», not «às», wherever {time} can include a date. +- A count in a string with no plural form (no `tn()`) goes after a colon, so it reads right for 1: «sincronizando feeds de cidades: {count}...», «CÂMERAS EM ESPERA: {count}». +- A placeholder that holds a layer or tool name of unknown gender and number gets a noun to agree with: «{label}: camada ativada», «A ferramenta "{owner}" está usando o ponteiro — feche-a primeiro». - 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, 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»). @@ -44,6 +46,8 @@ The `many` form is the `other` text with «de» after the number: «{count} de c | stale / degraded / unavailable | desatualizado / degradado / indisponível | | retry / retrying | nova tentativa / tentando de novo | | retrying (a fetch) / retrying slowly | buscando de novo / tentando de novo em intervalos maiores | +| rate-limited | «X: limite de requisições atingido» | +| dismiss (window, card, error) | fechar | | key (API key) / POWER UP / POWERED UP | chave / ATIVAR / ATIVADO | | Provider Settings | Configurações de provedores | | ON / OFF (state chips) | ATIVO / INATIVO | @@ -71,18 +75,21 @@ The `many` form is the `other` text with «de» after the number: «{count} de c | AQHI bands Low … Very High | Risco baixo … Risco muito alto | | radio station / broadcaster | estação de rádio / emissora | | Oldies / World (radio tags) | Flashback / Músicas do mundo | +| radio genre tags agree with «música» | Alternativa, Clássica, Eletrônica, Latina | | road / flow (traffic) | via / fluxo | | Street Level (layer) | Nível da rua | | scene / shot / scene recipe | cena / plano / receita de cena | | briefing / panel / dock | briefing / painel / dock | | cockpit | cabine | | preset / visual preset / EXPORT PRESETS | predefinição / predefinição visual / EXPORTAR PREDEF. | +| VISUAL PRESETS (dock caption) | PREDEF. VISUAIS | | Scope (circular viewport mask) / keyhole | mira / visor | | feather / fade | suavização / esmaecimento | | detection overlay | sobreposição de detecção | | pin (Draw tool) / box (Recent Imagery) | marcador / área | | Cyber (HUD layout, effect) | Ciber | | heading / course / altitude / ground speed | rumo / curso / altitude / velocidade solo (VELOC. SOLO) | +| map heading / bearing (compass, north up) | orientação (do mapa) | | 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. | @@ -95,4 +102,5 @@ The `many` form is the `other` text with «de» after the number: «{count} de c | 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) | +| {label} on / off (one layer) | «{label}: camada ativada / desativada» | | turn-by-turn: turn left / keep left / lane | vire à esquerda / mantenha-se à esquerda / faixa da esquerda | diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index fee5d1274..722bc6a74 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -17,7 +17,7 @@ "That shape could not be placed.": "Não foi possível colocar essa forma.", "Could not place the shape: {error}": "Não foi possível colocar a forma: {error}", "Board cleared.": "Quadro limpo.", - "{owner} is using the pointer — close it first.": "Ponteiro em uso: {owner}. Feche-o primeiro.", + "{owner} is using the pointer — close it first.": "A ferramenta \"{owner}\" está usando o ponteiro — feche-a primeiro.", "{label} · Detailed outline unavailable": "{label} · Contorno detalhado indisponível", "Flying to Austin, TX...": "Voando para Austin, TX...", "Restoring shared view...": "Restaurando a vista compartilhada...", @@ -187,18 +187,18 @@ "INITIAL M4.4 · RECLASSIFIED M5.2 LANDSLIDE": "INICIAL M4,4 · RECLASSIFICADO COMO DESLIZAMENTO M5,2", "USGS EVENT us7000tbwb · OPEN SOURCE": "EVENTO USGS us7000tbwb · ABRIR FONTE", "Open the USGS initial report for the 26 August 2026 Nepal landslide": "Abrir o relatório inicial do USGS sobre o deslizamento de 26 de agosto de 2026 no Nepal", - "Show the Nepal context callout without a border or country fill": "Mostra o destaque de contexto do Nepal sem fronteira nem preenchimento do país", - "Introduce Nepal, draw its border, then reveal the Bhote Koshi incident": "Apresenta o Nepal, desenha sua fronteira e depois revela o incidente do Bhote Koshi", + "Show the Nepal context callout without a border or country fill": "Mostrar o destaque de contexto do Nepal sem fronteira nem preenchimento do país", + "Introduce Nepal, draw its border, then reveal the Bhote Koshi incident": "Apresentar o Nepal, desenhar sua fronteira e depois revelar o incidente do Bhote Koshi", "CITIES": "CIDADES", - "Keep the regional incident context and introduce nearby city labels": "Mantém o contexto regional do incidente e apresenta os rótulos das cidades próximas", + "Keep the regional incident context and introduce nearby city labels": "Manter o contexto regional do incidente e apresentar os rótulos das cidades próximas", "PLACES": "LUGARES", - "Introduce the reconstruction scene key places from trigger to downstream": "Apresenta os lugares-chave da cena de reconstrução, do gatilho até rio abaixo", + "Introduce the reconstruction scene key places from trigger to downstream": "Apresentar os lugares-chave da cena de reconstrução, do gatilho até rio abaixo", "PATH": "PERCURSO", - "Animate the sourced cyan river-centerline route to Trishuli Bazaar": "Anima a rota ciano documentada pelo eixo do rio até Trishuli Bazaar", + "Animate the sourced cyan river-centerline route to Trishuli Bazaar": "Animar a rota ciano documentada pelo eixo do rio até Trishuli Bazaar", "OVERVIEW": "VISÃO GERAL", - "Keep the completed flood path and location labels visible": "Mantém visíveis o percurso completo da inundação e os rótulos dos locais", + "Keep the completed flood path and location labels visible": "Manter visíveis o percurso completo da inundação e os rótulos dos locais", "RECORD": "REGISTRO", - "Reveal the trigger record while making a slow camera approach": "Revela o registro do gatilho com uma aproximação lenta da câmera", + "Reveal the trigger record while making a slow camera approach": "Revelar o registro do gatilho com uma aproximação lenta da câmera", "{speed} kn": "{speed} kt", "That file is not valid JSON.": "Esse arquivo não é um JSON válido.", "That JSON file is not GeoJSON.": "Esse arquivo JSON não é GeoJSON.", @@ -210,7 +210,7 @@ "That file creates more than {limit} map objects (the limit).": "Esse arquivo cria mais de {limit} objetos no mapa (o limite).", "That file has more than {limit} style definitions and image links after cleanup (the limit).": "Após a limpeza, esse arquivo tem mais de {limit} definições de estilo e links de imagem (o limite).", "That file has more than {limit} features (the limit).": "Esse arquivo tem mais de {limit} elementos (o limite).", - "That file has more than {limit} points (the limit) across {features} features.": "Esse arquivo tem mais de {limit} pontos (o limite) em {features} elementos.", + "That file has more than {limit} points (the limit) across {features} features.": "Esse arquivo tem mais de {limit} pontos (o limite). Total de elementos: {features}.", "That file has more than {limit} points (the limit).": "Esse arquivo tem mais de {limit} pontos (o limite).", "That KMZ unpacks to more than the import limit.": "Descompactado, esse KMZ ultrapassa o limite de importação.", "That KMZ is damaged and could not be unpacked.": "Esse KMZ está danificado e não pôde ser descompactado.", @@ -236,7 +236,7 @@ "That file has {count} map objects; the limit is {limit}.": "Esse arquivo tem {count} objetos no mapa; o limite é {limit}.", "Ground heights could not be read. Try that file again.": "Não foi possível ler as altitudes do terreno. Tente esse arquivo de novo.", "That file has invalid point graphics.": "Esse arquivo tem gráficos de pontos inválidos.", - "Overpass rate-limited": "Overpass com limite de requisições", + "Overpass rate-limited": "Overpass: limite de requisições atingido", "Overpass timed out": "Tempo esgotado no Overpass", "Overpass could not complete the query": "Overpass não conseguiu concluir a consulta", "Map tiles temporarily unavailable": "Blocos do mapa temporariamente indisponíveis", @@ -494,7 +494,7 @@ "Focus {label}": "Focar {label}", "Global Context navigation": "Navegação do contexto global", "Previous — prior visited contact in the 250 km window": "Anterior — contato visitado anteriormente na janela de 250 km", - "PREVIOUS": "ANTERIOR", + "PREVIOUS": "ANT.", "FOCUS": "FOCAR", "Next — nearest unvisited contact in the 250 km window": "Próximo — contato não visitado mais próximo na janela de 250 km", "NEXT": "PRÓX.", @@ -515,7 +515,7 @@ "Military flights": "Voos militares", "AIS vessels": "Embarcações AIS", "Mapped installations": "Instalações mapeadas", - "syncing {count} city feeds...": "sincronizando {count} feeds de cidades...", + "syncing {count} city feeds...": "sincronizando feeds de cidades: {count}...", "scanning nearby systems...": "buscando sistemas próximos...", "GBFS fetch error": "erro ao obter dados GBFS", "Station {id}": "Estação {id}", @@ -525,7 +525,7 @@ "⚠️ Not renting": "⚠️ Sem aluguel", "⚠️ Not returning": "⚠️ Sem devolução", "HDG {value}° (ESTIMATED)": "HDG {value}° (ESTIMADO)", - "{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE": "{count} CÂMERAS EM ESPERA · NENHUMA CÂMERA SELECIONADA · CLIQUE EM UMA CÂMERA PARA ATIVAR", + "{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE": "CÂMERAS EM ESPERA: {count} · NENHUMA CÂMERA SELECIONADA · CLIQUE EM UMA CÂMERA PARA ATIVAR", "No cameras available in catalog.": "Nenhuma câmera disponível no catálogo.", "{city} CCTV": "CCTV {city}", "FOV {value}° (DATASHEET)": "FOV {value}° (FICHA TÉCNICA)", @@ -684,9 +684,9 @@ "Place A and B first": "Coloque A e B primeiro", "action|CLEAR": "LIMPAR", "Remove the route and both markers": "Remover a rota e os dois marcadores", - "press Escape twice to leave Draw": "pressione Esc duas vezes para sair de Desenho", - "{name} is active — turn {name} off, then set {endpoint}": "{name} está ativo — desative {name} e depois defina {endpoint}", - "{name} is active — {leave}, then set {endpoint}": "{name} está ativo — {leave} e depois defina {endpoint}", + "press Escape twice to leave Draw": "pressione Esc duas vezes para sair de \"Desenho\"", + "{name} is active — turn {name} off, then set {endpoint}": "A ferramenta \"{name}\" está ativa — desative \"{name}\" e depois defina {endpoint}", + "{name} is active — {leave}, then set {endpoint}": "A ferramenta \"{name}\" está ativa — {leave} e depois defina {endpoint}", "Only the first {count} turns of this route are shown": "Só as primeiras {count} conversões desta rota são mostradas", "Turn-by-turn directions": "Instruções passo a passo", "Another map tool is using clicks — close it, then SET A again": "Outra ferramenta do mapa está usando os cliques — feche-a e pressione DEFINIR A de novo", @@ -815,7 +815,7 @@ "ORBIT REPLAY · {mission}": "REPRISE DA ÓRBITA · {mission}", "PAUSED · {title}": "PAUSADO · {title}", "UNSPECIFIED": "NÃO ESPECIFICADO", - "+{count} additional payload records": "+{count} registros de carga útil adicionais", + "+{count} additional payload records": "Outros registros de carga útil: {count}", "PAYLOAD DATA UNAVAILABLE": "DADOS DA CARGA ÚTIL INDISPONÍVEIS", "{value} KM DOWNRANGE": "A {value} KM DO LOCAL DE LANÇAMENTO", "POSITION UNAVAILABLE": "POSIÇÃO INDISPONÍVEL", @@ -895,7 +895,7 @@ "opening receiver": "abrindo o receptor", "checking decoder feeds": "verificando os feeds do decodificador", "WebUSB needs desktop Chrome or Edge": "O WebUSB requer Chrome ou Edge para desktop", - "connect a receiver in Radio": "conecte um receptor em Rádio", + "connect a receiver in Radio": "conecte um receptor em \"Rádio\"", "receiver is in FM mode": "o receptor está no modo FM", "listening": "escutando", "USB error": "erro de USB", @@ -969,7 +969,7 @@ "Broadcaster stream is unavailable or blocked by the browser.": "O stream da emissora está indisponível ou foi bloqueado pelo navegador.", "Playback requires a direct click or tap.": "A reprodução exige um clique ou toque direto.", "Broadcaster stream could not be started.": "Não foi possível iniciar o stream da emissora.", - "Alternative": "Alternativo", + "Alternative": "Alternativa", "Ambient": "Ambiente", "Blues": "Blues", "Classical": "Clássica", @@ -1032,7 +1032,7 @@ "DOCKED · {name} · +{extra}": "ACOPLADO · {name} · +{extra}", "DOCKED · {name}": "ACOPLADO · {name}", "{name} needs the Google 3D map: choose it under MAP SOURCE": "{name} requer o mapa Google 3D: escolha-o em FONTE DO MAPA", - "zoom in to a street to open {name}": "aproxime-se de uma rua para abrir {name}", + "zoom in to a street to open {name}": "aproxime-se de uma rua para abrir o {name}", "loading coverage...": "carregando cobertura...", "No street-level imagery within {radius} m of that point": "Nenhuma imagem no nível da rua a até {radius} m desse ponto", "No street-level imagery within {radius} m of the view centre": "Nenhuma imagem no nível da rua a até {radius} m do centro da vista", @@ -1111,17 +1111,17 @@ "TomTom flow unavailable": "Fluxo do TomTom indisponível", "SIMULATED — {reason}": "SIMULADO — {reason}", "Syncing flow · Roads: {source}": "Sincronizando fluxo · Vias: {source}", - "LIVE · Roads: {source} · Roads without flow hidden": "AO VIVO · Vias: {source} · Vias sem fluxo ocultas", - "LIVE · Roads: {source} · No flow roads in view": "AO VIVO · Vias: {source} · Nenhuma via com fluxo na vista", - "LIVE · Roads: {source} · Flow {percent}%": "AO VIVO · Vias: {source} · Fluxo {percent}%", - "SIMULATED · Roads: {source} · Flow {percent}%": "SIMULADO · Vias: {source} · Fluxo {percent}%", + "LIVE · Roads: {source} · Roads without flow hidden": "AO VIVO · Vias: {source} · Vias sem dados de fluxo ocultas", + "LIVE · Roads: {source} · No flow roads in view": "AO VIVO · Vias: {source} · Nenhuma via com dados de fluxo na vista", + "LIVE · Roads: {source} · Flow {percent}%": "AO VIVO · Vias: {source} · Fluxo: {percent}% cob", + "SIMULATED · Roads: {source} · Flow {percent}%": "SIMULADO · Vias: {source} · Fluxo: {percent}% cob", "Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated": "Sincronizando fluxo · Vias: OpenStreetMap · Fluxo: TomTom · Sem correspondência: simulado", "LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated": "AO VIVO · Vias: OpenStreetMap · Fluxo: TomTom · {percent}% cob · Sem correspondência: simulado", "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 do TomTom para dados ao vivo", - "OpenFreeMap tiles rate-limited": "Blocos do OpenFreeMap com limite de requisições", + "OpenFreeMap tiles rate-limited": "Blocos do OpenFreeMap: limite de requisições atingido", "OpenFreeMap tiles timed out": "Os blocos do OpenFreeMap excederam o tempo limite", "OpenFreeMap tiles unavailable": "Blocos do OpenFreeMap indisponíveis", "occupancy|empty": "vazio", @@ -1231,9 +1231,9 @@ "Hourly global mosaic; usually 2–3 hours delayed": "Mosaico global por hora; normalmente com 2–3 horas de atraso", "GOES regional clouds; approximately 5-minute updates": "Nuvens regionais do GOES; atualizações a cada 5 minutos aproximadamente", "Clouds only": "Só nuvens", - "Apply the full cloud brightness key; 100% filter strength": "Aplica o filtro completo de brilho das nuvens; intensidade do filtro 100%", + "Apply the full cloud brightness key; 100% filter strength": "Aplicar o filtro completo de brilho das nuvens; intensidade do filtro 100%", "Full": "Completa", - "Show the complete infrared image; 0% filter strength": "Mostra a imagem infravermelha completa; intensidade do filtro 0%", + "Show the complete infrared image; 0% filter strength": "Mostrar a imagem infravermelha completa; intensidade do filtro 0%", "Soft": "Suave", "Vivid": "Intensa", "Image opacity; does not alter the observed values": "Opacidade da imagem; não altera os valores observados", @@ -1282,7 +1282,7 @@ "Reduced motion": "Movimento reduzido", "Resume": "Retomar", "Pause": "Pausar", - "Pause visual flow; forecast time does not advance with animation": "Pausa o fluxo visual; o horário da previsão não avança com a animação", + "Pause visual flow; forecast time does not advance with animation": "Pausar o fluxo visual; o horário da previsão não avança com a animação", "None": "Nenhum", "Speed": "Velocidade", "Pressure": "Pressão", @@ -1475,7 +1475,7 @@ "New anchor ID": "ID da nova âncora", "Capture camera as anchor": "Capturar a câmera como âncora", "Anchor added to draft": "Âncora adicionada ao rascunho", - "Set move start to current camera": "Definir o início do movimento na câmera atual", + "Set move start to current camera": "Usar a câmera atual como início do movimento", "Move added to draft; destination remains the shot camera": "Movimento adicionado ao rascunho; o destino continua sendo a câmera do plano", "Use ordinary flight": "Usar voo comum", "Invalid shot JSON": "JSON de plano inválido", @@ -1500,7 +1500,7 @@ "SDR decoder worker failed": "Falha no processo do decodificador SDR", "SDR decoder did not start; receiver stopped": "O decodificador SDR não iniciou; receptor parado", "SDR decoder stopped responding; receiver stopped": "O decodificador SDR parou de responder; receptor parado", - "SDR decoder fell behind; receiver stopped": "O decodificador SDR ficou para trás; receptor parado", + "SDR decoder fell behind; receiver stopped": "O decodificador SDR não acompanhou os dados recebidos; receptor parado", "WebUSB requires desktop Chrome or Edge": "O WebUSB requer Chrome ou Edge para desktop", "Waiting for an RTL-SDR device…": "Aguardando um dispositivo RTL-SDR…", "Browser audio is suspended; press Connect again to resume it": "O áudio do navegador está suspenso; pressione Conectar de novo para retomá-lo", @@ -1542,7 +1542,7 @@ "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": "PREDEFINIÇÕES VISUAIS", + "VISUAL PRESETS": "PREDEF. VISUAIS", "LOCATION": "LOCAL", "CONTACTS — nearest planes · vessels · sites": "CONTATOS — aviões · embarcações · locais mais próximos", "SPACE MISSIONS — launches & orbital assets": "MISSÕES ESPACIAIS — lançamentos e ativos orbitais", @@ -1554,7 +1554,7 @@ "Copy failed": "Falha ao copiar", "Return map to straight-down view": "Voltar o mapa à vista de cima", "Tilt map to oblique view": "Inclinar o mapa para uma vista oblíqua", - "Reset map to north up. Current heading {heading} degrees": "Orientar o mapa com o norte para cima. Rumo atual: {heading} graus", + "Reset map to north up. Current heading {heading} degrees": "Orientar o mapa com o norte para cima. Orientação atual: {heading} graus", "Tilted view": "Vista inclinada", "Straight-down view": "Vista de cima", "North up": "Norte para cima", @@ -1645,7 +1645,7 @@ "CYCLE ON": "CICLO LIGADO", "CYCLE OFF": "CICLO DESLIG.", "Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.": "Parar a rotação automática de páginas. Anterior, Próxima e as abas SIG/NOTÍCIAS/LOCAL continuam disponíveis.", - "Cycle briefing pages automatically every 9 seconds (Signals → News → Local). Pauses while you hover or focus the panel. Live signal data refreshes continuously either way.": "Alterna as páginas do briefing automaticamente a cada 9 segundos (Sinais → Notícias → Local). Pausa enquanto você passa o mouse ou foca o painel. Os dados de sinais ao vivo continuam atualizando de qualquer forma.", + "Cycle briefing pages automatically every 9 seconds (Signals → News → Local). Pauses while you hover or focus the panel. Live signal data refreshes continuously either way.": "Alternar as páginas do briefing automaticamente a cada 9 segundos (Sinais → Notícias → Local). A alternância pausa enquanto você passa o mouse ou foca o painel. Os dados de sinais ao vivo continuam sendo atualizados de qualquer forma.", "ACQUIRING REGIONAL NEWS": "OBTENDO NOTÍCIAS DA ÁREA", "REGIONAL NEWS UNAVAILABLE": "NOTÍCIAS DA ÁREA INDISPONÍVEIS", "RESOLVING REGION": "IDENTIFICANDO REGIÃO", @@ -1958,7 +1958,7 @@ "Station unavailable after directory refresh": "Estação indisponível após atualizar o diretório", "Radio could not {action} cleanly": "Não foi possível {action} o rádio corretamente", "Go to expanded Radio section": "Ir para a seção de rádio expandida", - "Expand Radio section in Context": "Expandir a seção de rádio em Contexto", + "Expand Radio section in Context": "Expandir a seção de rádio em \"Contexto\"", "Close compact Radio controls": "Fechar controles compactos do rádio", "Open compact Radio controls": "Abrir controles compactos do rádio", "SYNC": "SINC", @@ -2019,7 +2019,7 @@ "Point the camera at the ground to use the view": "Aponte a câmera para o chão para usar a vista", "That point cannot anchor a box": "Esse ponto não pode ancorar uma área", "Imagery stops at ±{limit}° latitude": "As imagens vão até ±{limit}° de latitude", - "{owner} is using the pointer — close it first": "Ponteiro em uso: {owner}. Feche-o primeiro", + "{owner} is using the pointer — close it first": "A ferramenta \"{owner}\" está usando o ponteiro — feche-a primeiro", "Box is {size} wide · limit {limit}": "A área tem {size} de largura · limite {limit}", "View is {size} wide · limit {limit}": "A vista tem {size} de largura · limite {limit}", "VS BASEMAP": "VS MAPA BASE", @@ -2167,7 +2167,7 @@ "RETRYING ALPR CAMERAS": "BUSCANDO DE NOVO AS CÂMERAS ALPR", "FETCHING ALPR CAMERAS": "BUSCANDO CÂMERAS ALPR", "OVERPASS TEMPORARILY UNAVAILABLE": "OVERPASS TEMPORARIAMENTE INDISPONÍVEL", - "OVERPASS RATE-LIMITED": "OVERPASS COM LIMITE DE REQUISIÇÕES", + "OVERPASS RATE-LIMITED": "OVERPASS: LIMITE DE REQUISIÇÕES ATINGIDO", "OVERPASS TIMED OUT": "TEMPO ESGOTADO NO OVERPASS", "OVERPASS COULD NOT COMPLETE THE QUERY": "OVERPASS NÃO CONSEGUIU CONCLUIR A CONSULTA", "MAP TILES TEMPORARILY UNAVAILABLE": "BLOCOS DO MAPA TEMPORARIAMENTE INDISPONÍVEIS", @@ -2232,8 +2232,8 @@ "other": "{count} feeds ao vivo" }, "{feed} {count} ({seconds}s old)": "{feed} {count} (há {seconds} s)", - "Street Level toggle failed": "Não foi possível ativar ou desativar a camada Nível da rua", - "Street Level could not start": "Não foi possível iniciar Nível da rua", + "Street Level toggle failed": "Não foi possível ativar ou desativar a camada \"Nível da rua\"", + "Street Level could not start": "Não foi possível iniciar a camada \"Nível da rua\"", "SHRINK": "REDUZIR", "EXPAND": "AMPLIAR", "Shrink": "Reduzir", @@ -2253,8 +2253,8 @@ "other": "ÚLTIMOS {count} DIAS" }, "{window} · SINCE {date}": "{window} · DESDE {date}", - "Turn Street Level off": "Desativar Nível da rua", - "Turn Street Level on": "Ativar Nível da rua", + "Turn Street Level off": "Desativar \"Nível da rua\"", + "Turn Street Level on": "Ativar \"Nível da rua\"", "{name} imagery on": "Imagens do {name} ativadas", "{name} imagery off": "Imagens do {name} desativadas", "Image by {creator}": "Imagem de {creator}", @@ -2262,7 +2262,7 @@ "OPEN {labels}": "ABRIR {labels}", "Open {names} at the centre of the view": "Abrir {names} no centro da vista", "Zoom in to a street to open {names} here": "Aproxime-se de uma rua para abrir {names} aqui", - "Switch Street Level on to draw its coverage.": "Ative Nível da rua para desenhar a cobertura.", + "Switch Street Level on to draw its coverage.": "Ative \"Nível da rua\" para desenhar a cobertura.", "Every provider is switched off: light a chip to draw its coverage.": "Todos os provedores estão desligados: ative um para desenhar a cobertura dele.", "Loading this sequence…": "Carregando esta sequência…", "{n} images in this sequence · Esc clears": { @@ -2346,7 +2346,7 @@ "Custom": "Personalizado", "These preferences stay in this browser.": "Estas preferências ficam neste navegador.", "VOICE SYSTEM ERROR": "ERRO NO SISTEMA DE VOZ", - "DISMISS": "DISPENSAR", + "DISMISS": "FECHAR", "Check microphone permission and network access, then try again.": "Verifique a permissão do microfone e o acesso à rede, e tente novamente.", "Gemini returned an oversized audio chunk": "O Gemini retornou um trecho de áudio grande demais", "Gemini returned an unsupported audio format": "O Gemini retornou um formato de áudio não suportado", @@ -2525,7 +2525,7 @@ "the flights layer loads by viewport": "a camada de voos carrega conforme a vista", "distance is ground distance, not overhead": "a distância é medida no solo até o ponto subsatélite, não até o satélite", "the count is a lower bound because at least one layer reached its retained limit.": "a contagem é um mínimo porque pelo menos uma camada atingiu o limite de retenção.", - "{label} off": "{label}: desativado", + "{label} off": "{label}: camada desativada", "{label} loading": "{label}: carregando", "{label} error": "{label}: erro", "counted the first {returned} loaded records": "foram contados os primeiros {returned} registros carregados", @@ -2578,7 +2578,7 @@ "{name} is no longer loaded": "{name} não está mais carregado", "Unknown target layer: {target}": "Camada de destino desconhecida: {target}", "nominal": "nominal", - "{label} on": "{label}: ativado", + "{label} on": "{label}: camada ativada", "Frame aircraft": "Enquadrar aeronaves", "Frame military aircraft": "Enquadrar aeronaves militares", "Frame ships": "Enquadrar embarcações", @@ -2949,7 +2949,7 @@ "RESET CAL": "REDEFINIR CAL", "SCENE SUMMARY": "RESUMO DA CENA", "STREET LEVEL": "NÍVEL DA RUA", - "Turn Street Level on or off": "Ativar ou desativar Nível da rua", + "Turn Street Level on or off": "Ativar ou desativar \"Nível da rua\"", "Show the street-level view full screen (Esc shrinks it)": "Mostrar a vista no nível da rua em tela cheia (Esc reduz)", "Image fit": "Ajuste da imagem", "FIT": "AJUSTAR", @@ -3002,6 +3002,6 @@ "EXPLORE MANUALLY": "EXPLORAR MANUALMENTE", "Begin with a clean globe": "Comece com um globo limpo", "Don't show this again": "Não mostrar novamente", - "ESC to dismiss": "ESC para dispensar", + "ESC to dismiss": "ESC para fechar", "Tip: the GEV MIC button in the dock lets you talk to the map.": "Dica: o botão GEV MIC no dock permite falar com o mapa." } diff --git a/src/layers/cctv/presentation.js b/src/layers/cctv/presentation.js index e47e08194..898e4605c 100644 --- a/src/layers/cctv/presentation.js +++ b/src/layers/cctv/presentation.js @@ -1,5 +1,5 @@ import { CCTV_AMBIENT_CARD_MAX } from '../../data/cctvLod.js'; -import { t } from '../../i18n/index.js'; +import { msg, t } from '../../i18n/index.js'; import { ACTIVE_FRAME_REFRESH_MS, IDLE_FRAME_REFRESH_MS } from './policy.js'; import { headingHudToken, isHeadingEstimated } from './headingConfidence.js'; import { @@ -8,6 +8,59 @@ import { isDatasheetFov, } from './modelSpecs.js'; +// The summary line names the CAL badge, the source kind and the view band in +// the page language; the codes underneath stay English. +const CAL_BADGE_LABELS = Object.freeze({ + calibrated: msg('CALIBRATED'), + curated: msg('CURATED'), + 'raw-prior': msg('RAW PRIOR'), +}); + +// The same source-kind words as the CCTV panel (src/ui/cctvFrames.js). +const SOURCE_KIND_LABELS = Object.freeze({ + snapshot: msg('SNAPSHOT'), + live: msg('LIVE'), + fallback: msg('FALLBACK'), + upstream: msg('UPSTREAM'), + synthetic: msg('SYNTHETIC'), + configured: msg('CONFIGURED'), + seed: msg('SEED'), + image: msg('IMAGE'), + unknown: msg('UNKNOWN'), +}); + +/** CAL badge id ('raw-prior') as the summary shows it; unknown ids stay codes. */ +export function summaryCalLabel(badge) { + const key = String(badge || ''); + return CAL_BADGE_LABELS[key] + ? t(CAL_BADGE_LABELS[key]) + : key.replace('-', ' ').toUpperCase(); +} + +/** Source kind ('image', 'snapshot') for the summary; HLS, MJPEG… stay codes. */ +export function summarySourceLabel(kind) { + const key = String(kind || 'image').toLowerCase(); + return SOURCE_KIND_LABELS[key] + ? t(SOURCE_KIND_LABELS[key]) + : key.toUpperCase(); +} + +/** View band id ('street', 'city'…) as the HUD names it. */ +export function summaryViewBandLabel(band) { + switch (band) { + case 'street': + return t('STREET', { context: 'view band' }); + case 'city': + return t('CITY', { context: 'view band' }); + case 'regional': + return t('REGIONAL', { context: 'view band' }); + case 'global': + return t('GLOBAL', { context: 'view band' }); + default: + return String(band || '').toUpperCase(); + } +} + export function createPresentation({ state: layerState, services, @@ -72,17 +125,13 @@ export function createPresentation({ : t('ISOLATED VIEW'), layerState._showProjection ? t('PROJ MONITOR') : t('PROJ OFF'), layerState._coverageMode === 'viewshed' ? t('VIEWSHED') : null, - t('CAL {value}', { - value: calBadge.replace('-', ' ').toUpperCase(), + t('CAL {value}', { value: summaryCalLabel(calBadge) }), + t('SRC {value}', { + value: summarySourceLabel( + health?.sourceKind || active.camera.feedType || 'image', + ), }), - health?.sourceKind - ? t('SRC {value}', { - value: String(health.sourceKind).toUpperCase(), - }) - : t('SRC {value}', { - value: String(active.camera.feedType || 'image').toUpperCase(), - }), - t('{value} CONTEXT', { value: viewBand.toUpperCase() }), + t('{value} CONTEXT', { value: summaryViewBandLabel(viewBand) }), // Identified hardware: append the model's datasheet specs + attribution. active.camera.spec ? t('{spec} · SPECS: CCTV-DATABASE.COM', { diff --git a/src/layers/cctv/presentation.test.mjs b/src/layers/cctv/presentation.test.mjs new file mode 100644 index 000000000..a0600f9a4 --- /dev/null +++ b/src/layers/cctv/presentation.test.mjs @@ -0,0 +1,38 @@ +// The CCTV summary line names the CAL badge, the source kind and the view +// band in the page language; unknown codes stay as uppercase codes. +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { setLocale } from '../../i18n/index.js'; +import { + summaryCalLabel, + summarySourceLabel, + summaryViewBandLabel, +} from './presentation.js'; + +function inLocale(locale, run) { + setLocale(locale); + try { + return run(); + } finally { + setLocale('en'); + } +} + +test('English keeps the summary codes', () => { + assert.equal(summaryCalLabel('raw-prior'), 'RAW PRIOR'); + assert.equal(summarySourceLabel('image'), 'IMAGE'); + assert.equal(summarySourceLabel('hls'), 'HLS'); + assert.equal(summaryViewBandLabel('street'), 'STREET'); + assert.equal(summaryViewBandLabel('none'), 'NONE'); +}); + +test('Portuguese summary values are translated', () => { + inLocale('pt', () => { + assert.equal(summaryCalLabel('raw-prior'), 'ESTIMATIVA BRUTA'); + assert.equal(summaryCalLabel('calibrated'), 'CALIBRADA'); + assert.equal(summarySourceLabel('IMAGE'), 'IMAGEM'); + assert.equal(summarySourceLabel('mjpeg'), 'MJPEG'); + assert.equal(summaryViewBandLabel('street'), 'RUA'); + assert.equal(summaryViewBandLabel('city'), 'CIDADE'); + }); +});