mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
M14: Bulgarian (Български) interface language (#466)
A complete clean-room catalog (2,035 entries: every command, All tools label and tl! literal), registered as `bg` with one/other plurals so Auto picks it on Bulgarian systems. Tests mirror the Russian ones (registration, plurals, persistence, command and literal coverage), and Bulgarian joins the Latin/Cyrillic font-coverage test. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3de2be8a68
commit
79eadc2aa8
6 files changed
+2170
-8
No files matched your search
+2
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
|
||||
|
||||
## In this repository (251)
|
||||
## In this repository (252)
|
||||
|
||||
| Asset | Title | Author | Licence | Source | Used for |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -16,6 +16,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
||||
| `crates/ui-egui/src/i18n/fr.tsv` | French interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/te.tsv` | Telugu interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/ru.tsv` | Russian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/ja.tsv` | Japanese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
|
||||
@@ -98,6 +98,17 @@ kind = "translation"
|
||||
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "259f795a3cdc032aa670693d2ebff010c5dd3bcf57dda11548d5bd46e78969b8"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/bg.tsv"
|
||||
title = "Bulgarian interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources"
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "121c3881bf6ba69682ca6acae32303e3bf425479a8c2cb3e875c6598f5ffef7d"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
||||
title = "Japanese interface translation catalog"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -87,7 +87,7 @@ fn plural_russian(n: u64) -> usize {
|
||||
}
|
||||
|
||||
/// The registry. English first: it is the fallback and the source language.
|
||||
pub static LANGUAGES: [LangInfo; 10] = [
|
||||
pub static LANGUAGES: [LangInfo; 11] = [
|
||||
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, catalog: OnceLock::new() },
|
||||
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, catalog: OnceLock::new() },
|
||||
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`).
|
||||
@@ -104,6 +104,8 @@ pub static LANGUAGES: [LangInfo; 10] = [
|
||||
LangInfo { code: "fr", name: "Français", source: include_str!("fr.tsv"), plural: plural_fr, catalog: OnceLock::new() },
|
||||
// Russian; every `ru-*` locale (`ru-RU`, `ru-BY`, `ru-KZ` ...) resolves here.
|
||||
LangInfo { code: "ru", name: "Русский", source: include_str!("ru.tsv"), plural: plural_russian, catalog: OnceLock::new() },
|
||||
// Bulgarian; every `bg-*` locale (`bg-BG`) resolves here.
|
||||
LangInfo { code: "bg", name: "Български", source: include_str!("bg.tsv"), plural: plural_one_other, catalog: OnceLock::new() },
|
||||
// Telugu; every `te-*` locale (`te-IN`) resolves here.
|
||||
LangInfo { code: "te", name: "తెలుగు", source: include_str!("te.tsv"), plural: plural_one_other, catalog: OnceLock::new() },
|
||||
];
|
||||
@@ -1229,6 +1231,109 @@ mod tests {
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulgarian_is_registered() {
|
||||
let bg = Lang::from_code("bg").expect("bg registered");
|
||||
assert_eq!(bg.name(), "Български");
|
||||
assert_eq!(normalize_pref("BG"), Some("bg"));
|
||||
assert_eq!(lang_from_tag("bg_BG.UTF-8"), Some(bg));
|
||||
assert_eq!(lang_from_tag("bg-BG"), Some(bg));
|
||||
assert_eq!(first_supported("bg-BG\r\nen-US"), Some(bg));
|
||||
assert_eq!(tr(bg, "File"), "Файл");
|
||||
assert_eq!(tr(bg, "Save as…"), "Запиши като…");
|
||||
assert_eq!(tr(bg, "Bookmarks"), "Отметки");
|
||||
assert_eq!(tr(bg, "Layers"), "Слоеве");
|
||||
assert_eq!(tr(bg, "Rapport de l'utilisateur.pdf"), "Rapport de l'utilisateur.pdf");
|
||||
// one (1), other (0, 2+)
|
||||
assert_eq!((0..=3).map(|n| (bg.0.plural)(n)).collect::<Vec<_>>(), [1, 0, 1, 1]);
|
||||
assert_eq!(trn(bg, 0, "{n} page", "{n} pages"), "0 страници");
|
||||
assert_eq!(trn(bg, 1, "{n} page", "{n} pages"), "1 страница");
|
||||
assert_eq!(trn(bg, 2, "{n} page", "{n} pages"), "2 страници");
|
||||
assert_eq!(trn(bg, 21, "{n} page", "{n} pages"), "21 страници");
|
||||
assert_eq!(trn(bg, 1, "{n} field", "{n} fields"), "1 поле");
|
||||
assert_eq!(trn(bg, 5, "{n} field", "{n} fields"), "5 полета");
|
||||
let mut app = crate::PdfCraftApp::default();
|
||||
app.set_option("language", "bg").unwrap();
|
||||
assert_eq!(app.language, "bg");
|
||||
let mut restored = crate::PdfCraftApp::default();
|
||||
restored.restore(&app.persist());
|
||||
assert_eq!(restored.language, "bg");
|
||||
}
|
||||
|
||||
/// Bulgarian translates every registered command and every All tools group, section and item.
|
||||
#[test]
|
||||
fn bulgarian_covers_commands_and_catalogue() {
|
||||
let bg = Lang::from_code("bg").expect("bg registered");
|
||||
for command in pdfcraft_engine::commands::COMMANDS {
|
||||
assert!(has(bg, command.label), "missing command: {}", command.label);
|
||||
if let Some(menu) = command.menu {
|
||||
assert!(has(bg, menu), "missing menu: {menu}");
|
||||
}
|
||||
}
|
||||
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
|
||||
assert!(has(bg, group.label), "missing group: {}", group.label);
|
||||
for section in group.sections {
|
||||
assert!(has(bg, section.title), "missing section: {}", section.title);
|
||||
for item in section.items {
|
||||
assert!(has(bg, item.label), "missing item: {}", item.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// New tl!("literal") labels must not silently fall back to English.
|
||||
#[test]
|
||||
fn bulgarian_covers_ui_literals() {
|
||||
let bg = Lang::from_code("bg").expect("bg registered");
|
||||
let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")];
|
||||
let mut literals = std::collections::BTreeSet::new();
|
||||
while let Some(dir) = stack.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("UI source directory") {
|
||||
let path = entry.expect("UI source entry").path();
|
||||
if path.is_dir() {
|
||||
if path.file_name().is_some_and(|name| name != "i18n") {
|
||||
stack.push(path);
|
||||
}
|
||||
} else if path.extension().is_some_and(|ext| ext == "rs") {
|
||||
let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n");
|
||||
let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default();
|
||||
while let Some((_, after)) = rest.split_once("tl!(\"") {
|
||||
let mut escaped = false;
|
||||
let end = after
|
||||
.char_indices()
|
||||
.find_map(|(i, c)| {
|
||||
if c == '"' && !escaped {
|
||||
return Some(i);
|
||||
}
|
||||
escaped = c == '\\' && !escaped;
|
||||
None
|
||||
})
|
||||
.expect("closed tl! literal");
|
||||
let (raw, tail) = after.split_at(end);
|
||||
if tail.starts_with("\")") {
|
||||
let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes");
|
||||
literals.insert(label);
|
||||
}
|
||||
rest = tail.strip_prefix('"').expect("closing quote");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(literals.len() > 900, "source scan found only {} literals", literals.len());
|
||||
let missing: Vec<_> = literals.iter().filter(|label| !has(bg, label)).collect();
|
||||
assert!(missing.is_empty(), "untranslated Bulgarian UI literals: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulgarian_diagnostics_preserve_user_values() {
|
||||
let bg = Lang::from_code("bg").expect("bg registered");
|
||||
set_current(bg);
|
||||
assert_eq!(action_label("Custom action {n}"), "Custom action {n}");
|
||||
assert_eq!(fmt(t("{n} pages selected"), &[("n", "3")]), "Избрани страници: 3");
|
||||
assert_eq!(tr(bg, "CheckBox"), "Квадратче за отметка");
|
||||
set_current(Lang::EN);
|
||||
}
|
||||
|
||||
/// Every bundled catalog is well-formed and consistent with its sources.
|
||||
#[test]
|
||||
fn bundled_catalogs_are_consistent() {
|
||||
|
||||
@@ -162,7 +162,7 @@ fn ui_fonts_work_without_craft_fonts() {
|
||||
theme::install_fonts(&ctx);
|
||||
}
|
||||
|
||||
/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, Spanish, French, Russian) are drawn entirely by the app's own faces
|
||||
/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, Spanish, French, Russian, Bulgarian) are drawn entirely by the app's own faces
|
||||
/// (Inter, JetBrains Mono): no letter falls through to egui's defaults or a CJK fallback. (egui's
|
||||
/// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.)
|
||||
#[test]
|
||||
@@ -175,6 +175,7 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
|
||||
("es", include_str!("../src/i18n/es.tsv")),
|
||||
("fr", include_str!("../src/i18n/fr.tsv")),
|
||||
("ru", include_str!("../src/i18n/ru.tsv")),
|
||||
("bg", include_str!("../src/i18n/bg.tsv")),
|
||||
] {
|
||||
let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜ¿¡«»…");
|
||||
for line in catalog.lines().filter(|l| !l.starts_with('#')) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Interface language
|
||||
|
||||
Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Čeština**, **Português (Brasil)**, **Español**, **Français** or **తెలుగు**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged.
|
||||
Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Español**, **Français** or **తెలుగు**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged.
|
||||
|
||||
**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English.
|
||||
**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, and Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English.
|
||||
|
||||
The control channel exposes the setting through `ui.set`:
|
||||
|
||||
@@ -10,11 +10,11 @@ The control channel exposes the setting through `ui.set`:
|
||||
{"method":"ui.set","params":{"key":"language","value":"ja"}}
|
||||
```
|
||||
|
||||
The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `cs`, `pt-br`, `es`, `fr`, `te`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `cs`, `pt-br`, `es`, `fr` or `te`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language.
|
||||
The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `es`, `fr`, `te`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `es`, `fr` or `te`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language.
|
||||
|
||||
Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Spanish, French and Telugu translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing.
|
||||
Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, Spanish, French and Telugu translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing.
|
||||
|
||||
Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Czech, Brazilian Portuguese, Spanish and French need only the bundled Latin faces (tested in `tests/fonts.rs`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet.
|
||||
Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, Spanish and French need only the bundled Latin faces (tested in `tests/fonts.rs`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet.
|
||||
|
||||
There is no right-to-left interface language yet. File names and document titles in Arabic or Hebrew are drawn with a craft-fonts `Arab` face when the build input has one; on desktop, a character no embedded face has is drawn with one font already installed on the machine (`PDFCRAFT_SYSTEM_FONTS=0` turns this off). The tab strip, the recent-files list and Properties show such names in display order; other places (bookmarks, comments, search results) have the glyphs but may order mixed-direction text wrongly.
|
||||
|
||||
|
||||
Reference in new issue
Block a user