Files
pdfcraft/crates/engine/src/dates.rs
T
aiongg 472717fcd7 Merge upstream main 88dad98dd into next
# Conflicts:
#	ATTRIBUTION.md
#	ATTRIBUTION.toml
#	crates/engine/src/dates.rs
#	crates/ui-egui/src/i18n/ar.tsv
#	crates/ui-egui/src/i18n/bg.tsv
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/kk.tsv
#	crates/ui-egui/src/i18n/lt.tsv
#	crates/ui-egui/src/i18n/mod.rs
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sl.tsv
#	crates/ui-egui/src/i18n/te.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/tests/fonts.rs
#	docs/localization-parity.md
#	docs/localization.md
#	docs/roadmap.md
#	parity/acrobat-features.toml
2026-10-22 05:22:09 +00:00

1008 lines
48 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The date format Fill & Sign stamps (Preferences ▸ Date format), kept on the [`Session`] so
//! the app and the automation tools write the same text. Acrobat's Fill & Sign has no such
//! setting (only Acrobat Sign's account settings do); this one is per user and takes any
//! pattern in Acrobat's date letters, not just a fixed list, with month and weekday names in
//! any of [`DATE_LANGUAGES`].
//!
//! Patterns use `util.printd`'s date letters: `yyyy` `yy`, `mmmm` `mmm` `mm` `m`, `dddd` `ddd`
//! `dd` `d`. `\` shows the next character as is (`d \de mmmm` → `8 de October`).
use crate::Session;
/// The format until the user picks one (Acrobat's: US month/day/year).
pub const DEFAULT_DATE_FORMAT: &str = "m/d/yyyy";
/// Ready-made formats for Preferences ▸ Date format. Any other valid pattern works too.
pub const DATE_FORMATS: [&str; 14] = [
"m/d/yyyy",
"mm/dd/yyyy",
"d/m/yyyy",
"dd/mm/yyyy",
"dd.mm.yyyy",
"yyyy-mm-dd",
"yyyy.mm.dd.",
// Year first with slashes, as Taiwan, Japan and China write it (Windows' zh-TW short date).
"yyyy/mm/dd",
"yyyy/m/d",
"d mmm yyyy",
"d mmmm yyyy",
"mmm d, yyyy",
"mmmm d, yyyy",
"dddd, mmmm d, yyyy",
];
/// The longest pattern kept (settings and tool arguments are untrusted).
pub const MAX_DATE_FORMAT_CHARS: usize = 64;
/// Month and weekday names in one language. Weekdays start on Sunday.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DateLanguage {
/// The interface-language code where there is one (`en`, `cs`, `pt-br`, `zh-hans`, …).
pub code: &'static str,
/// Its name in itself, for pickers.
pub name: &'static str,
pub months: [&'static str; 12],
/// The month after a day number (`mmmm` when the pattern also shows `d` or `dd`), where
/// the language changes it: Czech `8. října`, Russian `8 октября`.
pub months_with_day: Option<[&'static str; 12]>,
pub months_short: [&'static str; 12],
pub days: [&'static str; 7],
pub days_short: [&'static str; 7],
}
const CJK_MONTHS: [&str; 12] = ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"];
const ZH_MONTHS: [&str; 12] = ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"];
const ZH_DAYS: [&str; 7] = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
const AR_MONTHS: [&str; 12] = ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"];
const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"];
const CKB_MONTHS: [&str; 12] =
["کانوونی دووەم", "شوبات", "ئازار", "نیسان", "ئایار", "حوزەیران", "تەممووز", "ئاب", "ئەیلوول", "تشرینی یەکەم", "تشرینی دووەم", "کانوونی یەکەم"];
const CKB_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 static DATE_LANGUAGES: [DateLanguage; 34] = [
DateLanguage {
code: "en",
name: "English",
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
months_with_day: None,
months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
days_short: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
},
DateLanguage {
code: "ja",
name: "日本語",
months: CJK_MONTHS,
months_with_day: None,
months_short: CJK_MONTHS,
days: ["日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"],
days_short: ["日", "月", "火", "水", "木", "金", "土"],
},
DateLanguage {
code: "zh-hans",
name: "简体中文",
months: ZH_MONTHS,
months_with_day: None,
months_short: CJK_MONTHS,
days: ZH_DAYS,
days_short: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
},
DateLanguage {
code: "zh-hant",
name: "繁體中文",
months: ZH_MONTHS,
months_with_day: None,
months_short: CJK_MONTHS,
days: ZH_DAYS,
days_short: ["週日", "週一", "週二", "週三", "週四", "週五", "週六"],
},
DateLanguage {
code: "cs",
name: "Čeština",
months: ["leden", "únor", "březen", "duben", "květen", "červen", "červenec", "srpen", "září", "říjen", "listopad", "prosinec"],
months_with_day: Some([
"ledna",
"února",
"března",
"dubna",
"května",
"června",
"července",
"srpna",
"září",
"října",
"listopadu",
"prosince",
]),
months_short: ["led", "úno", "bře", "dub", "kvě", "čvn", "čvc", "srp", "zář", "říj", "lis", "pro"],
days: ["neděle", "pondělí", "úterý", "středa", "čtvrtek", "pátek", "sobota"],
days_short: ["ne", "po", "út", "st", "čt", "pá", "so"],
},
DateLanguage {
code: "pt-br",
name: "Português (Brasil)",
months: ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"],
months_with_day: None,
months_short: ["jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez"],
days: ["domingo", "segunda-feira", "terça-feira", "quarta-feira", "quinta-feira", "sexta-feira", "sábado"],
days_short: ["dom", "seg", "ter", "qua", "qui", "sex", "sáb"],
},
DateLanguage {
code: "de",
name: "Deutsch",
months: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
months_with_day: None,
months_short: ["Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.", "Okt.", "Nov.", "Dez."],
days: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
days_short: ["So.", "Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa."],
},
DateLanguage {
code: "es",
name: "Español",
months: ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"],
months_with_day: None,
months_short: ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sept", "oct", "nov", "dic"],
days: ["domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"],
days_short: ["dom", "lun", "mar", "mié", "jue", "vie", "sáb"],
},
DateLanguage {
code: "fr",
name: "Français",
months: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"],
months_with_day: None,
months_short: ["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."],
days: ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
days_short: ["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."],
},
DateLanguage {
code: "ru",
name: "Русский",
months: ["январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"],
months_with_day: Some(["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"]),
months_short: ["янв.", "февр.", "март", "апр.", "май", "июнь", "июль", "авг.", "сент.", "окт.", "нояб.", "дек."],
days: ["воскресенье", "понедельник", "вторник", "среда", "четверг", "пятница", "суббота"],
days_short: ["вс", "пн", "вт", "ср", "чт", "пт", "сб"],
},
DateLanguage {
code: "kk",
name: "Қазақша",
months: ["қаңтар", "ақпан", "наурыз", "сәуір", "мамыр", "маусым", "шілде", "тамыз", "қыркүйек", "қазан", "қараша", "желтоқсан"],
months_with_day: None,
months_short: ["қаң", "ақп", "нау", "сәу", "мам", "мау", "шіл", "там", "қыр", "қаз", "қар", "жел"],
days: ["жексенбі", "дүйсенбі", "сейсенбі", "сәрсенбі", "бейсенбі", "жұма", "сенбі"],
days_short: ["жк", "дс", "сс", "ср", "бс", "жм", "сб"],
},
DateLanguage {
code: "bg",
name: "Български",
months: ["януари", "февруари", "март", "април", "май", "юни", "юли", "август", "септември", "октомври", "ноември", "декември"],
months_with_day: None,
months_short: ["яну", "фев", "март", "апр", "май", "юни", "юли", "авг", "сеп", "окт", "ное", "дек"],
days: ["неделя", "понеделник", "вторник", "сряда", "четвъртък", "петък", "събота"],
days_short: ["нд", "пн", "вт", "ср", "чт", "пт", "сб"],
},
DateLanguage {
code: "te",
name: "తెలుగు",
months: ["జనవరి", "ఫిబ్రవరి", "మార్చి", "ఏప్రిల్", "మే", "జూన్", "జులై", "ఆగస్టు", "సెప్టెంబర్", "అక్టోబర్", "నవంబర్", "డిసెంబర్"],
months_with_day: None,
months_short: ["జన", "ఫిబ్ర", "మార్చి", "ఏప్రి", "మే", "జూన్", "జులై", "ఆగ", "సెప్టెం", "అక్టో", "నవం", "డిసెం"],
days: ["ఆదివారం", "సోమవారం", "మంగళవారం", "బుధవారం", "గురువారం", "శుక్రవారం", "శనివారం"],
days_short: ["ఆది", "సోమ", "మంగళ", "బుధ", "గురు", "శుక్ర", "శని"],
},
DateLanguage {
code: "hu",
name: "Magyar",
months: [
"január",
"február",
"március",
"április",
"május",
"június",
"július",
"augusztus",
"szeptember",
"október",
"november",
"december",
],
months_with_day: None,
months_short: ["jan.", "febr.", "márc.", "ápr.", "máj.", "jún.", "júl.", "aug.", "szept.", "okt.", "nov.", "dec."],
days: ["vasárnap", "hétfő", "kedd", "szerda", "csütörtök", "péntek", "szombat"],
days_short: ["V", "H", "K", "Sze", "Cs", "P", "Szo"],
},
DateLanguage {
code: "uk",
name: "Українська",
months: ["січень", "лютий", "березень", "квітень", "травень", "червень", "липень", "серпень", "вересень", "жовтень", "листопад", "грудень"],
months_with_day: Some([
"січня",
"лютого",
"березня",
"квітня",
"травня",
"червня",
"липня",
"серпня",
"вересня",
"жовтня",
"листопада",
"грудня",
]),
months_short: ["січ.", "лют.", "бер.", "квіт.", "трав.", "черв.", "лип.", "серп.", "вер.", "жовт.", "лист.", "груд."],
days: ["неділя", "понеділок", "вівторок", "середа", "четвер", "пʼятниця", "субота"],
days_short: ["нд", "пн", "вт", "ср", "чт", "пт", "сб"],
},
DateLanguage {
code: "it",
name: "Italiano",
months: ["gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"],
months_with_day: None,
months_short: ["gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"],
days: ["domenica", "lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato"],
days_short: ["dom", "lun", "mar", "mer", "gio", "ven", "sab"],
},
// Arabic has no shorter written forms of its month and weekday names.
DateLanguage {
code: "ar",
name: "العربية",
months: AR_MONTHS,
months_with_day: None,
months_short: AR_MONTHS,
days: AR_DAYS,
days_short: AR_DAYS,
},
// Central Kurdish (Sorani) has no shorter written forms of its month and weekday names either.
DateLanguage {
code: "ckb",
name: "کوردیی ناوەندی",
months: CKB_MONTHS,
months_with_day: None,
months_short: CKB_MONTHS,
days: CKB_DAYS,
days_short: CKB_DAYS,
},
DateLanguage {
code: "pl",
name: "Polski",
months: ["styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień"],
months_with_day: Some([
"stycznia",
"lutego",
"marca",
"kwietnia",
"maja",
"czerwca",
"lipca",
"sierpnia",
"września",
"października",
"listopada",
"grudnia",
]),
months_short: ["sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru"],
days: ["niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"],
days_short: ["niedz.", "pon.", "wt.", "śr.", "czw.", "pt.", "sob."],
},
DateLanguage {
code: "sl",
name: "Slovenščina",
months: ["januar", "februar", "marec", "april", "maj", "junij", "julij", "avgust", "september", "oktober", "november", "december"],
months_with_day: Some([
"januarja",
"februarja",
"marca",
"aprila",
"maja",
"junija",
"julija",
"avgusta",
"septembra",
"oktobra",
"novembra",
"decembra",
]),
months_short: ["jan.", "feb.", "mar.", "apr.", "maj", "jun.", "jul.", "avg.", "sep.", "okt.", "nov.", "dec."],
days: ["nedelja", "ponedeljek", "torek", "sreda", "četrtek", "petek", "sobota"],
days_short: ["ned.", "pon.", "tor.", "sre.", "čet.", "pet.", "sob."],
},
DateLanguage {
code: "el",
name: "Ελληνικά",
months: [
"Ιανουάριος",
"Φεβρουάριος",
"Μάρτιος",
"Απρίλιος",
"Μάιος",
"Ιούνιος",
"Ιούλιος",
"Αύγουστος",
"Σεπτέμβριος",
"Οκτώβριος",
"Νοέμβριος",
"Δεκέμβριος",
],
months_with_day: Some([
"Ιανουαρίου",
"Φεβρουαρίου",
"Μαρτίου",
"Απριλίου",
"Μαΐου",
"Ιουνίου",
"Ιουλίου",
"Αυγούστου",
"Σεπτεμβρίου",
"Οκτωβρίου",
"Νοεμβρίου",
"Δεκεμβρίου",
]),
months_short: ["Ιαν", "Φεβ", "Μαρ", "Απρ", "Μάι", "Ιουν", "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"],
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"],
},
DateLanguage {
code: "lt",
name: "Lietuvių",
months: ["sausis", "vasaris", "kovas", "balandis", "gegužė", "birželis", "liepa", "rugpjūtis", "rugsėjis", "spalis", "lapkritis", "gruodis"],
months_with_day: Some([
"sausio",
"vasario",
"kovo",
"balandžio",
"gegužės",
"birželio",
"liepos",
"rugpjūčio",
"rugsėjo",
"spalio",
"lapkričio",
"gruodžio",
]),
months_short: ["saus.", "vas.", "kov.", "bal.", "geg.", "birž.", "liep.", "rugpj.", "rugs.", "spal.", "lapkr.", "gruod."],
days: ["sekmadienis", "pirmadienis", "antradienis", "trečiadienis", "ketvirtadienis", "penktadienis", "šeštadienis"],
days_short: ["sk", "pr", "an", "tr", "kt", "pn", "št"],
},
// Indonesian. Month and weekday names are the same with and after a day number, so
// `months_with_day` is `None`.
DateLanguage {
code: "id",
name: "Bahasa Indonesia",
months: ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"],
months_with_day: None,
months_short: ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"],
days: ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"],
days_short: ["Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"],
},
// Vietnamese: numeric month forms (`tháng 1` with a day, `Tháng 1` standing alone).
// Fill & Sign still refuses any format whose final text fails the existing WinAnsi check
// (for example weekday names); `dd.mm.yyyy` remains writable.
DateLanguage {
code: "vi",
name: "Tiếng Việt",
months: [
"Tháng 1",
"Tháng 2",
"Tháng 3",
"Tháng 4",
"Tháng 5",
"Tháng 6",
"Tháng 7",
"Tháng 8",
"Tháng 9",
"Tháng 10",
"Tháng 11",
"Tháng 12",
],
months_with_day: Some([
"tháng 1",
"tháng 2",
"tháng 3",
"tháng 4",
"tháng 5",
"tháng 6",
"tháng 7",
"tháng 8",
"tháng 9",
"tháng 10",
"tháng 11",
"tháng 12",
]),
months_short: ["Thg 1", "Thg 2", "Thg 3", "Thg 4", "Thg 5", "Thg 6", "Thg 7", "Thg 8", "Thg 9", "Thg 10", "Thg 11", "Thg 12"],
days: ["Chủ Nhật", "Thứ Hai", "Thứ Ba", "Thứ Tư", "Thứ Năm", "Thứ Sáu", "Thứ Bảy"],
days_short: ["CN", "T2", "T3", "T4", "T5", "T6", "T7"],
},
DateLanguage {
code: "fi",
name: "Suomi",
months: [
"tammikuu",
"helmikuu",
"maaliskuu",
"huhtikuu",
"toukokuu",
"kesäkuu",
"heinäkuu",
"elokuu",
"syyskuu",
"lokakuu",
"marraskuu",
"joulukuu",
],
// After a day number the month takes the partitive: `14. lokakuuta 2026`.
months_with_day: Some([
"tammikuuta",
"helmikuuta",
"maaliskuuta",
"huhtikuuta",
"toukokuuta",
"kesäkuuta",
"heinäkuuta",
"elokuuta",
"syyskuuta",
"lokakuuta",
"marraskuuta",
"joulukuuta",
]),
months_short: ["tammi", "helmi", "maalis", "huhti", "touko", "kesä", "heinä", "elo", "syys", "loka", "marras", "joulu"],
days: ["sunnuntai", "maanantai", "tiistai", "keskiviikko", "torstai", "perjantai", "lauantai"],
days_short: ["su", "ma", "ti", "ke", "to", "pe", "la"],
},
DateLanguage {
code: "sq",
name: "Shqip",
months: ["janar", "shkurt", "mars", "prill", "maj", "qershor", "korrik", "gusht", "shtator", "tetor", "nëntor", "dhjetor"],
months_with_day: None,
months_short: ["jan", "shk", "mar", "pri", "maj", "qer", "korr", "gush", "sht", "tet", "nën", "dhj"],
days: ["e diel", "e hënë", "e martë", "e mërkurë", "e enjte", "e premte", "e shtunë"],
days_short: ["die", "hën", "mar", "mër", "enj", "pre", "sht"],
},
DateLanguage {
code: "th",
name: "ไทย",
months: ["มกราคม", "กุมภาพันธ์", "มีนาคม", "เมษายน", "พฤษภาคม", "มิถุนายน", "กรกฎาคม", "สิงหาคม", "กันยายน", "ตุลาคม", "พฤศจิกายน", "ธันวาคม"],
months_with_day: None,
months_short: ["ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค."],
days: ["วันอาทิตย์", "วันจันทร์", "วันอังคาร", "วันพุธ", "วันพฤหัสบดี", "วันศุกร์", "วันเสาร์"],
days_short: ["อา.", "จ.", "อ.", "พ.", "พฤ.", "ศ.", "ส."],
},
DateLanguage {
code: "ca",
name: "Català",
months: ["gener", "febrer", "març", "abril", "maig", "juny", "juliol", "agost", "setembre", "octubre", "novembre", "desembre"],
// After a day the month carries its own preposition, elided before a vowel: `7 d'abril`.
months_with_day: Some([
"de gener",
"de febrer",
"de març",
"d'abril",
"de maig",
"de juny",
"de juliol",
"d'agost",
"de setembre",
"d'octubre",
"de novembre",
"de desembre",
]),
months_short: ["gen.", "febr.", "març", "abr.", "maig", "juny", "jul.", "ag.", "set.", "oct.", "nov.", "des."],
days: ["diumenge", "dilluns", "dimarts", "dimecres", "dijous", "divendres", "dissabte"],
days_short: ["dg.", "dl.", "dt.", "dx.", "dj.", "dv.", "ds."],
},
// Romanian: month and weekday names are lowercase, also standing alone (`noiembrie 2023`).
DateLanguage {
code: "ro",
name: "Română",
months: [
"ianuarie",
"februarie",
"martie",
"aprilie",
"mai",
"iunie",
"iulie",
"august",
"septembrie",
"octombrie",
"noiembrie",
"decembrie",
],
months_with_day: None,
months_short: ["ian.", "feb.", "mar.", "apr.", "mai", "iun.", "iul.", "aug.", "sept.", "oct.", "nov.", "dec."],
days: ["duminică", "luni", "marți", "miercuri", "joi", "vineri", "sâmbătă"],
days_short: ["dum.", "lun.", "mar.", "mie.", "joi", "vin.", "sâm."],
},
DateLanguage {
code: "lo",
name: "ພາສາລາວ",
months: ["ມັງກອນ", "ກຸມພາ", "ມີນາ", "ເມສາ", "ພຶດສະພາ", "ມິຖຸນາ", "ກໍລະກົດ", "ສິງຫາ", "ກັນຍາ", "ຕຸລາ", "ພະຈິກ", "ທັນວາ"],
months_with_day: None,
months_short: ["ມ.ກ.", "ກ.ພ.", "ມ.ນ.", "ມ.ສ.", "ພ.ພ.", "ມິ.ຖ.", "ກ.ລ.", "ສ.ຫ.", "ກ.ຍ.", "ຕ.ລ.", "ພ.ຈ.", "ທ.ວ."],
days: ["ວັນອາທິດ", "ວັນຈັນ", "ວັນອັງຄານ", "ວັນພຸດ", "ວັນພະຫັດ", "ວັນສຸກ", "ວັນເສົາ"],
days_short: ["ອາທິດ", "ຈັນ", "ອັງຄານ", "ພຸດ", "ພະຫັດ", "ສຸກ", "ເສົາ"],
},
// Hindi (Devanagari). Month and weekday names are the same with and after a day number.
DateLanguage {
code: "hi",
name: "हिन्दी",
months: ["जनवरी", "फ़रवरी", "मार्च", "अप्रैल", "मई", "जून", "जुलाई", "अगस्त", "सितंबर", "अक्तूबर", "नवंबर", "दिसंबर"],
months_with_day: None,
months_short: ["जन॰", "फ़र॰", "मार्च", "अप्रैल", "मई", "जून", "जुला॰", "अग॰", "सित॰", "अक्तू॰", "नव॰", "दिस॰"],
days: ["रविवार", "सोमवार", "मंगलवार", "बुधवार", "गुरुवार", "शुक्रवार", "शनिवार"],
days_short: ["रवि", "सोम", "मंगल", "बुध", "गुरु", "शुक्र", "शनि"],
},
// Korean: months are numbered (`3월`) and weekdays end in 요일; no names change after a day.
DateLanguage {
code: "ko",
name: "한국어",
months: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
months_with_day: None,
months_short: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
days: ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"],
days_short: ["일", "월", "화", "수", "목", "금", "토"],
},
DateLanguage {
code: "nb",
name: "Norsk bokmål",
months: ["januar", "februar", "mars", "april", "mai", "juni", "juli", "august", "september", "oktober", "november", "desember"],
months_with_day: None,
months_short: ["jan.", "feb.", "mar.", "apr.", "mai", "jun.", "jul.", "aug.", "sep.", "okt.", "nov.", "des."],
days: ["søndag", "mandag", "tirsdag", "onsdag", "torsdag", "fredag", "lørdag"],
days_short: ["søn.", "man.", "tir.", "ons.", "tor.", "fre.", "lør."],
},
];
/// The date language for `code` (any case), or `None` when there's none.
pub fn date_language(code: &str) -> Option<&'static DateLanguage> {
DATE_LANGUAGES.iter().find(|l| l.code.eq_ignore_ascii_case(code.trim()))
}
/// The characters of `text` that Fill & Sign can't write into a PDF yet, each once. Its text is
/// drawn in Helvetica with WinAnsi encoding (Western European letters), so anything else (`ř`,
/// Japanese, Chinese) would become `?` in the file.
pub fn unwritable(text: &str) -> String {
let mut out = String::new();
for c in text.chars() {
let mut buf = [0u8; 4];
if c != '?' && pdfcraft_fonts::win_ansi(c.encode_utf8(&mut buf)) == b"?" && !out.contains(c) {
out.push(c);
}
}
out
}
/// `fmt` trimmed, when it's a usable date pattern: not too long, showing at least a year,
/// month or day, and no time letters (`H h M s t`) unless escaped with `\`.
pub fn check_date_format(fmt: &str) -> Result<&str, String> {
let fmt = fmt.trim();
if fmt.is_empty() {
return Err("the date format is empty".into());
}
if fmt.chars().count() > MAX_DATE_FORMAT_CHARS {
return Err(format!("the date format is longer than {MAX_DATE_FORMAT_CHARS} characters"));
}
let letters: String = unescaped(fmt).filter_map(|(c, escaped)| (!escaped).then_some(c)).collect();
if let Some(c) = letters.chars().find(|c| matches!(c, 'H' | 'h' | 'M' | 's' | 't')) {
return Err(format!("`{c}` is a time letter (H h M s t); put \\ before it to show it as is"));
}
if !letters.chars().any(|c| matches!(c, 'y' | 'm' | 'd')) {
return Err("the date format shows no year (y), month (m) or day (d)".into());
}
// Only whole codes: `yyy` or `mmmmm` would silently show something else.
for (c, n, _) in runs(fmt).into_iter().filter(|r| !r.2) {
let fits = match c {
'y' => n == 2 || n == 4,
'm' | 'd' => (1..=4).contains(&n),
_ => true,
};
if !fits {
let code: String = std::iter::repeat_n(c, n).collect();
let use_ = match c {
'y' => "yy or yyyy",
'm' => "m, mm, mmm or mmmm",
_ => "d, dd, ddd or dddd",
};
return Err(format!("`{code}` is not a date code; use {use_}"));
}
}
Ok(fmt)
}
/// `fmt` as runs of the same character with whether it's escaped: `dd.\mm` → (`d`, 2, no),
/// (`.`, 1, no), (`m`, 1, yes), (`m`, 1, no). Escaped characters are never grouped.
fn runs(fmt: &str) -> Vec<(char, usize, bool)> {
let mut runs: Vec<(char, usize, bool)> = Vec::new();
for (c, escaped) in unescaped(fmt) {
match runs.last_mut() {
Some((last, n, false)) if !escaped && *last == c => *n += 1,
_ => runs.push((c, 1, escaped)),
}
}
runs
}
/// The day `(year, month, day)` written in `fmt` with `lang`'s names. Anything but the date
/// letters is copied as is; a month or day out of range shows as its number.
pub fn format_day(fmt: &str, (y, m, d): (i64, u32, u32), lang: &DateLanguage) -> String {
let runs = runs(fmt);
let with_day = runs.iter().any(|&(c, n, escaped)| !escaped && c == 'd' && n <= 2);
let month = (m as usize).checked_sub(1);
let weekday = weekday(y, m, d);
let mut out = String::new();
for (c, n, escaped) in runs {
match (c, n) {
_ if escaped => out.push(c),
('y', 4..) => out.push_str(&format!("{:04}", y.clamp(0, 9999))),
('y', _) => out.push_str(&format!("{:02}", y.rem_euclid(100))),
('m', 3..) => {
let names = if n >= 4 { lang.months_with_day.filter(|_| with_day).unwrap_or(lang.months) } else { lang.months_short };
match month.and_then(|i| names.get(i)) {
Some(name) => out.push_str(name),
None => out.push_str(&m.to_string()),
}
}
('m', 2) => out.push_str(&format!("{m:02}")),
('m', _) => out.push_str(&m.to_string()),
('d', 3..) => {
let names = if n >= 4 { lang.days } else { lang.days_short };
match weekday.and_then(|w| names.get(w)) {
Some(name) => out.push_str(name),
None => out.push_str(&d.to_string()),
}
}
('d', 2) => out.push_str(&format!("{d:02}")),
('d', _) => out.push_str(&d.to_string()),
_ => out.extend(std::iter::repeat_n(c, n)),
}
}
out
}
/// The weekday (0 = Sunday) of a Gregorian date in years 1–9999, or `None` for an invalid one.
fn weekday(y: i64, m: u32, d: u32) -> Option<usize> {
const T: [i64; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
if !(1..=9999).contains(&y) || !(1..=31).contains(&d) {
return None;
}
let t = *T.get((m as usize).checked_sub(1)?)?;
let y = y - i64::from(m < 3);
Some((y + y / 4 - y / 100 + y / 400 + t + i64::from(d)).rem_euclid(7) as usize)
}
/// The characters of `fmt`, each with whether a `\` before it makes it literal.
fn unescaped(fmt: &str) -> impl Iterator<Item = (char, bool)> + '_ {
let mut chars = fmt.chars();
std::iter::from_fn(move || match chars.next()? {
'\\' => Some(chars.next().map_or(('\\', true), |c| (c, true))),
c => Some((c, false)),
})
}
impl Session {
/// Preferences ▸ Date format: the pattern Fill & Sign dates use. Rejects unusable patterns
/// (see [`check_date_format`]) and keeps the previous one.
pub fn set_date_format(&mut self, fmt: &str) -> Result<(), String> {
let fmt = check_date_format(fmt)?;
self.date_format = (fmt != DEFAULT_DATE_FORMAT).then(|| fmt.to_string());
Ok(())
}
pub fn date_format(&self) -> &str {
self.date_format.as_deref().unwrap_or(DEFAULT_DATE_FORMAT)
}
/// Preferences ▸ Date format ▸ Language: the language of month and weekday names, or
/// `None` to follow the interface language. Rejects codes not in [`DATE_LANGUAGES`].
pub fn set_date_language(&mut self, code: Option<&str>) -> Result<(), String> {
self.date_language = match code {
None => None,
Some(c) => Some(date_language(c).map(|l| l.code.to_string()).ok_or_else(|| {
let codes: Vec<&str> = DATE_LANGUAGES.iter().map(|l| l.code).collect();
format!("unknown date language {c:?} (use one of {})", codes.join(", "))
})?),
};
Ok(())
}
pub fn date_language(&self) -> Option<&str> {
self.date_language.as_deref()
}
/// Today's local date in `fmt` (or the session's date format) with the names of `lang`
/// (or the session's date language; English when neither is set or known).
pub fn today_text(&self, fmt: Option<&str>, lang: Option<&str>) -> Result<String, String> {
let fmt = match fmt {
Some(f) => check_date_format(f)?,
None => self.date_format(),
};
let lang = lang.or(self.date_language()).and_then(date_language).unwrap_or(&DATE_LANGUAGES[0]);
Ok(format_day(fmt, self.today(), lang))
}
/// [`Session::today_text`] for writing into a PDF: refused when the date has characters
/// Fill & Sign can't write yet (see [`unwritable`]), rather than saving them as `?`.
pub fn today_text_for_pdf(&self, fmt: Option<&str>, lang: Option<&str>) -> Result<String, String> {
let text = self.today_text(fmt, lang)?;
match unwritable(&text) {
bad if bad.is_empty() => Ok(text),
bad => Err(format!(
"\"{text}\" can't be written into the PDF yet: Fill & Sign text is Western European only ({bad}); pick a numeric format such as dd.mm.yyyy"
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DAY: (i64, u32, u32) = (2026, 3, 7);
const EN: &DateLanguage = &DATE_LANGUAGES[0];
fn in_lang(fmt: &str, code: &str) -> String {
format_day(fmt, DAY, date_language(code).unwrap())
}
#[test]
fn presets_write_the_day_in_their_own_order() {
let shown: Vec<String> = DATE_FORMATS.iter().map(|f| format_day(f, DAY, EN)).collect();
assert_eq!(
shown,
[
"3/7/2026",
"03/07/2026",
"7/3/2026",
"07/03/2026",
"07.03.2026",
"2026-03-07",
"2026.03.07.",
"2026/03/07",
"2026/3/7",
"7 Mar 2026",
"7 March 2026",
"Mar 7, 2026",
"March 7, 2026",
"Saturday, March 7, 2026",
]
);
assert!(DATE_FORMATS.iter().all(|f| check_date_format(f).is_ok()));
assert_eq!(DATE_FORMATS[0], DEFAULT_DATE_FORMAT);
}
#[test]
fn escaped_letters_are_shown_as_is() {
assert_eq!(format_day("d \\de mmmm \\de yyyy", DAY, EN), "7 de March de 2026");
assert_eq!(format_day("yyyy\\", DAY, EN), "2026\\");
assert_eq!(check_date_format("d \\de mmmm"), Ok("d \\de mmmm"));
}
#[test]
fn unusable_patterns_are_rejected() {
assert!(check_date_format(" ").is_err());
assert!(check_date_format("Date: dd/mm").unwrap_err().contains("`t`"));
assert!(check_date_format("mm/dd HH:MM").is_err());
assert!(check_date_format("Year").is_err(), "no date letters");
assert!(check_date_format(&"d".repeat(MAX_DATE_FORMAT_CHARS + 1)).is_err());
assert_eq!(check_date_format(" yyyy-mm-dd "), Ok("yyyy-mm-dd"));
}
#[test]
fn only_whole_date_codes_are_accepted() {
for (fmt, code) in [("yyyyyyyy", "`yyyyyyyy`"), ("yyy", "`yyy`"), ("d/m/y", "`y`"), ("mmmmm d", "`mmmmm`"), ("ddddd", "`ddddd`")] {
let e = check_date_format(fmt).unwrap_err();
assert!(e.contains(code) && e.contains("is not a date code"), "{fmt}: {e}");
}
for fmt in ["yy", "yyyy", "m", "mm", "mmm", "mmmm", "d", "dd", "ddd", "dddd", r"\y\y\y yyyy"] {
assert!(check_date_format(fmt).is_ok(), "{fmt}");
}
}
#[test]
fn hostile_days_and_patterns_do_not_panic() {
for fmt in ["mmmm", "mmm", "dddd", "ddd", "\\", "yyyyyyyyy", "é d ü", "d. mmmm"] {
for day in [(i64::MAX, u32::MAX, u32::MAX), (i64::MIN, 0, 0), (2026, 13, 40), (0, 2, 29)] {
for lang in &DATE_LANGUAGES {
let _ = format_day(fmt, day, lang);
}
}
}
assert_eq!(format_day("mmmm dddd", (2026, 13, 40), EN), "13 40", "out of range shows numbers");
}
#[test]
fn the_session_stamps_today_in_its_format() {
// 2023-11-14 (UTC) on the injected clock.
let mut s = Session::new().with_clock(|| 1_700_000_000);
assert_eq!(s.date_format(), DEFAULT_DATE_FORMAT);
assert_eq!(s.today_text(None, None).as_deref(), Ok("11/14/2023"));
s.set_date_format("dd.mm.yyyy").unwrap();
assert_eq!(s.today_text(None, None).as_deref(), Ok("14.11.2023"));
assert_eq!(s.today_text(Some("yyyy-mm-dd"), None).as_deref(), Ok("2023-11-14"));
assert!(s.today_text(Some("HH"), None).is_err());
assert!(s.set_date_format("nothing").is_err());
assert_eq!(s.date_format(), "dd.mm.yyyy", "a rejected pattern keeps the previous one");
}
/// 21:30 on 14 Oct 2026 in São Paulo (UTC-3) is 00:30Z on the 15th. The date Fill & Sign
/// stamps is the wall-clock day (14th); the time the file records (`/M`, `/ModDate`) is UTC.
/// The test shortcut (`with_clock`) takes the injected time as local and says the 15th, which
/// is why it cannot serve a host that knows both the UTC time and the zone.
#[test]
fn a_wall_clock_gives_the_local_day_and_keeps_utc_for_the_file() {
let sao_paulo = Session::new().with_wall_clock(|| 1_792_024_200, || -3 * 3600);
assert_eq!(sao_paulo.today(), (2026, 10, 14));
assert_eq!(sao_paulo.today_text(None, None).as_deref(), Ok("10/14/2026"));
assert_eq!(sao_paulo.signing_date(), "D:20261014213000-03'00'");
assert_eq!(sao_paulo.stamp_by_line("Ada"), "By Ada at 9:30 pm, Oct 14, 2026");
assert_eq!(sao_paulo.now_secs(), 1_792_024_200, "the file's instant stays in UTC");
let utc = Session::new().with_wall_clock(|| 1_792_024_200, || 0);
assert_eq!(utc.today(), (2026, 10, 15));
let clock_only = Session::new().with_clock(|| 1_792_024_200);
assert_eq!(clock_only.today(), (2026, 10, 15), "on its own, an injected clock counts as local time");
}
#[test]
fn names_follow_the_date_language() {
assert_eq!(in_lang("dddd, d \\de mmmm \\de yyyy", "es"), "sábado, 7 de marzo de 2026");
assert_eq!(in_lang("ddd d mmm", "pt-br"), "sáb 7 mar");
assert_eq!(in_lang("dddd d mmmm yyyy", "fr"), "samedi 7 mars 2026");
assert_eq!(in_lang("dddd d mmmm yyyy", "it"), "sabato 7 marzo 2026");
assert_eq!(in_lang("ddd d mmm", "it"), "sab 7 mar");
assert_eq!(in_lang("dddd, d mmmm", "te"), "శనివారం, 7 మార్చి");
assert_eq!(in_lang("dddd, d mmmm", "ar"), "السبت, 7 مارس");
assert_eq!(in_lang("yyyy\\年mmmmd\\日 dddd", "ja"), "2026年3月7日 土曜日");
assert_eq!(in_lang("yyyy\\年mmmmd\\日 dddd", "zh-hans"), "2026年三月7日 星期六");
assert_eq!(in_lang("ddd", "zh-hant"), "週六");
assert_eq!(in_lang("dddd, d mmmm yyyy", "sq"), "e shtunë, 7 mars 2026");
assert_eq!(in_lang("ddd d mmm", "sq"), "sht 7 mar");
assert_eq!(in_lang("dddd d. mmmm yyyy", "nb"), "lørdag 7. mars 2026");
assert_eq!(in_lang("ddd d. mmm", "nb"), "lør. 7. mar.");
assert_eq!(date_language("PT-BR").map(|l| l.code), Some("pt-br"));
assert!(date_language("xx").is_none());
for l in &DATE_LANGUAGES {
assert!(l.months.iter().chain(&l.months_short).chain(&l.days).chain(&l.days_short).all(|n| !n.is_empty()), "{}", l.code);
}
}
#[test]
fn czech_and_russian_months_change_after_a_day() {
assert_eq!(in_lang("d. mmmm yyyy", "cs"), "7. března 2026");
assert_eq!(in_lang("mmmm yyyy", "cs"), "březen 2026");
assert_eq!(in_lang("d mmmm yyyy", "ru"), "7 марта 2026");
assert_eq!(in_lang("mmmm yyyy", "ru"), "март 2026");
// A weekday (dddd) is not a day number.
assert_eq!(in_lang("dddd, mmmm", "cs"), "sobota, březen");
}
/// Indonesian month and weekday names are the same with and after a day number. (Vietnamese
/// has its own test upstream: its numeric month forms are writable, weekday names are not.)
#[test]
fn indonesian_month_and_weekday_names() {
assert_eq!(in_lang("d mmmm yyyy", "id"), "7 Maret 2026");
assert_eq!(in_lang("mmmm yyyy", "id"), "Maret 2026");
assert_eq!(in_lang("dddd, d mmmm yyyy", "id"), "Sabtu, 7 Maret 2026");
// Indonesian month names are Latin, so they reach a PDF.
let mut s = Session::new().with_clock(|| 1_700_000_000);
s.set_date_language(Some("id")).unwrap();
assert_eq!(s.today_text_for_pdf(Some("d mmmm yyyy"), None).as_deref(), Ok("14 November 2023"));
}
/// Hindi and Korean names are the same with and after a day number. Neither is Western
/// European, so the date-text path refuses them with what to use instead.
#[test]
fn hindi_and_korean_month_and_weekday_names() {
assert_eq!(in_lang("d mmmm yyyy", "hi"), "7 मार्च 2026");
assert_eq!(in_lang("dddd, mmmm", "hi"), "शनिवार, मार्च");
assert_eq!(in_lang("ddd, d mmm", "hi"), "शनि, 7 मार्च");
assert_eq!(in_lang("yyyy\\년 mmmm d\\일 dddd", "ko"), "2026년 3월 7일 토요일");
assert_eq!(in_lang("ddd", "ko"), "토");
let mut s = Session::new().with_clock(|| 1_700_000_000);
for code in ["hi", "ko"] {
s.set_date_language(Some(code)).unwrap();
assert!(s.today_text_for_pdf(Some("d mmmm yyyy"), None).unwrap_err().contains("dd.mm.yyyy"), "{code}");
}
}
#[test]
fn catalan_months_elide_the_preposition_after_a_day() {
assert_eq!(in_lang("dddd d mmmm \\de yyyy", "ca"), "dissabte 7 de març de 2026");
assert_eq!(in_lang("mmmm yyyy", "ca"), "març 2026");
let ca = date_language("ca").unwrap();
assert_eq!(format_day("d mmmm \\de yyyy", (2026, 4, 23), ca), "23 d'abril de 2026");
assert_eq!(format_day("d mmmm", (2026, 8, 1), ca), "1 d'agost");
assert_eq!(format_day("d mmmm", (2026, 10, 8), ca), "8 d'octubre");
assert_eq!(format_day("mmmm", (2026, 10, 8), ca), "octubre", "no preposition without a day");
}
#[test]
fn the_session_writes_names_in_its_date_language() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
s.set_date_format("dddd d mmmm").unwrap();
assert_eq!(s.today_text(None, None).as_deref(), Ok("Tuesday 14 November"), "English by default");
assert_eq!(s.today_text(None, Some("es")).as_deref(), Ok("martes 14 noviembre"), "a caller's language");
s.set_date_language(Some("PT-BR")).unwrap();
assert_eq!(s.date_language(), Some("pt-br"));
assert_eq!(s.today_text(None, None).as_deref(), Ok("terça-feira 14 novembro"));
assert_eq!(s.today_text(Some("d mmmm"), Some("uk")).as_deref(), Ok("14 листопада"), "Ukrainian months after a day");
assert_eq!(s.today_text(Some("dddd d mmmm"), Some("de")).as_deref(), Ok("Dienstag 14 November"));
assert!(s.set_date_language(Some("xx")).unwrap_err().contains("zh-hant"), "only the interface languages");
assert_eq!(s.date_language(), Some("pt-br"), "an unknown language keeps the previous one");
s.set_date_language(Some("LT")).unwrap();
assert_eq!(s.date_language(), Some("lt"));
assert_eq!(s.today_text(Some("dddd d mmmm"), None).as_deref(), Ok("antradienis 14 lapkričio"));
s.set_date_language(Some("IT")).unwrap();
assert_eq!(s.date_language(), Some("it"));
assert_eq!(s.today_text_for_pdf(None, None).as_deref(), Ok("martedì 14 novembre"), "Italian names and accents can be written into a PDF");
s.set_date_language(None).unwrap();
assert_eq!(s.date_language(), None);
}
#[test]
fn romanian_month_and_weekday_names() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
s.set_date_language(Some("ro")).unwrap();
assert_eq!(s.date_language(), Some("ro"));
s.set_date_format("d mmmm yyyy").unwrap();
assert_eq!(s.today_text(None, None).as_deref(), Ok("14 noiembrie 2023"));
s.set_date_format("mmmm yyyy").unwrap();
assert_eq!(s.today_text(None, None).as_deref(), Ok("noiembrie 2023"));
s.set_date_format("ddd, d mmm").unwrap();
assert_eq!(s.today_text(None, None).as_deref(), Ok("mar., 14 nov."));
// `14 noiembrie 2023` is WinAnsi; the weekday `marți` (ț, comma below) is not, so Fill & Sign refuses that format.
assert_eq!(s.today_text_for_pdf(Some("d mmmm yyyy"), Some("ro")).as_deref(), Ok("14 noiembrie 2023"));
let e = s.today_text_for_pdf(Some("dddd d mmmm yyyy"), Some("ro")).unwrap_err();
assert!(e.contains("can't be written into the PDF yet"), "{e}");
}
#[test]
fn vietnamese_month_and_weekday_names() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
s.set_date_language(Some("vi")).unwrap();
assert_eq!(s.date_language(), Some("vi"));
s.set_date_format("d mmmm yyyy").unwrap();
assert_eq!(s.today_text(None, None).as_deref(), Ok("14 tháng 11 2023"));
s.set_date_format("mmmm yyyy").unwrap();
assert_eq!(s.today_text(None, None).as_deref(), Ok("Tháng 11 2023"));
s.set_date_format("ddd, d mmm").unwrap();
assert_eq!(s.today_text(None, None).as_deref(), Ok("T3, 14 Thg 11"));
// Encoding follows the spelled text: `14 tháng 11 2023` is WinAnsi-representable;
// a weekday name such as `Thứ Ba` is not, so Fill & Sign refuses that format.
assert_eq!(s.today_text_for_pdf(Some("d mmmm yyyy"), Some("vi")).as_deref(), Ok("14 tháng 11 2023"));
let e = s.today_text_for_pdf(Some("dddd d mmmm yyyy"), Some("vi")).unwrap_err();
assert!(e.contains("can't be written into the PDF yet") && e.contains("dd.mm.yyyy"), "{e}");
assert!(s.today_text_for_pdf(Some("dd.mm.yyyy"), Some("vi")).is_ok());
}
#[test]
fn dates_the_pdf_cannot_hold_are_refused_not_damaged() {
assert_eq!(unwritable("8. října 2026"), "ř");
assert_eq!(unwritable("2026年10月8日"), "年月日");
assert_eq!(unwritable("sábado, 7 de março – 2026?"), "", "Western European, dashes and ? are fine");
assert_eq!(unwritable("lørdag 7. mars, søn. 8. mai (æøå ÆØÅ)"), "", "Norwegian letters are Western European");
let mut s = Session::new().with_clock(|| 1_700_000_000);
s.set_date_format("d. mmmm yyyy").unwrap();
assert_eq!(s.today_text_for_pdf(None, Some("es")).as_deref(), Ok("14. noviembre 2023"));
assert_eq!(s.today_text_for_pdf(None, Some("cs")).as_deref(), Ok("14. listopadu 2023"), "no letter outside WinAnsi");
let e = s.today_text_for_pdf(Some("dddd"), Some("ja")).unwrap_err();
assert!(e.contains("火曜日") && e.contains("can't be written into the PDF yet"), "{e}");
assert!(s.today_text_for_pdf(Some("dd.mm.yyyy"), Some("ja")).is_ok(), "numbers are fine in any language");
assert!(s.today_text_for_pdf(Some("yyyy\\年mm\\月"), Some("ja")).unwrap_err().contains("年月"));
}
}