Build the two rule descriptions as sentences, not fragments
Both describeRule functions assembled their output by concatenation, which no catalogue could fix. A translator handed " and " or " on " in isolation cannot move it: German puts the verb last, Japanese does not separate list items with a word at all, and the fragments arrive in an order the English sentence chose. Reported by a native speaker reviewing the German catalogue (#247), whose "the summaries" item is the Sieve one. Every branch is now one whole sentence with placeholders, so a translator rewrites the sentence including its word order. Joining is Intl.ListFormat, which gives "A, B und C" for an allof rule and the language's own disjunction for anyof, rather than a hardcoded " and " that would be wrong twice over. The recurrence tail no longer appends: ", 5 times" and ", until 2026-05-03" wrap the sentence they qualify, so a language that puts the limit first can. Ordinals become words. The old suffix table -- st, nd, rd, th, picked by arithmetic -- is English spelling rules in code, and no catalogue can reach a suffix chosen that way. German writes "1.", Japanese "第1". nthOfPeriod is 1-5 or -1 in practice, so five words and "last" cover it. WEEKDAYS is gone. Its long names could have been catalogue entries but its short ones never could: "T" is Tuesday and Thursday, "S" is Saturday and Sunday, and a catalogue cannot hold two translations under one key. That was bad data rather than missing translation, and Intl has every name in every locale in three widths. lib/datetime.ts gains weekdayName, weekdayNames and formatList; recurrence.ts keeps WEEKDAY_KEYS for the ordering, which is not a language question. Adds the first tests either function has had. Neither had any, and no test would have caught what was wrong with them, since the English output was correct -- so these pin the two properties that actually matter: fragments go through the catalogue, and the joining is Intl's. 32 strings and 9 plural forms are new and land with each language. Verified: typecheck clean, 1009 tests pass.
This commit is contained in:
@@ -557,3 +557,48 @@ export function localeOptions(): LocaleOption[] {
|
||||
optionsExtras = extras;
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Weekday names in the reader's locale, indexed by JSCalendar's two-letter day.
|
||||
*
|
||||
* These used to be a table of English strings with a `short` of "M", "T", "W"…
|
||||
* which could not become catalogue entries at all: "T" is both Tuesday and
|
||||
* Thursday and "S" is both Saturday and Sunday, so the key collides with
|
||||
* itself. A catalogue cannot hold two translations under one key, and no
|
||||
* amount of translating fixes that — the data was wrong, not the wiring.
|
||||
*
|
||||
* Intl has the names already, in every locale, in three widths, and gets the
|
||||
* plural and capitalisation conventions right without anybody maintaining a
|
||||
* list. 2026-06-01 is a Monday; the rest follow from it.
|
||||
*/
|
||||
export type WeekdayKey = "mo" | "tu" | "we" | "th" | "fr" | "sa" | "su";
|
||||
|
||||
const WEEKDAY_ORDER: WeekdayKey[] = ["mo", "tu", "we", "th", "fr", "sa", "su"];
|
||||
const WEEKDAY_BASE = Date.UTC(2026, 5, 1); // a Monday
|
||||
|
||||
export function weekdayName(day: WeekdayKey, width: "long" | "short" | "narrow" = "long"): string {
|
||||
const i = WEEKDAY_ORDER.indexOf(day);
|
||||
if (i < 0) return day;
|
||||
return intl({ weekday: width, timeZone: "UTC" }).format(new Date(WEEKDAY_BASE + i * 86_400_000));
|
||||
}
|
||||
|
||||
/** Every weekday, Monday first, for pickers that show all seven. */
|
||||
export function weekdayNames(width: "long" | "short" | "narrow" = "long"): Array<{ key: WeekdayKey; name: string }> {
|
||||
return WEEKDAY_ORDER.map((key) => ({ key, name: weekdayName(key, width) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* "A, B and C" — or "A, B oder C", or the comma the locale actually uses.
|
||||
*
|
||||
* Joining with a translated " and " does not work: Japanese does not separate
|
||||
* list items with a word, and the last separator differs from the others in
|
||||
* English. Intl.ListFormat knows all of that.
|
||||
*/
|
||||
export function formatList(items: string[], type: "conjunction" | "disjunction" = "conjunction"): string {
|
||||
if (items.length < 2) return items[0] ?? "";
|
||||
try {
|
||||
return new Intl.ListFormat(resolvedLocale(), { style: "long", type }).format(items);
|
||||
} catch {
|
||||
return items.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user