From f424d7d56b26fa72ac33c550fac418f5168f0ecc Mon Sep 17 00:00:00 2001 From: Eduardo Soares Date: Fri, 9 Oct 2026 18:18:57 -0300 Subject: [PATCH] i18n: Brazilian Portuguese interface language (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds PtBr to the interface-language system from #31: a PORTUGUESE table covering the same 259 chrome strings as JAPANESE (a test keeps the two tables in step), Excel's pt-BR wording throughout, "pt"/"pt-br" codes (serde-stored as "pt"), Português (Brasil) in the switcher, the app.language.portuguese command, and first-run detection of a pt-BR system locale via the existing tag reduction. --- contributors/people.toml | 3 + crates/ui-egui/examples/snapshot.rs | 2 +- crates/ui-egui/src/i18n.rs | 303 +++++++++++++++++++++++++++- crates/ui-egui/src/lib.rs | 8 +- crates/ui-egui/src/ribbon.rs | 6 +- 5 files changed, 311 insertions(+), 11 deletions(-) diff --git a/contributors/people.toml b/contributors/people.toml index 89c6db2..ea52018 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -21,3 +21,6 @@ real_name = "Brandon Thomas" display_name = "Brandon Thomas" sync_github_name = false + +[people.edurbs] +sync_github_name = true diff --git a/crates/ui-egui/examples/snapshot.rs b/crates/ui-egui/examples/snapshot.rs index f384d18..c160471 100644 --- a/crates/ui-egui/examples/snapshot.rs +++ b/crates/ui-egui/examples/snapshot.rs @@ -1,7 +1,7 @@ //! Renders the whole GridCraft window offscreen (wgpu, no window) to a PNG. //! //! `cargo run --release -p gridcraft-ui-egui --example snapshot -- [--sample sales] [--in file.xlsx] -//! [--size 1440x900] [--scale 2] [--cmd 'id={json}']... [--tab Insert] [--dark] [--language ja] +//! [--size 1440x900] [--scale 2] [--cmd 'id={json}']... [--tab Insert] [--dark] [--language en|ja|pt] //! [--dialog 'name={json}'] out.png` use gridcraft_engine::Session; diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 48e809d..a1db066 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -15,16 +15,19 @@ pub enum Language { #[default] En, Ja, + #[serde(rename = "pt")] + PtBr, } impl Language { - pub const ALL: [Self; 2] = [Self::En, Self::Ja]; + pub const ALL: [Self; 3] = [Self::En, Self::Ja, Self::PtBr]; /// The language's own name, shown in the switcher. pub fn name(self) -> &'static str { match self { Self::En => "English", Self::Ja => "日本語", + Self::PtBr => "Português (Brasil)", } } @@ -32,6 +35,7 @@ impl Language { match code { "en" => Some(Self::En), "ja" => Some(Self::Ja), + "pt" | "pt-br" => Some(Self::PtBr), _ => None, } } @@ -57,12 +61,12 @@ impl Language { } pub fn tr(self, text: &str) -> &str { - if self == Self::Ja - && let Some((_, japanese)) = JAPANESE.iter().find(|(english, _)| *english == text) - { - return japanese; - } - text + let table: &[(&str, &str)] = match self { + Self::En => &[], + Self::Ja => JAPANESE, + Self::PtBr => PORTUGUESE, + }; + table.iter().find(|(english, _)| *english == text).map_or(text, |(_, translated)| translated) } } @@ -346,6 +350,271 @@ const JAPANESE: &[(&str, &str)] = &[ ("More\nFunctions", "その他の関数"), ]; +/// English label → Brazilian Portuguese, keyed like [`JAPANESE`] so the tables stay in step (a test +/// enforces it) and the diff reads against the Japanese column. Labels follow Excel's pt-BR wording; +/// anything absent falls back to English. +const PORTUGUESE: &[(&str, &str)] = &[ + ("Paste", "Colar"), + ("Paste Values", "Colar Valores"), + ("Paste Formulas", "Colar Fórmulas"), + ("Paste Formatting", "Colar Formatação"), + ("Transpose", "Transpor"), + ("Paste Link", "Colar Vínculo"), + ("Paste Special…", "Colar Especial…"), + ("Cut (⌘X)", "Recortar (⌘X)"), + ("Copy (⌘C)", "Copiar (⌘C)"), + ("Format Painter (double-click to keep it on)", "Pincel de Formatação (clique duas vezes para mantê-lo ativo)"), + ("Bold (⌘B)", "Negrito (⌘B)"), + ("Italic (⌘I)", "Itálico (⌘I)"), + ("Underline (⌘U)", "Sublinhado (⌘U)"), + ("Double Underline", "Sublinhado Duplo"), + ("Strikethrough", "Riscado"), + ("Underline", "Sublinhado"), + ("Increase Font Size", "Aumentar Tamanho da Fonte"), + ("Decrease Font Size", "Diminuir Tamanho da Fonte"), + ("Wrap Text", "Quebrar Texto Automaticamente"), + ("Merge & Center", "Mesclar e Centralizar"), + ("Merge Cells", "Mesclar Células"), + ("Merge Across", "Mesclar Através"), + ("Unmerge Cells", "Desmesclar Células"), + ("Fill Color", "Cor de Preenchimento"), + ("Font Color", "Cor da Fonte"), + ("Borders", "Bordas"), + ("Top\nAlign", "Alinhar\nno Topo"), + ("Middle\nAlign", "Alinhar\nno Meio"), + ("Bottom\nAlign", "Alinhar\nem Baixo"), + ("Left", "Esquerda"), + ("Right", "Direita"), + ("Center", "Centro"), + ("Horizontal", "Horizontal"), + ("Vertical Text", "Texto Vertical"), + ("Decrease Indent", "Diminuir Recuo"), + ("Increase Indent", "Aumentar Recuo"), + ("Orientation", "Orientação"), + ("Angle Counterclockwise", "Ângulo Anti-Horário"), + ("Angle Clockwise", "Ângulo Horário"), + ("Rotate Text Up", "Girar Texto para Cima"), + ("Rotate Text Down", "Girar Texto para Baixo"), + ("Accounting Number Format", "Formato de Número Contábil"), + ("Percent Style", "Estilo de Porcentagem"), + ("Comma Style", "Estilo de Vírgula"), + ("Increase Decimal", "Aumentar Casas Decimais"), + ("Decrease Decimal", "Diminuir Casas Decimais"), + ("More Accounting Formats…", "Mais Formatos Contábeis…"), + ("Conditional\nFormatting", "Formatação\nCondicional"), + ("Conditional Formatting", "Formatação Condicional"), + ("Format\nas Table", "Formatar\ncomo Tabela"), + ("Cell\nStyles", "Estilos\nde Célula"), + ("Format Cells…", "Formatar Células…"), + ("Format Cell Alignment…", "Formatar Alinhamento de Células…"), + ("Format\nPane", "Painel de\nFormatação"), + ("Format", "Formatar"), + ("Insert", "Inserir"), + ("Insert Cells…", "Inserir Células…"), + ("Delete", "Excluir"), + ("Delete Cells…", "Excluir Células…"), + ("Insert Sheet Rows", "Inserir Linhas de Planilha"), + ("Insert Sheet Columns", "Inserir Colunas de Planilha"), + ("Delete Sheet Rows", "Excluir Linhas de Planilha"), + ("Delete Sheet Columns", "Excluir Colunas de Planilha"), + ("Row Height…", "Altura da Linha…"), + ("AutoFit Row Height", "Ajustar Altura da Linha"), + ("Column Width…", "Largura da Coluna…"), + ("AutoFit Column Width", "Ajustar Largura da Coluna"), + ("Default Width…", "Largura Padrão…"), + ("Hide Rows", "Ocultar Linhas"), + ("Unhide Rows", "Reexibir Linhas"), + ("Hide Columns", "Ocultar Colunas"), + ("Unhide Columns", "Reexibir Colunas"), + ("Lock Cell", "Bloquear Célula"), + ("Clear", "Limpar"), + ("Clear All", "Limpar Tudo"), + ("Clear Formats", "Limpar Formatos"), + ("Clear Contents", "Limpar Conteúdo"), + ("Clear Comments and Notes", "Limpar Comentários e Notas"), + ("Clear Hyperlinks", "Limpar Hiperlinks"), + ("AutoSum", "Soma Automática"), + ("AutoSum (⌘⇧T)", "Soma Automática (⌘⇧T)"), + ("Flash\nFill", "Preenchimento\nRelâmpago"), + ("Flash Fill", "Preenchimento Relâmpago"), + ("Fill", "Preencher"), + ("Clear Print Area", "Limpar Área de Impressão"), + ("Find &\nSelect", "Localizar e\nSelecionar"), + ("Find…", "Localizar…"), + ("Replace…", "Substituir…"), + ("Go To…", "Ir Para…"), + ("Go To Special…", "Ir Para Especial…"), + ("PivotTable", "Tabela Dinâmica"), + ("Recommended\nCharts", "Gráficos\nRecomendados"), + ("Table", "Tabela"), + ("Pictures", "Imagens"), + ("Shapes", "Formas"), + ("Icons", "Ícones"), + ("Text\nBox", "Caixa\nde Texto"), + ("Comment", "Comentário"), + ("New\nComment", "Novo\nComentário"), + ("New Note", "Nova Nota"), + ("Show/Hide Note", "Mostrar/Ocultar Nota"), + ("Header &\nFooter", "Cabeçalho e\nRodapé"), + ("Text to\nColumns", "Texto para\nColunas"), + ("Link", "Vínculo"), + ("Symbol", "Símbolo"), + ("Sparklines", "Minigráficos"), + ("Header & Footer", "Cabeçalho e Rodapé"), + ("Draw with ink", "Desenhar com Tinta"), + ("Eraser", "Borracha"), + ("Ink to\nShape", "Tinta para\nForma"), + ("Select\nObjects", "Selecionar\nObjetos"), + ("Themes", "Temas"), + ("Margins", "Margens"), + ("Size", "Tamanho"), + ("Print\nArea", "Área de\nImpressão"), + ("Set Print Area", "Definir Área de Impressão"), + ("Breaks", "Quebras"), + ("Insert Page Break", "Inserir Quebra de Página"), + ("Remove Page Break", "Remover Quebra de Página"), + ("Reset All Page Breaks", "Redefinir Todas as Quebras de Página"), + ("Print\nTitles", "Imprimir\nTítulos"), + ("Narrow", "Estreita"), + ("Wide", "Ampla"), + ("Normal", "Normal"), + ("Portrait", "Retrato"), + ("Landscape", "Paisagem"), + ("Custom Margins…", "Margens Personalizadas…"), + ("Insert\nFunction", "Inserir\nFunção"), + ("Name\nManager", "Gerenciador\nde Nomes"), + ("Define Name", "Definir Nome"), + ("Use in Formula", "Usar na Fórmula"), + ("Create from Selection", "Criar a Partir da Seleção"), + ("Trace Precedents", "Rastrear Precedentes"), + ("Trace Dependents", "Rastrear Dependentes"), + ("Remove Arrows", "Remover Setas"), + ("Show Formulas", "Exibir Fórmulas"), + ("Error Checking", "Verificação de Erros"), + ("Evaluate Formula", "Avaliar Fórmula"), + ("Watch Window", "Janela de Inspeção"), + ("Calculation\nOptions", "Opções de\nCálculo"), + ("Calculate Now", "Calcular Agora"), + ("Calculate Sheet", "Calcular Planilha"), + ("Automatic", "Automático"), + ("Automatic Except for Data Tables", "Automático Exceto Tabelas de Dados"), + ("Manual", "Manual"), + ("Sort &\nFilter", "Classificar e\nFiltrar"), + ("Sort", "Classificar"), + ("Sort A to Z", "Classificar de A a Z"), + ("Sort Z to A", "Classificar de Z a A"), + ("Custom Sort…", "Classificação Personalizada…"), + ("Filter", "Filtrar"), + ("Reapply", "Aplicar Novamente"), + ("Get Data\n(Text/CSV)", "Obter Dados\n(Texto/CSV)"), + ("Remove\nDuplicates", "Remover\nDuplicatas"), + ("Data\nValidation", "Validação\nde Dados"), + ("Data Validation", "Validação de Dados"), + ("Data Validation…", "Validação de Dados…"), + ("Circle Invalid Data", "Circundar Dados Inválidos"), + ("Clear Validation", "Limpar Validação"), + ("Group", "Agrupar"), + ("Ungroup", "Desagrupar"), + ("Subtotal", "Subtotal"), + ("What-If\nAnalysis", "Análise de\nHipóteses"), + ("Data Table…", "Tabela de Dados…"), + ("Scenario Manager…", "Gerenciador de Cenários…"), + ("Goal Seek…", "Buscar Objetivo…"), + ("Spelling", "Ortografia"), + ("Check\nAccessibility", "Verificar\nAcessibilidade"), + ("Threaded Comments", "Comentários Encadeados"), + ("Show\nComments", "Mostrar\nComentários"), + ("Protect\nWorkbook", "Proteger\nPasta de Trabalho"), + ("Unprotect\nSheet", "Desproteger\nPlanilha"), + ("Protect Sheet…", "Proteger Planilha…"), + ("Comments", "Comentários"), + ("Notes", "Notas"), + ("Page Break\nPreview", "Visualização\nde Quebras de Página"), + ("Page\nLayout", "Layout\nde Página"), + ("Formula Bar", "Barra de Fórmulas"), + ("Gridlines", "Linhas de Grade"), + ("Headings", "Títulos"), + ("Zoom", "Zoom"), + ("100%", "100%"), + ("Zoom to\nSelection", "Zoom na\nSeleção"), + ("Freeze\nPanes", "Congelar\nPainéis"), + ("Freeze Panes", "Congelar Painéis"), + ("Freeze Top Row", "Congelar Linha Superior"), + ("Freeze First Column", "Congelar Primeira Coluna"), + ("Unfreeze Panes", "Descongelar Painéis"), + ("Dark Mode", "Modo Escuro"), + ("Interface language", "Idioma da interface"), + ("Command\nPalette", "Paleta de\nComandos"), + ("Agent\nControl", "Controle\ndo Agente"), + ("Action\nJournal", "Registro\nde Ações"), + ("About\nGridCraft", "Sobre o\nGridCraft"), + ("Share", "Compartilhar"), + ("Add Chart\nElement", "Adicionar\nElemento de Gráfico"), + ("Change\nChart Type", "Alterar\nTipo de Gráfico"), + ("Switch\nRow/Column", "Alternar\nLinha/Coluna"), + ("Data Labels: None", "Rótulos de Dados: Nenhum"), + ("Data Labels: Show", "Rótulos de Dados: Exibir"), + ("Legend: None", "Legenda: Nenhuma"), + ("Legend: Bottom", "Legenda: Abaixo"), + ("Legend: Top", "Legenda: Acima"), + ("Legend: Right", "Legenda: À Direita"), + ("Gridlines: None", "Linhas de Grade: Nenhuma"), + ("Gridlines: Show", "Linhas de Grade: Exibir"), + ("Chart\nTitle", "Título\ndo Gráfico"), + ("Delete\nChart", "Excluir\nGráfico"), + ("Convert\nto Range", "Converter\nem Intervalo"), + ("Table\nStyles", "Estilos\nde Tabela"), + ("Top 10 Items", "10 Primeiros Itens"), + ("Top 10%", "10% Primeiros"), + ("Bottom 10 Items", "10 Últimos Itens"), + ("Bottom 10%", "10% Últimos"), + ("Above Average", "Acima da Média"), + ("Below Average", "Abaixo da Média"), + ("Clear Rules from Entire Sheet", "Limpar Regras de Toda a Planilha"), + ("Clear Rules from Selected Cells", "Limpar Regras das Células Selecionadas"), + ("Constants", "Constantes"), + ("Data Bars", "Barras de Dados"), + ("Color Scales", "Escalas de Cores"), + ("Icon Sets", "Conjuntos de Ícones"), + ("Home", "Página Inicial"), + ("Draw", "Desenhar"), + ("Page Layout", "Layout da Página"), + ("Formulas", "Fórmulas"), + ("Data", "Dados"), + ("Review", "Revisão"), + ("View", "Exibir"), + ("Automate", "Automatizar"), + ("Table Design", "Design de Tabela"), + ("Chart Design", "Design de Gráfico"), + ("Ready", "Pronto"), + ("Enter", "Inserir"), + ("Edit", "Editar"), + ("Point", "Apontar"), + ("Calculate", "Calcular"), + ("Select destination and press Enter or choose Paste", "Selecione o destino e pressione Enter ou escolha Colar"), + ("General", "Geral"), + ("Number", "Número"), + ("Currency", "Moeda"), + ("Accounting", "Contábil"), + ("Date", "Data"), + ("Time", "Hora"), + ("Percentage", "Porcentagem"), + ("Fraction", "Fração"), + ("Scientific", "Científico"), + ("Text", "Texto"), + ("Special", "Especial"), + ("Custom", "Personalizado"), + ("Short Date", "Data Abreviada"), + ("Long Date", "Data Por Extenso"), + ("More Number Formats…", "Mais Formatos de Número…"), + ("Financial", "Financeira"), + ("Logical", "Lógica"), + ("Date &\nTime", "Data e\nHora"), + ("Lookup &\nReference", "Pesquisa e\nReferência"), + ("Math &\nTrig", "Matemática e\nTrigonometria"), + ("More\nFunctions", "Mais\nFunções"), +]; + #[cfg(test)] mod tests { use super::*; @@ -357,8 +626,21 @@ mod tests { assert!(JAPANESE.iter().take(i).all(|(other, _)| en != other)); assert_eq!(Language::En.tr(en), *en); } + for (i, (en, pt)) in PORTUGUESE.iter().enumerate() { + assert!(!pt.is_empty()); + assert!(PORTUGUESE.iter().take(i).all(|(other, _)| en != other)); + assert_eq!(Language::En.tr(en), *en); + } + // The tables cover the same interface strings, so per-language coverage stays even. + for (en, _) in PORTUGUESE { + assert!(JAPANESE.iter().any(|(other, _)| en == other), "{en:?} missing from JAPANESE"); + } + assert_eq!(JAPANESE.len(), PORTUGUESE.len()); assert_eq!(Language::Ja.tr("Data"), "データ"); + assert_eq!(Language::PtBr.tr("Data"), "Dados"); + assert_eq!(Language::PtBr.tr("Date"), "Data"); assert_eq!(Language::Ja.tr("Sheet1!A1"), "Sheet1!A1"); + assert_eq!(Language::PtBr.tr("Sheet1!A1"), "Sheet1!A1"); assert_eq!(Language::parse("xx"), None); } @@ -367,6 +649,9 @@ mod tests { assert_eq!(Language::from_tag("ja-JP"), Some(Language::Ja)); assert_eq!(Language::from_tag("en_US"), Some(Language::En)); assert_eq!(Language::from_tag("de-DE"), None); + assert_eq!(Language::from_tag("pt-BR"), Some(Language::PtBr)); + assert_eq!(Language::parse("pt"), Some(Language::PtBr)); + assert_eq!(Language::parse("pt-br"), Some(Language::PtBr)); } #[test] @@ -374,8 +659,10 @@ mod tests { let mut app = crate::SheetApp::new(gridcraft_engine::Session::default(), Default::default()); app.run("app.language.japanese", serde_json::json!({})).unwrap(); assert_eq!(app.ui.language, Language::Ja); + app.run("app.language.portuguese", serde_json::json!({})).unwrap(); + assert_eq!(app.ui.language, Language::PtBr); let restored: crate::UiState = serde_json::from_str(&serde_json::to_string(&app.ui).unwrap()).unwrap(); - assert_eq!(restored.language, Language::Ja); + assert_eq!(restored.language, Language::PtBr); app.run("app.language.english", serde_json::json!({})).unwrap(); assert_eq!(app.ui.language, Language::En); } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 4e7a383..f41ad36 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -199,7 +199,9 @@ impl SheetApp { self.ui.language = l; Ok(json!({"language": l})) } - None => Err(format!("unknown language {:?} (use \"en\" or \"ja\")", p.get("language").and_then(Json::as_str).unwrap_or(""))), + None => { + Err(format!("unknown language {:?} (use \"en\", \"ja\" or \"pt\")", p.get("language").and_then(Json::as_str).unwrap_or(""))) + } } } "app.language.english" => { @@ -210,6 +212,10 @@ impl SheetApp { self.ui.language = i18n::Language::Ja; Ok(json!({"language": i18n::Language::Ja})) } + "app.language.portuguese" => { + self.ui.language = i18n::Language::PtBr; + Ok(json!({"language": i18n::Language::PtBr})) + } "ui.dialog" => { let name = p.get("name").and_then(Json::as_str).unwrap_or(""); self.open_dialog(name, p.clone()); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 52bc34b..0dbc885 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1588,7 +1588,11 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { |ui| { for l in crate::i18n::Language::ALL { if ui.selectable_label(l == lang, l.name()).clicked() { - let code = if l == crate::i18n::Language::Ja { "app.language.japanese" } else { "app.language.english" }; + let code = match l { + crate::i18n::Language::En => "app.language.english", + crate::i18n::Language::Ja => "app.language.japanese", + crate::i18n::Language::PtBr => "app.language.portuguese", + }; act(app, code, json!({})); } }