Files
BrainWind/app/src/lib/i18n.ts
T

255 lines
15 KiB
TypeScript

/**
* 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<string, Record<Locale, string>>;
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, string | number>): 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, string | number>,
): 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;
}