Merge pull request #82 from edurbs/feat/i18n-portuguese

i18n: Brazilian Portuguese interface language (#79)
This commit is contained in:
Brandon Thomas 2026-10-10 13:25:35 -04:00
commit e72e16a0ec
8 changed files with 299 additions and 6 deletions

View File

@ -25,3 +25,6 @@ sync_github_name = false
[people.nihilist-narwhal]
display_name = "nihilist-narwhal"
sync_github_name = false
[people.edurbs]
sync_github_name = true

View File

@ -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|zh|ja|ko|ru|pt]
//! [--dialog 'name={json}'] out.png`
use gridcraft_engine::Session;

View File

@ -163,7 +163,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -
("view.theme", "Display Theme", ""),
("view.zoom100", "100%", ""),
("ui.dialog", "Open Dialog", ""),
("app.language.set", "Interface Language", "{language: \"en\"|\"zh\"|\"ja\"|\"ko\"|\"ru\"} (alias: code)"),
("app.language.set", "Interface Language", "{language: \"en\"|\"zh\"|\"ja\"|\"ko\"|\"ru\"|\"pt\"} (alias: code)"),
("app.language.english", "Interface Language: English", ""),
("app.language.japanese", "Interface Language: Japanese", ""),
] {

View File

@ -10,6 +10,7 @@
use serde::{Deserialize, Serialize};
mod ko;
mod pt;
mod ru;
mod zh;
@ -22,10 +23,12 @@ pub enum Language {
Ja,
Ko,
Ru,
#[serde(rename = "pt")]
PtBr,
}
impl Language {
pub const ALL: [Self; 5] = [Self::En, Self::Zh, Self::Ja, Self::Ko, Self::Ru];
pub const ALL: [Self; 6] = [Self::En, Self::Zh, Self::Ja, Self::Ko, Self::Ru, Self::PtBr];
/// The language's own name, shown in the switcher.
pub fn name(self) -> &'static str {
@ -35,6 +38,7 @@ impl Language {
Self::Ja => "日本語",
Self::Ko => "한국어",
Self::Ru => "Русский",
Self::PtBr => "Português (Brasil)",
}
}
@ -46,6 +50,7 @@ impl Language {
Self::Ja => "ja",
Self::Ko => "ko",
Self::Ru => "ru",
Self::PtBr => "pt",
}
}
@ -60,6 +65,7 @@ impl Language {
"ja" => Some(Self::Ja),
"ko" => Some(Self::Ko),
"ru" => Some(Self::Ru),
"pt" => Some(Self::PtBr),
_ => None,
}
}
@ -88,6 +94,7 @@ impl Language {
Self::Zh => zh::translate(text),
Self::Ko => ko::translate(text),
Self::Ru => ru::translate(text),
Self::PtBr => pt::translate(text),
};
translated.unwrap_or_else(|| text.rsplit_once('|').map_or(text, |(_, label)| label))
}
@ -418,11 +425,15 @@ mod tests {
assert!(zh::translate(english).is_some_and(|text| !text.is_empty()), "missing Simplified Chinese: {english}");
assert!(ko::translate(english).is_some_and(|text| !text.is_empty()), "missing Korean: {english}");
assert!(ru::translate(english).is_some_and(|text| !text.is_empty()), "missing Russian: {english}");
assert!(pt::translate(english).is_some_and(|text| !text.is_empty()), "missing Portuguese: {english}");
}
assert_eq!(Language::Ja.tr("Data"), "データ");
assert_eq!(Language::Zh.tr("Data"), "数据");
assert_eq!(Language::Ko.tr("Data"), "데이터");
assert_eq!(Language::Ru.tr("Data"), "Данные");
assert_eq!(Language::PtBr.tr("Data"), "Dados");
assert_eq!(Language::PtBr.tr("Date"), "Data");
assert_eq!(Language::PtBr.tr("Sheet1!A1"), "Sheet1!A1");
assert_eq!(Language::Ja.tr("Sheet1!A1"), "Sheet1!A1");
assert_eq!(Language::Zh.tr("Sheet1!A1"), "Sheet1!A1");
assert_eq!(Language::Ko.tr("Sheet1!A1"), "Sheet1!A1");
@ -457,6 +468,9 @@ mod tests {
assert_eq!(Language::from_tag("ko-KR"), Some(Language::Ko));
assert_eq!(Language::from_tag("RU_ru"), Some(Language::Ru));
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]

View File

@ -0,0 +1,274 @@
//! Brazilian Portuguese translations for the interface strings in i18n.rs. Labels follow Excel's
//! pt-BR wording; anything absent falls back to English.
pub(super) fn translate(text: &str) -> Option<&'static str> {
Some(match text {
"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",
"Page Layout|Orientation" => "Orientação",
"Display theme" => "Tema de exibição",
"System" => "Sistema",
"Light" => "Claro",
"Dark" => "Escuro",
"Sheet Options|View" => "Exibir",
"Print" => "Imprimir",
_ => return None,
})
}

View File

@ -303,7 +303,7 @@ impl SheetApp {
self.ui.language = l;
Ok(json!({"language": l}))
}
None => Err(format!("unknown language {code:?} (use \"en\", \"zh\", \"ja\", \"ko\" or \"ru\")")),
None => Err(format!("unknown language {code:?} (use \"en\", \"zh\", \"ja\", \"ko\", \"ru\" or \"pt\")")),
}
}
"app.language.english" => {

View File

@ -587,7 +587,7 @@ mod tests {
assert_eq!(keys(HanOrder::Japanese), ["sys-cjk-han-ja", "sys-cjk-han-zh", "sys-cjk-hangul", "sys-thai", "sys-hebrew"]);
assert_eq!(keys(HanOrder::Chinese), ["sys-cjk-han-zh", "sys-cjk-han-ja", "sys-cjk-hangul", "sys-thai", "sys-hebrew"]);
assert_eq!(HanOrder::of(Language::Ja), HanOrder::Japanese);
for l in [Language::En, Language::Zh, Language::Ko, Language::Ru] {
for l in [Language::En, Language::Zh, Language::Ko, Language::Ru, Language::PtBr] {
assert_eq!(HanOrder::of(l), HanOrder::Chinese, "{l:?} shares the default order (no font rebuild)");
}
}

View File

@ -199,7 +199,9 @@ fn language_switch_translates_the_ribbon_and_persists() {
for id in ["app.language.set", "app.language.english", "app.language.japanese"] {
assert!(ids.contains(&id), "engine.commands lists {id}");
}
for (language, expected) in [(Language::Zh, "数据"), (Language::Ja, "データ"), (Language::Ko, "데이터"), (Language::Ru, "Данные")] {
for (language, expected) in
[(Language::Zh, "数据"), (Language::Ja, "データ"), (Language::Ko, "데이터"), (Language::Ru, "Данные"), (Language::PtBr, "Dados")]
{
h.state_mut().run("app.language.set", json!({"language": language.code()})).unwrap();
h.run_steps(2); // every locale renders the ribbon without panicking
assert_eq!(h.state().ui.language, language);