i18n: add Brazilian Portuguese (pt-br) UI translation

Add pt-br as a fifth interface language beside English, Simplified
Chinese, Traditional Chinese (Taiwan) and Japanese.

`crates/ui-egui/locales/pt-br.json` and `pt-br-formats.json` carry the
messages and the format strings; the language table gains the entry
(`PtBr, "pt-br", "Português (Brasil)", "Latn", ...`), and Edit > Language
plus `app.language.portuguese` select it. Locale tags pt-BR, pt_BR.UTF-8
and pt all normalize to the same language.

Translation follows pt-BR UI conventions (Adobe terms: revelação,
balanço de branco, máscara, cópia virtual…); proper nouns and file
formats stay as written (LightCraft, ArtCraft, GitHub, DNG, TIFF, XMP).
Placeholders, format specs and punctuation match the English source; the
English plural suffix is consumed with `{:.0}` where Portuguese does not
need it.

Tests: every_language_has_a_catalog_covering_the_source,
catalogs_agree_on_placeholders_and_report_gaps, locale_tags_normalize
and the painted-window test now cover pt-br. Docs: localization.md, a
new localization-pt-br.md and the parity rows.

Co-Authored-By: DeepSeek V4 Flash (opencode) <noreply@opencode.ai>
This commit is contained in:
RicSchonfelderandDeepSeek V4 Flash committed 2026-10-07 17:03:20 -03:00
1 parent b35487add2
commit fb01966a00
8 files changed
+1439 -6

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
{
" ({} in Recently Deleted)": " ({} em Excluídos recentemente)",
" · {skipped} skipped (file exists)": " · {skipped} ignorada(s) (arquivo existente)",
" · {} duplicate{} skipped": " · {} duplicada{:.0} ignorada",
" · {} failed: {}": " · {} falhou: {}",
" · {} moved": " · {} movida(s)",
" · {} not readable": " · {} não legível",
", {failed} file{} not readable": ", {failed} arquivo{:.0} não legível",
"Add {local_n} to My Photos": "Adicionar {local_n} a Minhas fotos",
"Add {} Photo{}": "Adicionar {} foto{:.0}",
"Add “{}” to Favorites": "Adicionar “{}” aos favoritos",
"Added {n} photo{} to My Photos": "Adicionada {n} foto{:.0} a Minhas fotos",
"Added {n} photo{} to “{}”": "Adicionada {n} foto{:.0} a “{}”",
"Adding photos… {} of {}": "Adicionando fotos… {} de {}",
"All {n} selected photos move by the same amount.": "Todas as {n} fotos selecionadas se movem pela mesma quantidade.",
"Already in the library, in Recently Deleted: {} photo{} selected · right-click ▸ Restore, or Delete Permanently to import afresh": "Já na biblioteca, em Excluídos recentemente: {} foto{:.0} selecionada · clique com o botão direito ▸ Restaurar, ou Excluir permanentemente para importar de novo",
"Already in the library: {} photo{} selected in All Photos": "Já na biblioteca: {} foto{:.0} selecionada em Todas as fotos",
"Auto ({})": "Automático ({})",
"Auto Sync: edits apply to {n} photos": "Sincronização automática: edições aplicadas a {n} fotos",
"Automatic ({} MB)": "Automático ({} MB)",
"Build 1:1 Previews ({scope})": "Gerar pré-visualizações 1:1 ({scope})",
"Build Smart Previews ({scope})": "Gerar pré-visualizações inteligentes ({scope})",
"Build Standard-Sized Previews ({scope})": "Gerar pré-visualizações em tamanho padrão ({scope})",
"Click {}:1": "Clique em {}:1",
"Create {n} Virtual Copies": "Criar {n} cópias virtuais",
"Decoder: {reason}": "Decodificador: {reason}",
"Delete \"{label}\"": "Excluir \"{label}\"",
"Delete \"{label}\" (and the mask)": "Excluir \"{label}\" (e a máscara)",
"Delete {n} Photos": "Excluir {n} fotos",
"Delete “{current}”": "Excluir “{current}”",
"Discard Smart Previews ({scope})": "Descartar pré-visualizações inteligentes ({scope})",
"Duplicate \"{label}\"": "Duplicar \"{label}\"",
"Example: {example}": "Exemplo: {example}",
"Exported {n} photo{}": "Exportada {n} foto{:.0}",
"Exported {ok} of {total} photo{}": "Exportadas {ok} de {total} foto{:.0}",
"Exported {} point curve preset{}": "Exportadas {} predefinição{:.0} de curva de pontos",
"Exported {} preset{}": "Exportadas {} predefinição{:.0}",
"Exporting {} of {total}": "Exportando {} de {total}",
"Filter bar — {active} active filter{}": "Barra de filtros — {active} filtro{:.0} ativo",
"Healed {n} dust spot{}": "Recuperadas {n} mancha{:.0} de poeira",
"Import cancelled · {} photo{} added": "Importação cancelada · {} foto{:.0} adicionada",
"Imported {n} point curve preset{}": "Importadas {n} predefinição{:.0} de curva de pontos",
"Imported {n} preset{}": "Importadas {n} predefinição{:.0}",
"Imported {n} preset{}, {f} file{} not readable": "Importadas {n} predefinição{:.0}, {f} arquivo{:.0} não legível",
"Imported {} photo{}": "Importadas {} foto{:.0}",
"Label Set: {name}": "Conjunto de rótulos: {name}",
"Matches: {}": "Correspondências: {}",
"Merging… {stage} {:.0}%": "Mesclando… {stage} {:.0}%",
"Move {what} to Recently Deleted?": "Mover {what} para Excluídos recentemente?",
"Named ({named_n})": "Nomeadas ({named_n})",
"Paste into {n} photo{} — only settings that were copied are pasted": "Colar em {n} foto{:.0} — somente os ajustes copiados são colados",
"Preset: {name}": "Predefinição: {name}",
"Preview only — editing the camera's embedded JPEG ({})": "Somente pré-visualização — editando o JPEG incorporado da câmera ({})",
"Preview only — {}": "Somente pré-visualização — {}",
"Profile: {}": "Perfil: {}",
"Proof Copy ({})": "Cópia de prova ({})",
"Proof Preview · {}": "Pré-visualização de prova · {}",
"Quick Develop · {n} photos": "Revelação rápida · {n} fotos",
"Quick Develop: {name}": "Revelação rápida: {name}",
"Rated {}": "Avaliada {}",
"Reading photos… {done} of {total}": "Lendo fotos… {done} de {total}",
"Reading photos… {} of {}": "Lendo fotos… {} de {}",
"Redo {}": "Refazer {}",
"Remove “{}” from Favorites": "Remover “{}” dos favoritos",
"Rename {n} Photos…": "Renomear {n} fotos…",
"Rename “{name}”": "Renomear “{name}”",
"Rendering on the CPU: {why}": "Renderizando na CPU: {why}",
"Reset {name}": "Redefinir {name}",
"Reset {} Channel": "Redefinir canal {}",
"Saved export preset “{}”": "Predefinição de exportação “{}” salva",
"Show {hidden} hidden location{}": "Mostrar {hidden} local{:.0} oculto",
"Showing {n} of {}": "Mostrando {n} de {}",
"Smart album: {}": "Álbum inteligente: {}",
"Undo {}": "Desfazer {}",
"Updated “{name}”": "“{name}” atualizado",
"Version {} — a clean-room, pure-Rust photo library and raw developer.": "Versão {} — uma biblioteca de fotos e revelador RAW em Rust puro, desenvolvida de forma independente.",
"Version: {}": "Versão: {}",
"none (CPU rendering): {why}": "nenhum (renderização na CPU): {why}",
"the {n} selected photos": "as {n} fotos selecionadas",
"the {} photos in view": "as {} fotos em exibição",
"these {count} photos": "estas {count} fotos",
"{count} smart preview{} · {:.1} MB": "{count} pré-visualizações{:.0} · {:.1} MB",
"{f} folders (and their subfolders)": "{f} pastas (e suas subpastas)",
"{label} Grading": "Graduação de {label}",
"{label} {d:+} on every selected photo": "{label} {d:+} em cada foto selecionada",
"{label}, {n} photos": "{label}, {n} fotos",
"{matching} of {t} photos": "{matching} de {t} fotos",
"{matching} photos": "{matching} fotos",
"{mode} spot {} of {}": "{mode}: ponto {} de {}",
"{month}": "{month}",
"{month} {year}": "{month}/{year}",
"{msg} ({failed} failed)": "{msg} ({failed} com falha)",
"{name} Label": "Rótulo {name}",
"{name} Off": "{name} desativado",
"{name} On": "{name} ativado",
"{name} — double-click to reset it": "{name} — clique duas vezes para redefinir",
"{n} change{} saved in memory but not written to disk: {e}\nLightCraft retries automatically; quitting now would lose {}.": "{n} alteração{:.0} salva na memória mas não gravada no disco: {e}\nO LightCraft tenta novamente automaticamente; encerrar agora perderia {}.",
"{n} found · {sel} selected": "{n} encontradas · {sel} selecionadas",
"{n} photos": "{n} fotos",
"{n} photos in {} are hidden by the filters above": "{n} fotos em {} estão ocultas pelos filtros acima",
"{n} photo{}": "{n} foto{:.0}",
"{n} photo{} match · updates automatically as photos change": "{n} foto{:.0} correspondente(s) · atualiza automaticamente conforme as fotos mudam",
"{n} photo{} now · updates automatically as photos change": "{n} foto{:.0} agora · atualiza automaticamente conforme as fotos mudam",
"{sel_n} selected · {counted}": "{sel_n} selecionadas · {counted}",
"{stage}… {:.0}%": "{stage}… {:.0}%",
"{verb} {n} Photo{}": "{verb} {n} foto{:.0}",
"{weekday}, {day}": "{weekday}, {day}",
"{weekday}, {day} {month} {year}": "{weekday}, {day}/{month}/{year}",
"{what} merge added": "{what} mesclagem adicionada",
"{what} merge failed: {e}": "{what} falha na mesclagem: {e}",
"{year}": "{year}",
"{} and higher": "{} ou superior",
"{} files": "{} arquivos",
"{} files and {f} folder{}": "{} arquivos e {f} pasta{:.0}",
"{} of 4 guides — drag along lines that should be vertical or horizontal.": "{} de 4 guias — arraste ao longo das linhas que devem ficar verticais ou horizontais.",
"{} photos": "{} fotos",
"{} photo{} not readable": "{} foto{:.0} não legível",
"{} spot(s) on this photo": "{} ponto(s) nesta foto",
"· {dups} already in the library": "· {dups} já na biblioteca",
"· {n} photo{}": "· {n} foto{:.0}",
"… and {more} more": "… e mais {more}"
}
File diff suppressed because it is too large. Load diff
+11
View File
@@ -120,6 +120,7 @@ language_table! {
ZhHans, "zh-hans", "简体中文", "Hans", include_str!("../locales/zh-hans.json");
ZhHant, "zh-hant", "繁體中文(台灣)", "Hant", include_str!("../locales/zh-hant.json");
Ja, "ja", "日本語", "Jpan", include_str!("../locales/ja.json");
PtBr, "pt-br", "Português (Brasil)", "Latn", include_str!("../locales/pt-br.json");
}
// The settings file stores the BCP-47 code (`"zh-hans"`), never the Rust variant name, so a
@@ -339,6 +340,10 @@ mod tests {
assert_eq!(Locale::parse_tag("zh-Hant-TW"), Some(Locale::ZhHant));
assert_eq!(Locale::parse_tag("zh-CN"), Some(Locale::ZhHans));
assert_eq!(Locale::parse_tag("ja_JP.UTF-8"), Some(Locale::Ja));
assert_eq!(Locale::parse_tag("pt-br"), Some(Locale::PtBr));
assert_eq!(Locale::parse_tag("pt-BR"), Some(Locale::PtBr));
assert_eq!(Locale::parse_tag("pt_BR.UTF-8"), Some(Locale::PtBr));
assert_eq!(Locale::parse_tag("pt"), Some(Locale::PtBr));
assert_eq!(Locale::parse_tag("de"), None);
}
@@ -353,6 +358,9 @@ mod tests {
assert_eq!(tr("A string no catalog has"), "A string no catalog has");
set_language(Locale::Ja);
assert_eq!(tr("Exposure"), "露出");
set_language(Locale::PtBr);
assert_eq!(tr("Exposure"), "Exposição");
assert_eq!(tr("Settings"), "Configurações");
set_language(Locale::En);
assert_eq!(tr("Exposure"), "Exposure");
}
@@ -457,6 +465,7 @@ mod tests {
("app.language.english", Locale::En),
("app.language.simplifiedChinese", Locale::ZhHans),
("app.language.japanese", Locale::Ja),
("app.language.portuguese", Locale::PtBr),
];
// One command per language, and every command reachable from the menu table.
assert_eq!(commands.len(), Locale::ALL.len());
@@ -559,6 +568,8 @@ mod tests {
assert!(text.contains("マイフォト") && text.contains("すべての写真"), "{text}");
let text = painted_text(&ctx, &mut app, Locale::ZhHans);
assert!(text.contains("我的照片") && text.contains("所有照片"), "{text}");
let text = painted_text(&ctx, &mut app, Locale::PtBr);
assert!(text.contains("Minhas fotos") && text.contains("Todas as fotos"), "{text}");
set_language(Locale::En);
}
+2
View File
@@ -17,6 +17,7 @@ pub const LANGUAGE_COMMANDS: &[UiCommand] = &[
("app.language.simplifiedChinese", crate::i18n::Locale::ZhHans.name(), None, "Edit>Language"),
("app.language.traditionalChinese", crate::i18n::Locale::ZhHant.name(), None, "Edit>Language"),
("app.language.japanese", crate::i18n::Locale::Ja.name(), None, "Edit>Language"),
("app.language.portuguese", crate::i18n::Locale::PtBr.name(), None, "Edit>Language"),
];
/// Every UI command: the languages, then everything else. `xtask parity` reads both tables from
@@ -33,6 +34,7 @@ pub fn language_from_command(id: &str) -> Option<crate::i18n::Locale> {
"app.language.simplifiedChinese" => Some(crate::i18n::Locale::ZhHans),
"app.language.traditionalChinese" => Some(crate::i18n::Locale::ZhHant),
"app.language.japanese" => Some(crate::i18n::Locale::Ja),
"app.language.portuguese" => Some(crate::i18n::Locale::PtBr),
_ => None,
}
}
+36
View File
@@ -0,0 +1,36 @@
# LightCraft em português do Brasil
O LightCraft pode ser exibido em inglês, chinês simplificado, chinês tradicional (Taiwan), japonês e
português do Brasil. A preferência de idioma é gravada em `language` no `ui.json`.
- Troque em **Editar → Idioma** ou em **Configurações → Geral → Idioma**. A escolha é mantida nas
próximas inicializações.
- Traduzimos menus, edição de fotos, máscaras, recorte, configurações, importação, exportação e as
principais mensagens de progresso.
- Menus, telas e o texto com valores variáveis usam as fontes Inter (regular e semibold). Texto em
chinês e japonês exibido nos nomes de idioma (por exemplo, 日本語) continua dependendo do repositório
de fontes [storytold/craft-fonts](https://github.com/storytold/craft-fonts).
- A tradução fica na camada de apresentação. IDs de comandos, nomes de arquivos de fotos e metadados
digitados pelo usuário nunca são alterados.
- Erros técnicos, informações complementares e notas de versão não traduzidos aparecem em inglês.
- A tradução para chinês está em [`localization-zh-hans.md`](localization-zh-hans.md); para japonês,
em [`localization-ja.md`](localization-ja.md). Como adicionar outro idioma está em
[`localization.md`](localization.md).
## Manutenção da tradução
Os textos fixos ficam em `crates/ui-egui/locales/pt-br.json` e os textos com valores variáveis em
`crates/ui-egui/locales/pt-br-formats.json`. O inglês é usado como chave.
Toda tradução de formato é verificada por `format!` do Rust no build. Referencie os valores por nome
(`{n}`) ou por posição (`{0}`, `{1}`) e use as mesmas especificações de formato do inglês
(`{:.1}`, `{d:+}`, `{:.0}%`). Quando o plural inglês (`"s"` / `""`) não for necessário em português,
consuma o argumento correspondente com `{:.0}` — um texto com precisão 0 não imprime nada.
Aparência, fontes, troca de idioma, persistência da preferência e estabilidade dos IDs de comando são
verificadas por `cargo test -p lightcraft-ui-egui i18n::tests` (a checagem de glifos só roda com
`CRAFT_FONTS_DIR`). Para visualizar o idioma, renderize sem interface gráfica:
```sh
LIGHTCRAFT_LANGUAGE=pt-br lightcraft-cli snapshot --demo --script tour.jsonl -o out.png --size 1600x1000
```
+4 -3
View File
@@ -2,9 +2,9 @@
The interface ships in the language it is written in (English) plus every language in the table in
`crates/ui-egui/src/i18n.rs`. Today that is English, Simplified Chinese (`zh-hans`), Traditional
Chinese (`zh-hant`, Taiwan) and Japanese (`ja`). This file is the reference for **adding or maintaining a language**; the per-language notes
are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-hant.md`](localization-zh-hant.md) and
[`localization-ja.md`](localization-ja.md).
Chinese (`zh-hant`, Taiwan), Japanese (`ja`) and Brazilian Portuguese (`pt-br`). This file is the reference for **adding or maintaining a language**; the per-language notes
are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-hant.md`](localization-zh-hant.md),
[`localization-ja.md`](localization-ja.md) and [`localization-pt-br.md`](localization-pt-br.md).
## Adding a language
@@ -24,6 +24,7 @@ are in [`localization-zh-hans.md`](localization-zh-hans.md), [`localization-zh-h
ZhHans, "zh-hans", "简体中文", "Hans", include_str!("../locales/zh-hans.json");
ZhHant, "zh-hant", "繁體中文(台灣)", "Hant", include_str!("../locales/zh-hant.json");
Ja, "ja", "日本語", "Jpan", include_str!("../locales/ja.json");
PtBr, "pt-br", "Português (Brasil)", "Latn", include_str!("../locales/pt-br.json");
}
```
+2 -2
View File
@@ -505,8 +505,8 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
| LR-BEHAV-EMPTY-STATES | Empty states | P1 | ✅ | `crates/ui-egui/src/panels/mod.rs` (`empty_message`) | |
| LR-BEHAV-TOOLTIPS | Tooltips with shortcuts | P0 | ✅ | `crates/ui-egui/src/panels/bottombar.rs` | |
| LR-BEHAV-ACCESS | Accessibility | P2 | 🟡 | `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/panels/grid.rs` | AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control and value, buttons / icon buttons / dropdowns / section headers / sources their labels and state, grid thumbnails file name, rating, flag and label; the canvas tools (crop, masks) are pointer-only; not audited with a screen reader |
| LR-BEHAV-LOCALE | Language options | P2 | ✅ | `cmd:app.language.english`, `cmd:app.language.simplifiedChinese`, `cmd:app.language.traditionalChinese`, `cmd:app.language.japanese` | Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md) |
| LR-BEHAV-LOCALIZE | Localisation | P2 | 🟡 | `crates/ui-egui/src/i18n.rs`, `crates/ui-egui/locales/`, `docs/localization.md`, `docs/localization-zh-hans.md`, `docs/localization-zh-hant.md`, `docs/localization-ja.md` | One table entry per language (code, endonym, ISO 15924 script, catalog): English, Simplified Chinese, Traditional Chinese (Taiwan) and Japanese UI with a persisted language preference and craft-fonts regular/bold faces per script; menus, edit, crop/masks, settings, import/export and primary progress messages. Adding a language is a table entry plus two catalogs. Stock presets/profiles, history steps, library headings, date headings and capture times are localised (user-named items stay verbatim); the Traditional Chinese catalog is the most complete, the others fall back to English for messages they lack. Technical errors/release notes remain English; Traditional Chinese borrows the Simplified Chinese face (no TC face in craft-fonts yet), and the web build embeds only the Japanese face. |
| LR-BEHAV-LOCALE | Language options | P2 | ✅ | `cmd:app.language.english`, `cmd:app.language.simplifiedChinese`, `cmd:app.language.traditionalChinese`, `cmd:app.language.japanese`, `cmd:app.language.portuguese` | Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md) |
| LR-BEHAV-LOCALIZE | Localisation | P2 | 🟡 | `crates/ui-egui/src/i18n.rs`, `crates/ui-egui/locales/`, `docs/localization.md`, `docs/localization-zh-hans.md`, `docs/localization-zh-hant.md`, `docs/localization-ja.md`, `docs/localization-pt-br.md` | One table entry per language (code, endonym, ISO 15924 script, catalog): English, Simplified Chinese, Traditional Chinese (Taiwan), Japanese and Brazilian Portuguese UI with a persisted language preference and craft-fonts regular/bold faces per script; menus, edit, crop/masks, settings, import/export and primary progress messages. Adding a language is a table entry plus two catalogs. Stock presets/profiles, history steps, library headings, date headings and capture times are localised (user-named items stay verbatim); the Traditional Chinese catalog is the most complete, the others fall back to English for messages they lack. Technical errors/release notes remain English; Traditional Chinese borrows the Simplified Chinese face (no TC face in craft-fonts yet), and the web build embeds only the Japanese face. |
| LR-BEHAV-LEARN | Tutorials | OOS | 🚫 | | |
| LR-BEHAV-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew`, `docs/whats-new.md` | Help ▸ What's New: release highlights |
| LR-BEHAV-AI-EA | Early-access badges | P2 | ⬜ | | |
+1 -1
View File
@@ -452,7 +452,7 @@ old
fn reads_the_language_commands_from_menus_rs() {
let menus = include_str!("../../crates/ui-egui/src/menus.rs");
let ids = ui_command_ids(menus);
for id in ["view.detail", "app.language.english", "app.language.japanese", "app.language.simplifiedChinese"] {
for id in ["view.detail", "app.language.english", "app.language.japanese", "app.language.simplifiedChinese", "app.language.portuguese"] {
assert!(ids.iter().any(|known| known == id), "{id} not found");
}
assert_eq!(ids.iter().filter(|id| id.as_str() == "app.language.english").count(), 1, "one Language menu entry per language");