/** * i18n completo — M9.8 * * Dicionário pt-BR + en-US para todas as strings de UI do VentoApp. * * Convenção: * - Chaves em snake_case agrupadas por área (nav_*, app_*, common_*, etc.) * - Fallback automático: chave → en-US → pt-BR * - Interpolação via {placeholder} (substituição simples) * - Persistência em localStorage com chave 'ventoapp.locale' */ export type Locale = 'pt-BR' | 'en-US'; export const supportedLocales: readonly Locale[] = ['pt-BR', 'en-US'] as const; export const DEFAULT_LOCALE: Locale = 'pt-BR'; const LOCALE_STORAGE_KEY = 'ventoapp.locale'; /** Carrega locale do localStorage ou retorna o padrão. */ export function loadStoredLocale(): Locale { if (typeof window === 'undefined') return DEFAULT_LOCALE; try { const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY); if (stored === 'pt-BR' || stored === 'en-US') return stored; } catch { // localStorage indisponível (modo privado, etc.) — usa padrão } return DEFAULT_LOCALE; } /** Persiste locale no localStorage. */ export function saveStoredLocale(locale: Locale): void { if (typeof window === 'undefined') return; try { window.localStorage.setItem(LOCALE_STORAGE_KEY, locale); } catch { // localStorage indisponível — silenciosamente ignora } } /** Dicionário principal de traduções. */ type Dict = Record>; const translations: Dict = { // === Aplicação === app_title: { 'pt-BR': 'BrainWind v1.0', 'en-US': 'BrainWind v1.0' }, app_subtitle: { 'pt-BR': 'Cálculo de cargas de vento — NBR 6123:2023', 'en-US': 'Wind load calculation — NBR 6123:2023' }, app_loading: { 'pt-BR': 'Carregando...', 'en-US': 'Loading...' }, // === Navegação === nav_home: { 'pt-BR': 'Início', 'en-US': 'Home' }, nav_warehouse: { 'pt-BR': 'Galpão', 'en-US': 'Warehouse' }, nav_cylinder: { 'pt-BR': 'Cilindro', 'en-US': 'Cylinder' }, nav_vault: { 'pt-BR': 'Abóbada', 'en-US': 'Vault' }, nav_dome: { 'pt-BR': 'Cúpula', 'en-US': 'Dome' }, nav_sign: { 'pt-BR': 'Muros/Placas', 'en-US': 'Signs/Walls' }, nav_isolated_roof: { 'pt-BR': 'Coberturas Isoladas', 'en-US': 'Isolated Roofs' }, nav_bar: { 'pt-BR': 'Barras', 'en-US': 'Bars' }, nav_bridge: { 'pt-BR': 'Pontes', 'en-US': 'Bridges' }, nav_tower: { 'pt-BR': 'Torres', 'en-US': 'Towers' }, nav_dynamics: { 'pt-BR': 'Dinâmica + Vórtices', 'en-US': 'Dynamics + Vortex' }, nav_settings: { 'pt-BR': 'Preferências', 'en-US': 'Preferences' }, nav_collapse: { 'pt-BR': 'Recolher sidebar', 'en-US': 'Collapse sidebar' }, nav_expand: { 'pt-BR': 'Expandir sidebar', 'en-US': 'Expand sidebar' }, // === Comum (botões / ações) === common_save: { 'pt-BR': 'Salvar', 'en-US': 'Save' }, common_cancel: { 'pt-BR': 'Cancelar', 'en-US': 'Cancel' }, common_delete: { 'pt-BR': 'Excluir', 'en-US': 'Delete' }, common_edit: { 'pt-BR': 'Editar', 'en-US': 'Edit' }, common_download: { 'pt-BR': 'Baixar', 'en-US': 'Download' }, common_clear: { 'pt-BR': 'Limpar', 'en-US': 'Clear' }, common_export: { 'pt-BR': 'Exportar', 'en-US': 'Export' }, common_import: { 'pt-BR': 'Importar', 'en-US': 'Import' }, common_apply: { 'pt-BR': 'Aplicar', 'en-US': 'Apply' }, common_close: { 'pt-BR': 'Fechar', 'en-US': 'Close' }, common_yes: { 'pt-BR': 'Sim', 'en-US': 'Yes' }, common_no: { 'pt-BR': 'Não', 'en-US': 'No' }, common_ok: { 'pt-BR': 'OK', 'en-US': 'OK' }, common_loading: { 'pt-BR': 'Carregando...', 'en-US': 'Loading...' }, common_error: { 'pt-BR': 'Erro', 'en-US': 'Error' }, common_warning: { 'pt-BR': 'Atenção', 'en-US': 'Warning' }, common_success: { 'pt-BR': 'Sucesso', 'en-US': 'Success' }, common_back: { 'pt-BR': 'Voltar', 'en-US': 'Back' }, common_next: { 'pt-BR': 'Próximo', 'en-US': 'Next' }, // === Exportação === export_csv: { 'pt-BR': 'Exportar CSV', 'en-US': 'Export CSV' }, export_pdf: { 'pt-BR': 'Exportar PDF', 'en-US': 'Export PDF' }, export_ftool: { 'pt-BR': 'Ftool', 'en-US': 'Ftool' }, export_snapshot: { 'pt-BR': 'Exportar estado', 'en-US': 'Export state' }, export_import: { 'pt-BR': 'Importar projeto', 'en-US': 'Import project' }, // === Configurações / Tema === settings_appearance: { 'pt-BR': 'Aparência', 'en-US': 'Appearance' }, settings_appearance_desc: { 'pt-BR': 'Tema do aplicativo (claro/escuro/sistema).', 'en-US': 'Application theme (light/dark/system).' }, settings_theme_light: { 'pt-BR': 'Claro', 'en-US': 'Light' }, settings_theme_dark: { 'pt-BR': 'Escuro', 'en-US': 'Dark' }, settings_theme_system: { 'pt-BR': 'Sistema', 'en-US': 'System' }, settings_effective: { 'pt-BR': 'Tema efetivo atual', 'en-US': 'Current effective theme' }, settings_projects: { 'pt-BR': 'Projetos Salvos', 'en-US': 'Saved Projects' }, settings_projects_desc: { 'pt-BR': 'Persistência local via IndexedDB.', 'en-US': 'Local persistence via IndexedDB.' }, settings_projects_count: { 'pt-BR': '{count} projeto(s) armazenado(s).', 'en-US': '{count} project(s) stored.' }, settings_no_projects: { 'pt-BR': 'Nenhum projeto salvo ainda.', 'en-US': 'No saved projects yet.' }, settings_importing: { 'pt-BR': 'Importando...', 'en-US': 'Importing...' }, settings_import_success: { 'pt-BR': 'Importação concluída', 'en-US': 'Import successful' }, settings_import_error: { 'pt-BR': 'Falha na importação', 'en-US': 'Import failed' }, settings_import_module: { 'pt-BR': 'Módulo', 'en-US': 'Module' }, settings_import_project: { 'pt-BR': 'Projeto', 'en-US': 'Project' }, settings_import_fields: { 'pt-BR': 'Campos aplicados ({count})', 'en-US': 'Applied fields ({count})' }, settings_import_warnings: { 'pt-BR': 'Avisos', 'en-US': 'Warnings' }, settings_state: { 'pt-BR': 'Estado Atual', 'en-US': 'Current State' }, settings_state_desc: { 'pt-BR': 'Snapshot do windStore para debug.', 'en-US': 'windStore snapshot for debug.' }, settings_about: { 'pt-BR': 'Sobre', 'en-US': 'About' }, settings_about_desc: { 'pt-BR': 'Cálculo de cargas de vento conforme NBR 6123:2023.', 'en-US': 'Wind load calculation per NBR 6123:2023.' }, settings_stack: { 'pt-BR': 'Stack', 'en-US': 'Stack' }, // === Galpão / Warehouse === geom_width: { 'pt-BR': 'Largura', 'en-US': 'Width' }, geom_length: { 'pt-BR': 'Comprimento', 'en-US': 'Length' }, geom_height: { 'pt-BR': 'Altura', 'en-US': 'Height' }, geom_pitch: { 'pt-BR': 'Inclinação', 'en-US': 'Roof pitch' }, geom_clearance: { 'pt-BR': 'Distância do solo', 'en-US': 'Ground clearance' }, geom_diameter: { 'pt-BR': 'Diâmetro', 'en-US': 'Diameter' }, tab_geometry: { 'pt-BR': 'Geometria', 'en-US': 'Geometry' }, tab_norm: { 'pt-BR': 'NBR', 'en-US': 'NBR' }, tab_cpi: { 'pt-BR': 'Cpi', 'en-US': 'Cpi' }, tab_local: { 'pt-BR': 'Local', 'en-US': 'Location' }, tab_result: { 'pt-BR': 'Resultados', 'en-US': 'Results' }, wind_direction: { 'pt-BR': 'Direção do Vento', 'en-US': 'Wind Direction' }, wind_perpendicular: { 'pt-BR': '0° (Perpendicular à largura)', 'en-US': '0° (Perpendicular to width)' }, wind_parallel: { 'pt-BR': '90° (Paralelo à largura)', 'en-US': '90° (Parallel to width)' }, // === Cargas Lineares (M9.2) === linear_loads_title: { 'pt-BR': 'Cargas Lineares (M9.2)', 'en-US': 'Linear Loads (M9.2)' }, linear_loads_desc: { 'pt-BR': 'kN/m por barra para software estrutural (Ftool, SAP2000, Eberick, TQS).', 'en-US': 'kN/m per member for structural software (Ftool, SAP2000, etc).' }, linear_loads_frame_spacing: { 'pt-BR': 'Espaçamento entre pórticos (m)', 'en-US': 'Frame spacing (m)' }, linear_loads_purlin_spacing: { 'pt-BR': 'Espaçamento entre terças (m)', 'en-US': 'Purlin spacing (m)' }, linear_loads_frame_help: { 'pt-BR': 'Vão entre pórticos principais (eixo X)', 'en-US': 'Span between main frames (X axis)' }, linear_loads_purlin_help: { 'pt-BR': 'Distância entre terças no plano do telhado', 'en-US': 'Distance between purlins in roof plane' }, linear_loads_tab_pillars: { 'pt-BR': 'Pilares', 'en-US': 'Columns' }, linear_loads_tab_purlins: { 'pt-BR': 'Terças', 'en-US': 'Purlins' }, linear_loads_tab_reactions: { 'pt-BR': 'Reações', 'en-US': 'Reactions' }, linear_loads_pillar_windward: { 'pt-BR': 'Barlavento', 'en-US': 'Windward' }, linear_loads_pillar_leeward: { 'pt-BR': 'Sotavento', 'en-US': 'Leeward' }, linear_loads_pillar_side1: { 'pt-BR': 'Lateral 1', 'en-US': 'Side 1' }, linear_loads_pillar_side2: { 'pt-BR': 'Lateral 2', 'en-US': 'Side 2' }, linear_loads_sign_positive: { 'pt-BR': 'Sinal positivo = empuxo (empurrando o pilar para dentro). Sinal negativo = sucção (puxando para fora).', 'en-US': 'Positive sign = pressure (pushing the column inward). Negative sign = suction (pulling outward).' }, linear_loads_purlin_apply: { 'pt-BR': 'Cargas já com fator cos θ aplicado (terça é horizontal). Aplicar a barra como uniformemente distribuída no Ftool/SAP2000.', 'en-US': 'Loads already include cos θ factor (purlin is horizontal). Apply as uniformly distributed in Ftool/SAP2000.' }, linear_loads_reaction_base: { 'pt-BR': 'Reações na base dos pilares (kN) e momentos (kN·m)', 'en-US': 'Pillar base reactions (kN) and moments (kN·m)' }, linear_loads_total_reaction: { 'pt-BR': 'Reação total', 'en-US': 'Total reaction' }, linear_loads_warning_simplified: { 'pt-BR': 'Reações são estimativas simplificadas (pilar em balanço). Para pórticos com continuidade nos nós, usar software estrutural com análise elástica.', 'en-US': 'Reactions are simplified estimates (cantilever column). For frames with continuity at nodes, use structural software with elastic analysis.' }, // === Captura 3D (M9.3) === scene_capture_title: { 'pt-BR': 'Captura 3D (M9.3)', 'en-US': '3D Capture (M9.3)' }, scene_capture_desc: { 'pt-BR': 'Screenshot da cena 3D para incluir no PDF ou exportar isoladamente.', 'en-US': 'Screenshot of 3D scene for PDF or standalone export.' }, scene_capture_format: { 'pt-BR': 'Formato de Saída', 'en-US': 'Output Format' }, scene_capture_width: { 'pt-BR': 'Largura máxima (px)', 'en-US': 'Max width (px)' }, scene_capture_quality: { 'pt-BR': 'Qualidade JPEG', 'en-US': 'JPEG Quality' }, scene_capture_btn: { 'pt-BR': 'Capturar cena atual', 'en-US': 'Capture current scene' }, scene_capture_waiting: { 'pt-BR': 'Aguardando canvas...', 'en-US': 'Waiting for canvas...' }, scene_capture_capturing: { 'pt-BR': 'Capturando...', 'en-US': 'Capturing...' }, scene_capture_preview: { 'pt-BR': 'Preview', 'en-US': 'Preview' }, scene_capture_pdf_hint: { 'pt-BR': 'A imagem será incluída automaticamente no PDF quando você exportar após capturar.', 'en-US': 'The image is automatically included in the PDF when you export after capturing.' }, scene_capture_width_help: { 'pt-BR': '0 mantém resolução original do canvas. 1600 px é ideal para PDF A4.', 'en-US': '0 keeps the original canvas resolution. 1600 px is ideal for A4 PDF.' }, // === Ftool (M9.4) === ftool_title: { 'pt-BR': 'Exportar para Ftool (M9.4)', 'en-US': 'Export to Ftool (M9.4)' }, ftool_desc: { 'pt-BR': 'Pórtico 2D com nós, barras e cargas lineares para Ftool (PUC-Rio).', 'en-US': '2D frame with nodes, members and linear loads for Ftool (PUC-Rio).' }, ftool_content: { 'pt-BR': 'Conteúdo do arquivo .txt', 'en-US': 'Content of the .txt file' }, ftool_import_hint: { 'pt-BR': 'Import no Ftool: File → Import', 'en-US': 'Import in Ftool: File → Import' }, ftool_sign_convention: { 'pt-BR': 'Sinal de carga: positivo = na direção positiva do eixo Y (empuxo). Cargas de coluna em GlobalX (horizontal).', 'en-US': 'Load sign: positive = in the positive Y-axis direction (pressure). Column loads on GlobalX (horizontal).' }, ftool_download: { 'pt-BR': 'Baixar galpao_ftool.txt', 'en-US': 'Download galpao_ftool.txt' }, // === Auditoria LLM === audit_title: { 'pt-BR': 'Auditoria LLM', 'en-US': 'LLM Audit' }, audit_desc: { 'pt-BR': 'Validação dos cálculos de vento usando LLMs sequenciais. 100+ cenários cobrindo todos os módulos.', 'en-US': 'Wind calculation validation using sequential LLMs. 100+ scenarios covering all modules.' }, audit_run: { 'pt-BR': 'Executar Auditoria', 'en-US': 'Run Audit' }, audit_cancel: { 'pt-BR': 'Cancelar', 'en-US': 'Cancel' }, audit_export_pdf: { 'pt-BR': 'Exportar PDF', 'en-US': 'Export PDF' }, audit_provider: { 'pt-BR': 'Provedor', 'en-US': 'Provider' }, audit_model: { 'pt-BR': 'Modelo', 'en-US': 'Model' }, audit_apikey: { 'pt-BR': 'API Key', 'en-US': 'API Key' }, audit_baseurl: { 'pt-BR': 'Base URL (opcional)', 'en-US': 'Base URL (optional)' }, audit_generating: { 'pt-BR': 'Gerando cenários...', 'en-US': 'Generating scenarios...' }, audit_running: { 'pt-BR': 'Processando cenários...', 'en-US': 'Running scenarios...' }, audit_progress: { 'pt-BR': '{current} de {total}', 'en-US': '{current} of {total}' }, audit_no_apikey: { 'pt-BR': 'Informe uma API key para continuar.', 'en-US': 'Provide an API key to continue.' }, audit_results: { 'pt-BR': 'Resultados', 'en-US': 'Results' }, audit_summary: { 'pt-BR': '{passed}/{total} PASS — W:{warnings} F:{failed}', 'en-US': '{passed}/{total} PASS — W:{warnings} F:{failed}' }, audit_total: { 'pt-BR': 'Total', 'en-US': 'Total' }, audit_module: { 'pt-BR': 'Módulo', 'en-US': 'Module' }, audit_click_module: { 'pt-BR': 'Clique em um módulo para ver detalhes.', 'en-US': 'Click a module to view details.' }, audit_checks: { 'pt-BR': 'Verificações', 'en-US': 'Checks' }, audit_expected: { 'pt-BR': 'Esperado', 'en-US': 'Expected' }, audit_actual: { 'pt-BR': 'Obtido', 'en-US': 'Actual' }, audit_inputs: { 'pt-BR': 'Entradas', 'en-US': 'Inputs' }, audit_detail_title: { 'pt-BR': 'Detalhes do Cenário', 'en-US': 'Scenario Details' }, audit_copy_module: { 'pt-BR': 'Copiar para o módulo', 'en-US': 'Copy to module' }, audit_close: { 'pt-BR': 'Fechar', 'en-US': 'Close' }, audit_export_popup: { 'pt-BR': 'Exportar PDF deste cenário', 'en-US': 'Export PDF of this scenario' }, // === Home (App.tsx) === home_full_coverage: { 'pt-BR': 'Cobertura completa da norma', 'en-US': 'Full standard coverage' }, // === Idioma === language: { 'pt-BR': 'Idioma', 'en-US': 'Language' }, language_pt: { 'pt-BR': 'Português (BR)', 'en-US': 'Portuguese (BR)' }, language_en: { 'pt-BR': 'Inglês (EUA)', 'en-US': 'English (US)' }, // === Erros === error_generic: { 'pt-BR': 'Erro desconhecido', 'en-US': 'Unknown error' }, error_invalid_json: { 'pt-BR': 'JSON inválido', 'en-US': 'Invalid JSON' }, error_unknown_format: { 'pt-BR': 'Formato não reconhecido', 'en-US': 'Unknown format' }, }; /** Substitui {placeholder} por valores fornecidos. */ function interpolate(template: string, params?: Record): string { if (!params) return template; return template.replace(/\{(\w+)\}/g, (_, key) => { const v = params[key]; return v === undefined ? `{${key}}` : String(v); }); } /** Tradução pura (sem hook). */ export function t( key: string, locale: Locale = DEFAULT_LOCALE, params?: Record, ): string { const entry = translations[key]; if (entry) return interpolate(entry[locale] ?? entry[DEFAULT_LOCALE] ?? key, params); // Fallback: retorna a chave return params ? interpolate(key, params) : key; } /** Lista todas as chaves disponíveis (útil para debug). */ export function listKeys(): string[] { return Object.keys(translations).sort(); } /** Detecta locale preferido do navegador. */ export function detectBrowserLocale(): Locale { if (typeof navigator === 'undefined') return DEFAULT_LOCALE; const lang = navigator.language; if (lang.startsWith('pt')) return 'pt-BR'; if (lang.startsWith('en')) return 'en-US'; return DEFAULT_LOCALE; }