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.
94 lines
4.1 KiB
TypeScript
94 lines
4.1 KiB
TypeScript
/**
|
|
* The two sentence builders, which had no tests while they were building
|
|
* English by concatenation -- and no test would have caught the thing wrong
|
|
* with them, since the English output was correct. These pin the two
|
|
* properties that matter now: every fragment goes through the catalogue, and
|
|
* the joining is Intl's rather than a hardcoded " and ".
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import { describeRule as describeSieve } from "../sieve";
|
|
import { describeRule as describeRecurrence, weekdayOptions } from "../recurrence";
|
|
import { setUiLanguageForFormatting } from "../datetime";
|
|
import { setCatalog } from "../i18n";
|
|
|
|
describe("sieve describeRule", () => {
|
|
it("names the header and operator through the catalogue", () => {
|
|
const s = describeSieve({
|
|
id: "1", name: "r", join: "allof", enabled: true,
|
|
tests: [{ type: "header", header: "subject", op: "contains", value: "invoice" }],
|
|
actions: [{ type: "fileinto", mailbox: "Work" }],
|
|
} as never);
|
|
expect(s).toContain("Subject");
|
|
expect(s).toContain("contains");
|
|
expect(s).toContain("invoice");
|
|
expect(s).toContain("Work");
|
|
});
|
|
|
|
it("joins an allof rule as a conjunction and anyof as a disjunction", () => {
|
|
const base = {
|
|
id: "1", name: "r", enabled: true,
|
|
tests: [
|
|
{ type: "header", header: "from", op: "is", value: "a@b" },
|
|
{ type: "header", header: "to", op: "is", value: "c@d" },
|
|
],
|
|
actions: [{ type: "keep" }],
|
|
};
|
|
expect(describeSieve({ ...base, join: "allof" } as never)).toContain(" and ");
|
|
expect(describeSieve({ ...base, join: "anyof" } as never)).toContain(" or ");
|
|
});
|
|
|
|
it("says 'always' when a rule has no tests", () => {
|
|
const s = describeSieve({ id: "1", name: "r", join: "allof", enabled: true, tests: [], actions: [{ type: "stop" }] } as never);
|
|
expect(s).toContain("always");
|
|
});
|
|
});
|
|
|
|
describe("recurrence describeRule", () => {
|
|
it("describes the simple frequencies", () => {
|
|
expect(describeRecurrence(undefined)).toBe("Does not repeat");
|
|
expect(describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily" } as never)).toBe("Daily");
|
|
expect(describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily", interval: 3 } as never)).toBe("Every 3 days");
|
|
});
|
|
|
|
it("recognises Monday to Friday as every weekday", () => {
|
|
const rule = {
|
|
"@type": "RecurrenceRule", frequency: "weekly",
|
|
byDay: ["mo", "tu", "we", "th", "fr"].map((day) => ({ "@type": "NDay", day })),
|
|
};
|
|
expect(describeRecurrence(rule as never)).toBe("Every weekday");
|
|
});
|
|
|
|
it("uses a word, not a suffix, for the nth weekday of a month", () => {
|
|
const s = describeRecurrence({
|
|
"@type": "RecurrenceRule", frequency: "monthly",
|
|
byDay: [{ "@type": "NDay", day: "tu", nthOfPeriod: 2 }],
|
|
} as never);
|
|
expect(s).toContain("second");
|
|
expect(s).not.toContain("2nd");
|
|
});
|
|
|
|
it("wraps the sentence for count and until rather than appending to it", () => {
|
|
const s = describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily", count: 5 } as never);
|
|
expect(s).toBe("Daily, 5 times");
|
|
const u = describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily", until: "2026-05-03T00:00:00" } as never);
|
|
expect(u).toBe("Daily, until 2026-05-03");
|
|
});
|
|
|
|
it("takes its weekday names from the locale, not a table of English", () => {
|
|
setUiLanguageForFormatting("de-DE");
|
|
const names = weekdayOptions().map((w) => w.label);
|
|
expect(names[0]).toBe("Montag");
|
|
expect(names).toHaveLength(7);
|
|
// The narrow forms collide in English ("T" for both Tuesday and Thursday),
|
|
// which is why they cannot be catalogue keys and come from Intl instead.
|
|
expect(weekdayOptions().map((w) => w.short)).toHaveLength(7);
|
|
setUiLanguageForFormatting(null);
|
|
});
|
|
|
|
it("renders a translated rule through the catalogue", () => {
|
|
setCatalog("de", { strings: { Daily: "Täglich" }, plurals: {} });
|
|
expect(describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily" } as never)).toBe("Täglich");
|
|
setCatalog("en", { strings: {}, plurals: {} });
|
|
});
|
|
});
|