feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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': 'VentoApp', 'en-US': 'VentoApp' },
|
||||
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': 'Configurações', 'en-US': 'Settings' },
|
||||
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' },
|
||||
|
||||
// === 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;
|
||||
}
|
||||
Reference in New Issue
Block a user