feat(i18n): add complete Turkish interface language support (#1057)

* feat(i18n): add complete Turkish interface language support

* Turkish: rows for the labels that landed alongside this catalog; refresh hashes

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Ufuk Efe
2026-10-11 16:34:25 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent d1980ec99f
commit 3d2bdfe71a
7 changed files with 2370 additions and 5 deletions
+10 -1
View File
@@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء",
/// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language):
/// the interface languages, in the same order and with the same names.
pub const DATE_LANGUAGES: [DateLanguage; 20] = [
pub const DATE_LANGUAGES: [DateLanguage; 21] = [
DateLanguage {
code: "en",
name: "English",
@@ -303,6 +303,15 @@ pub const DATE_LANGUAGES: [DateLanguage; 20] = [
days: ["Κυριακή", "Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο"],
days_short: ["Κυρ", "Δευ", "Τρί", "Τετ", "Πέμ", "Παρ", "Σάβ"],
},
DateLanguage {
code: "tr",
name: "Türkçe",
months: ["Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"],
months_with_day: None,
months_short: ["Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"],
days: ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"],
days_short: ["Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"],
},
];
/// The date language for `code` (any case), or `None` when there's none.
+93 -2
View File
@@ -136,7 +136,7 @@ fn plural_sl(n: u64) -> usize {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 20] = [
pub static LANGUAGES: [LangInfo; 21] = [
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`).
@@ -192,6 +192,8 @@ pub static LANGUAGES: [LangInfo; 20] = [
// Slovene; every `sl-*` locale (`sl-SI`) resolves here. Four plural forms (one, two, few, other).
LangInfo { code: "sl", name: "Slovenščina", source: include_str!("sl.tsv"), plural: plural_sl, rtl: false, catalog: OnceLock::new() },
LangInfo { code: "el", name: "Ελληνικά", source: include_str!("el.tsv"), plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
// Turkish; every `tr-*` locale (`tr-TR`, `tr_TR.UTF-8`) resolves here.
LangInfo { code: "tr", name: "Türkçe", source: include_str!("tr.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
];
impl LangInfo {
@@ -841,7 +843,7 @@ mod tests {
labels.insert(label.to_string());
}
assert!(labels.len() > 20 && labels.contains("Comb of characters"), "the scan found the dialog's labels: {labels:?}");
for code in ["bg", "cs", "de", "es", "fr", "hu", "it", "ja", "pt-br", "ru", "te", "uk", "zh-hans", "zh-hant"] {
for code in ["bg", "cs", "de", "es", "fr", "hu", "it", "ja", "pt-br", "ru", "te", "tr", "uk", "zh-hans", "zh-hant"] {
let lang = Lang::from_code(code).expect("registered");
let missing: Vec<_> = labels.iter().filter(|l| !has(lang, l)).collect();
assert!(missing.is_empty(), "{code} lacks Shared Field Properties labels: {missing:#?}");
@@ -1391,6 +1393,95 @@ mod tests {
set_current(Lang::EN);
}
/// Turkish covers every registered menu title and menu command label, keeps ellipses, and
/// persists in preferences.
#[test]
fn turkish_covers_every_menu_label() {
let tr_lang = Lang::from_code("tr").expect("tr registered");
assert_eq!(tr_lang.name(), "Türkçe");
assert_eq!(tr(tr_lang, "File"), "Dosya");
assert_eq!(tr(tr_lang, "Türkçe Belge.pdf"), "Türkçe Belge.pdf");
assert_eq!((0..=6).map(|n| (tr_lang.0.plural)(n)).collect::<Vec<_>>(), [0; 7]);
for spec in pdfcraft_engine::commands::COMMANDS {
let Some(menu) = spec.menu else { continue };
assert!(has(tr_lang, menu), "Turkish lacks the menu title {menu:?}");
assert!(has(tr_lang, spec.label), "Turkish lacks the {menu} menu label {:?} ({})", spec.label, spec.id);
}
const KEEP_AS_IS: &[&str] = &[
"OK",
"PDF/A…",
"Microsoft Word (.docx)",
"PostScript / EPS",
"Preflight",
"JavaScript",
"ZIP",
"{n} {kind}",
"{rule} - {status}",
"Poster",
"{field}: {e}",
"`{command}` {when}",
"{0}: {1}",
"{month} {y}",
"+LOC",
"−LOC",
"ΔLOC",
"+Bin",
"−Bin",
"Model",
"Home / End",
];
let (entries, errors) = parse_entries(tr_lang.0.source, tr_lang.0.plural_forms());
assert!(errors.is_empty(), "Turkish catalog errors: {errors:?}");
for e in &entries {
assert!(
e.source != e.translation || KEEP_AS_IS.contains(&e.source.as_str()) || (e.context == "key" && KEY_CAP_NAMES.contains(&e.source.as_str())),
"{:?} is untranslated",
e.source
);
assert!(!e.translation.contains("..."), "use … rather than three dots: {:?}", e.translation);
assert_eq!(e.source.starts_with(' '), e.translation.starts_with(' '), "leading space mismatch: {:?}", e.translation);
assert_eq!(e.source.ends_with(' '), e.translation.ends_with(' '), "trailing space mismatch: {:?}", e.translation);
}
let mut app = crate::PdfCraftApp::default();
app.set_option("language", "tr").unwrap();
assert!(app.set_option("language", "tu").is_err());
assert_eq!(app.language, "tr");
let mut restored = crate::PdfCraftApp::default();
restored.restore(&app.persist());
assert_eq!(restored.language, "tr");
}
/// Turkish translates every registered command, menu and All tools label.
#[test]
fn turkish_covers_commands_and_catalogue() {
let tr_lang = Lang::from_code("tr").expect("tr registered");
for command in pdfcraft_engine::commands::COMMANDS {
assert!(has(tr_lang, command.label), "missing command: {}", command.label);
if let Some(menu) = command.menu {
assert!(has(tr_lang, menu), "missing menu: {menu}");
}
}
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
assert!(has(tr_lang, group.label), "missing group: {}", group.label);
for section in group.sections {
assert!(has(tr_lang, section.title), "missing section: {}", section.title);
for item in section.items {
assert!(has(tr_lang, item.label), "missing item: {}", item.label);
}
}
}
}
/// Turkish translates every `tl!("…")` literal in the UI source.
#[test]
fn turkish_covers_ui_literals() {
let literals = ui_literals();
assert!(literals.len() > 900, "source scan found only {} literals", literals.len());
let tr_lang = Lang::from_code("tr").expect("tr registered");
let missing: Vec<_> = literals.iter().filter(|label| !has(tr_lang, label)).collect();
assert!(missing.is_empty(), "untranslated Turkish UI literals: {missing:#?}");
}
#[test]
fn simplified_chinese_is_registered() {
let zh = Lang::from_code("zh-hans").expect("zh-hans registered");
File diff suppressed because it is too large Load Diff
+1
View File
@@ -238,6 +238,7 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
("hu", include_str!("../src/i18n/hu.tsv")),
("uk", include_str!("../src/i18n/uk.tsv")),
("it", include_str!("../src/i18n/it.tsv")),
("tr", include_str!("../src/i18n/tr.tsv")),
("sl", include_str!("../src/i18n/sl.tsv")),
] {
let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺ");