Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17d98748c4 | ||
|
|
1070ee13bc | ||
|
|
71827a2d04 | ||
|
|
5dd0a56732 | ||
|
|
b55c8b13bc | ||
|
|
0cf9b81444 | ||
|
|
8386444ac7 | ||
|
|
e1ae97139c | ||
|
|
503eaf17ec | ||
|
|
1e02d9ebba | ||
|
|
d40dbf04b8 | ||
|
|
2a9e18f04c | ||
|
|
4d89f5e672 | ||
|
|
95e5c69e8f | ||
|
|
50d08a18e4 | ||
|
|
9a634311b2 | ||
|
|
b811c84b12 | ||
|
|
b9b01ce02c | ||
|
|
104e3c7ba0 | ||
|
|
0a03c64ff3 | ||
|
|
4c430ea995 | ||
|
|
112b3ea52f | ||
|
|
31edf33839 | ||
|
|
1a842d8d14 |
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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: {} });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { isTextEntry, keyboard } from "@/lib/keyboard";
|
||||
|
||||
/*
|
||||
* Shortcuts after a click on a checkbox (#260).
|
||||
*
|
||||
* The guard that stops "a" archiving while you are typing into the search box
|
||||
* tested `tagName === "INPUT"`, which is also true of a checkbox. A checkbox
|
||||
* keeps focus after a click, so ticking "select all" disabled every shortcut
|
||||
* until the reader clicked somewhere else — and nothing about a checkbox
|
||||
* swallows a keystroke in the first place.
|
||||
*/
|
||||
|
||||
const pressFrom = (el: Element, key: string) => {
|
||||
const e = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true });
|
||||
el.dispatchEvent(e);
|
||||
return e;
|
||||
};
|
||||
|
||||
let pop: (() => void) | null = null;
|
||||
afterEach(() => {
|
||||
pop?.();
|
||||
pop = null;
|
||||
document.body.innerHTML = "";
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("isTextEntry", () => {
|
||||
const input = (type?: string) => {
|
||||
const el = document.createElement("input");
|
||||
if (type) el.setAttribute("type", type);
|
||||
return el;
|
||||
};
|
||||
|
||||
it("is false for the inputs you cannot type into", () => {
|
||||
for (const type of ["checkbox", "radio", "button", "submit", "reset", "file", "color", "range"]) {
|
||||
expect(isTextEntry(input(type)), type).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("is true for the ones you can", () => {
|
||||
for (const type of ["text", "search", "email", "url", "tel", "password", "number", "date", "time"]) {
|
||||
expect(isTextEntry(input(type)), type).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("treats an input with no type as text, which is what the browser does", () => {
|
||||
expect(isTextEntry(input())).toBe(true);
|
||||
});
|
||||
|
||||
it("covers textarea, select and contenteditable", () => {
|
||||
expect(isTextEntry(document.createElement("textarea"))).toBe(true);
|
||||
// A select takes letters too: typing jumps to the matching option, and a
|
||||
// shortcut would steal that.
|
||||
expect(isTextEntry(document.createElement("select"))).toBe(true);
|
||||
const div = document.createElement("div");
|
||||
div.contentEditable = "true";
|
||||
Object.defineProperty(div, "isContentEditable", { value: true });
|
||||
expect(isTextEntry(div)).toBe(true);
|
||||
});
|
||||
|
||||
it("is false for a button and for nothing at all", () => {
|
||||
expect(isTextEntry(document.createElement("button"))).toBe(false);
|
||||
expect(isTextEntry(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shortcuts with a checkbox focused", () => {
|
||||
it("still fire — the reported bug", () => {
|
||||
const handler = vi.fn();
|
||||
pop = keyboard.pushScope("test", [{ keys: "e", description: "Archive", group: "Mail", handler }]);
|
||||
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
document.body.appendChild(box);
|
||||
box.focus();
|
||||
|
||||
pressFrom(box, "e");
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("still do not fire from a text field", () => {
|
||||
const handler = vi.fn();
|
||||
pop = keyboard.pushScope("test", [{ keys: "e", description: "Archive", group: "Mail", handler }]);
|
||||
|
||||
const field = document.createElement("input");
|
||||
field.type = "search";
|
||||
document.body.appendChild(field);
|
||||
field.focus();
|
||||
|
||||
pressFrom(field, "e");
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -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(", ");
|
||||
}
|
||||
}
|
||||
|
||||
+33
-3
@@ -54,9 +54,7 @@ class Keyboard {
|
||||
// Let modal dialogs and popovers handle their own keys (Escape, arrows, ...).
|
||||
if (document.querySelector(".dialog-backdrop, .popover")) return;
|
||||
const target = e.target as HTMLElement | null;
|
||||
const inInput =
|
||||
!!target &&
|
||||
(target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable);
|
||||
const inInput = isTextEntry(target);
|
||||
const combo = comboOf(e);
|
||||
if (!combo) return;
|
||||
|
||||
@@ -102,6 +100,38 @@ class Keyboard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the focused element somewhere the reader is typing?
|
||||
*
|
||||
* This guard exists so that pressing "a" in the search box searches for "a"
|
||||
* rather than archiving the message behind it. The test used to be
|
||||
* `tagName === "INPUT"`, which is true of a checkbox — and a checkbox keeps
|
||||
* focus after you click it, so ticking "select all" silently disabled every
|
||||
* shortcut until the reader clicked somewhere else (#260). Nothing about a
|
||||
* checkbox swallows a keystroke: space toggles it and the browser handles
|
||||
* that before this listener ever runs.
|
||||
*
|
||||
* So the question is not "is this an input" but "does this input take text".
|
||||
* A `<select>` does, in the sense that matters here: typing a letter jumps to
|
||||
* the option beginning with it, which a shortcut would steal.
|
||||
*/
|
||||
const TEXT_ENTRY_TYPES = new Set([
|
||||
"text", "search", "email", "url", "tel", "password", "number",
|
||||
"date", "datetime-local", "month", "time", "week",
|
||||
]);
|
||||
|
||||
export function isTextEntry(el: Element | null): boolean {
|
||||
if (!el) return false;
|
||||
const node = el as HTMLElement;
|
||||
if (node.isContentEditable) return true;
|
||||
const tag = node.tagName;
|
||||
if (tag === "TEXTAREA" || tag === "SELECT") return true;
|
||||
if (tag !== "INPUT") return false;
|
||||
// An <input> with no type attribute is a text field.
|
||||
const type = (node as HTMLInputElement).type?.toLowerCase() || "text";
|
||||
return TEXT_ENTRY_TYPES.has(type);
|
||||
}
|
||||
|
||||
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
|
||||
|
||||
export function comboOf(e: KeyboardEvent): string | null {
|
||||
|
||||
+101
-31
@@ -1,14 +1,25 @@
|
||||
import type { JSCalendarRecurrenceRule, JSCalendarNDay } from "@/jmap/types";
|
||||
import { formatList, weekdayName, weekdayNames } from "./datetime";
|
||||
import { plural, t } from "@/lib/i18n";
|
||||
|
||||
export const WEEKDAYS: Array<{ key: JSCalendarNDay["day"]; label: string; short: string }> = [
|
||||
{ key: "mo", label: "Monday", short: "M" },
|
||||
{ key: "tu", label: "Tuesday", short: "T" },
|
||||
{ key: "we", label: "Wednesday", short: "W" },
|
||||
{ key: "th", label: "Thursday", short: "T" },
|
||||
{ key: "fr", label: "Friday", short: "F" },
|
||||
{ key: "sa", label: "Saturday", short: "S" },
|
||||
{ key: "su", label: "Sunday", short: "S" },
|
||||
];
|
||||
/**
|
||||
* The seven days, Monday first, named in the reader's locale.
|
||||
*
|
||||
* This was a table of English strings carrying `label: "Monday"` and
|
||||
* `short: "M"`, rendered straight into the picker. The long names could have
|
||||
* become catalogue entries; the short ones could not, because "T" is both
|
||||
* Tuesday and Thursday and "S" is both Saturday and Sunday, and a catalogue
|
||||
* cannot hold two translations under one key. Intl knows all of them.
|
||||
*/
|
||||
export const WEEKDAY_KEYS: Array<JSCalendarNDay["day"]> = ["mo", "tu", "we", "th", "fr", "sa", "su"];
|
||||
|
||||
export function weekdayOptions(): Array<{ key: JSCalendarNDay["day"]; label: string; short: string }> {
|
||||
return weekdayNames("long").map(({ key, name }) => ({
|
||||
key: key as JSCalendarNDay["day"],
|
||||
label: name,
|
||||
short: weekdayName(key, "narrow"),
|
||||
}));
|
||||
}
|
||||
|
||||
export type RecurrencePreset = "none" | "daily" | "weekly" | "weekdays" | "monthly" | "yearly" | "custom";
|
||||
|
||||
@@ -28,7 +39,7 @@ export function presetFor(rule: JSCalendarRecurrenceRule | undefined): Recurrenc
|
||||
}
|
||||
|
||||
export function ruleFromPreset(preset: RecurrencePreset, start: Date): JSCalendarRecurrenceRule | undefined {
|
||||
const dow = WEEKDAYS[(start.getDay() + 6) % 7]!.key;
|
||||
const dow = WEEKDAY_KEYS[(start.getDay() + 6) % 7]!;
|
||||
switch (preset) {
|
||||
case "daily":
|
||||
return { "@type": "RecurrenceRule", frequency: "daily" };
|
||||
@@ -45,48 +56,107 @@ export function ruleFromPreset(preset: RecurrencePreset, start: Date): JSCalenda
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A recurrence rule as a sentence.
|
||||
*
|
||||
* Built as whole sentences with placeholders rather than by concatenation.
|
||||
* The old version appended fragments -- `base += " on " + names` -- which is
|
||||
* untranslatable however complete the catalogue is: German puts the weekday
|
||||
* list somewhere else in the clause, and a translator handed " on " alone
|
||||
* cannot move it. Every branch below is one key a translator can rewrite in
|
||||
* full, including the word order.
|
||||
*/
|
||||
export function describeRule(rule: JSCalendarRecurrenceRule | undefined): string {
|
||||
if (!rule) return "Does not repeat";
|
||||
if (!rule) return t("Does not repeat");
|
||||
const n = rule.interval ?? 1;
|
||||
const every = n !== 1;
|
||||
let base: string;
|
||||
|
||||
switch (rule.frequency) {
|
||||
case "daily":
|
||||
base = n === 1 ? "Daily" : `Every ${n} days`;
|
||||
base = every ? plural(n, { one: "Every {n} day", other: "Every {n} days" }) : t("Daily");
|
||||
break;
|
||||
|
||||
case "weekly": {
|
||||
base = n === 1 ? "Weekly" : `Every ${n} weeks`;
|
||||
if (rule.byDay?.length) {
|
||||
const names = rule.byDay.map((d) => WEEKDAYS.find((w) => w.key === d.day)?.label ?? d.day);
|
||||
const set = rule.byDay.map((d) => d.day).sort().join(",");
|
||||
if (set === ["mo", "tu", "we", "th", "fr"].sort().join(",") && n === 1) base = "Every weekday";
|
||||
else base += ` on ${names.join(", ")}`;
|
||||
const days = rule.byDay?.length ? rule.byDay.map((d) => d.day) : [];
|
||||
const weekdaysOnly =
|
||||
days.length === 5 && ["mo", "tu", "we", "th", "fr"].every((d) => days.includes(d as JSCalendarNDay["day"]));
|
||||
if (weekdaysOnly && !every) {
|
||||
base = t("Every weekday");
|
||||
} else if (days.length) {
|
||||
const list = formatList(days.map((d) => weekdayName(d as never)));
|
||||
base = every
|
||||
? plural(n, { one: "Every {n} week on {days}", other: "Every {n} weeks on {days}" }, { days: list })
|
||||
: t("Weekly on {days}", { days: list });
|
||||
} else {
|
||||
base = every ? plural(n, { one: "Every {n} week", other: "Every {n} weeks" }) : t("Weekly");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "monthly": {
|
||||
base = n === 1 ? "Monthly" : `Every ${n} months`;
|
||||
if (rule.byMonthDay?.length) base += ` on day ${rule.byMonthDay.join(", ")}`;
|
||||
else if (rule.byDay?.length) {
|
||||
if (rule.byMonthDay?.length) {
|
||||
const list = formatList(rule.byMonthDay.map(String));
|
||||
base = every
|
||||
? plural(n, { one: "Every {n} month on day {days}", other: "Every {n} months on day {days}" }, { days: list })
|
||||
: t("Monthly on day {days}", { days: list });
|
||||
} else if (rule.byDay?.length) {
|
||||
const d = rule.byDay[0]!;
|
||||
const ord = d.nthOfPeriod ? ordinal(d.nthOfPeriod) + " " : "";
|
||||
base += ` on the ${ord}${WEEKDAYS.find((w) => w.key === d.day)?.label ?? d.day}`;
|
||||
const weekday = weekdayName(d.day as never);
|
||||
if (d.nthOfPeriod) {
|
||||
const ord = ordinal(d.nthOfPeriod);
|
||||
base = every
|
||||
? plural(n, { one: "Every {n} month on the {ordinal} {weekday}", other: "Every {n} months on the {ordinal} {weekday}" }, { ordinal: ord, weekday })
|
||||
: t("Monthly on the {ordinal} {weekday}", { ordinal: ord, weekday });
|
||||
} else {
|
||||
base = every
|
||||
? plural(n, { one: "Every {n} month on {weekday}", other: "Every {n} months on {weekday}" }, { weekday })
|
||||
: t("Monthly on {weekday}", { weekday });
|
||||
}
|
||||
} else {
|
||||
base = every ? plural(n, { one: "Every {n} month", other: "Every {n} months" }) : t("Monthly");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "yearly":
|
||||
base = n === 1 ? "Yearly" : `Every ${n} years`;
|
||||
base = every ? plural(n, { one: "Every {n} year", other: "Every {n} years" }) : t("Yearly");
|
||||
break;
|
||||
|
||||
default:
|
||||
base = `Every ${n} ${rule.frequency}`;
|
||||
// An RFC frequency this build has no sentence for. The frequency word
|
||||
// itself stays as the server sent it rather than being invented.
|
||||
base = t("Every {n} {frequency}", { n, frequency: rule.frequency });
|
||||
}
|
||||
|
||||
// The tail wraps the sentence rather than being glued to its end, so a
|
||||
// translator can put "until 3 May" first if that is what the language does.
|
||||
if (rule.count) {
|
||||
base = plural(rule.count, { one: "{rule}, {n} time", other: "{rule}, {n} times" }, { rule: base });
|
||||
}
|
||||
if (rule.until) {
|
||||
base = t("{rule}, until {date}", { rule: base, date: rule.until.slice(0, 10) });
|
||||
}
|
||||
if (rule.count) base += `, ${rule.count} times`;
|
||||
if (rule.until) base += `, until ${rule.until.slice(0, 10)}`;
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* "first", "second", "last" -- words, not "1st".
|
||||
*
|
||||
* The suffix table this replaced ("st", "nd", "rd", "th") is English spelling
|
||||
* rules in code: German writes "1.", Japanese "第1", and no catalogue can
|
||||
* reach a suffix chosen by arithmetic. JSCalendar's nthOfPeriod is 1-5 or -1
|
||||
* in practice, so five words and "last" cover it; anything else falls back to
|
||||
* the bare number, which is wrong in no language.
|
||||
*/
|
||||
function ordinal(n: number): string {
|
||||
if (n === -1) return "last";
|
||||
const s = ["th", "st", "nd", "rd"];
|
||||
const v = n % 100;
|
||||
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]!);
|
||||
switch (n) {
|
||||
case -1: return t("last");
|
||||
case 1: return t("first");
|
||||
case 2: return t("second");
|
||||
case 3: return t("third");
|
||||
case 4: return t("fourth");
|
||||
case 5: return t("fifth");
|
||||
default: return String(n);
|
||||
}
|
||||
}
|
||||
|
||||
+58
-44
@@ -6,6 +6,9 @@
|
||||
* Sieve below each comment is what the server actually runs.
|
||||
*/
|
||||
|
||||
import { formatList } from "./datetime";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export type HeaderOp = "contains" | "notcontains" | "is" | "notis" | "matches" | "notmatches" | "regex" | "notregex" | "exists" | "notexists";
|
||||
|
||||
export type SieveTest =
|
||||
@@ -318,49 +321,60 @@ export function reorderRules(rules: SieveRule[], fromId: string, toId: string, b
|
||||
return [...rest.slice(0, at), moved, ...rest.slice(at)];
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter rule as a sentence, for the rule list.
|
||||
*
|
||||
* Rebuilt as whole sentences with placeholders. The old version concatenated
|
||||
* fragments -- a header name, an operator, a quoted value, joined by " and "
|
||||
* -- which no catalogue could fix: German puts the verb last, Japanese does
|
||||
* not separate list items with a word at all, and a translator handed " and "
|
||||
* on its own cannot move anything. Reported by a native speaker reviewing the
|
||||
* German catalogue (#247).
|
||||
*
|
||||
* Intl.ListFormat does the joining, so "A, B and C" becomes "A, B und C" and,
|
||||
* for an anyof rule, the disjunction the language actually uses.
|
||||
*/
|
||||
export function describeRule(r: SieveRule): string {
|
||||
const tests = r.tests
|
||||
.map((t) => {
|
||||
switch (t.type) {
|
||||
case "header":
|
||||
return `${t.header} ${HEADER_OPS.find((o) => o.value === t.op)?.label ?? t.op} "${t.value}"`;
|
||||
case "address":
|
||||
return `${t.header} address ${HEADER_OPS.find((o) => o.value === t.op)?.label ?? t.op} "${t.value}"`;
|
||||
case "size":
|
||||
return `size ${t.op} ${Math.round(t.value / 1024)} KB`;
|
||||
case "body":
|
||||
return `body ${t.op === "contains" ? "contains" : "does not contain"} "${t.value}"`;
|
||||
case "true":
|
||||
return "always";
|
||||
}
|
||||
})
|
||||
.join(r.join === "allof" ? " and " : " or ");
|
||||
const actions = r.actions
|
||||
.map((a) => {
|
||||
switch (a.type) {
|
||||
case "fileinto":
|
||||
return `move to ${a.mailbox}`;
|
||||
case "redirect":
|
||||
return `forward to ${a.address}`;
|
||||
case "discard":
|
||||
return "delete";
|
||||
case "keep":
|
||||
return "keep";
|
||||
case "reject":
|
||||
return "reject";
|
||||
case "markread":
|
||||
return "mark read";
|
||||
case "flag":
|
||||
return "star";
|
||||
case "addflag":
|
||||
case "setflag":
|
||||
return `add ${a.flag}`;
|
||||
case "removeflag":
|
||||
return `remove ${a.flag}`;
|
||||
case "stop":
|
||||
return "stop";
|
||||
}
|
||||
})
|
||||
.join(", ");
|
||||
return `${tests || "always"} → ${actions}`;
|
||||
const headerLabel = (h: string): string => t(HEADER_CHOICES.find((c) => c.value === h)?.label ?? h);
|
||||
const opLabel = (op: string): string => t(HEADER_OPS.find((o) => o.value === op)?.label ?? op);
|
||||
|
||||
const tests = r.tests.map((test) => {
|
||||
switch (test.type) {
|
||||
case "header":
|
||||
return t('{header} {op} "{value}"', { header: headerLabel(test.header), op: opLabel(test.op), value: test.value });
|
||||
case "address":
|
||||
return t('{header} address {op} "{value}"', { header: headerLabel(test.header), op: opLabel(test.op), value: test.value });
|
||||
case "size":
|
||||
return test.op === "over"
|
||||
? t("size is over {n} KB", { n: Math.round(test.value / 1024) })
|
||||
: t("size is under {n} KB", { n: Math.round(test.value / 1024) });
|
||||
case "body":
|
||||
return test.op === "contains"
|
||||
? t('body contains "{value}"', { value: test.value })
|
||||
: t('body does not contain "{value}"', { value: test.value });
|
||||
case "true":
|
||||
return t("always");
|
||||
}
|
||||
});
|
||||
|
||||
const actions = r.actions.map((a) => {
|
||||
switch (a.type) {
|
||||
case "fileinto": return t("move to {folder}", { folder: a.mailbox });
|
||||
case "redirect": return t("forward to {address}", { address: a.address });
|
||||
case "discard": return t("delete it");
|
||||
case "keep": return t("keep it");
|
||||
case "reject": return t("reject it");
|
||||
case "markread": return t("mark it read");
|
||||
case "flag": return t("star it");
|
||||
case "addflag":
|
||||
case "setflag": return t("add {flag}", { flag: a.flag });
|
||||
case "removeflag": return t("remove {flag}", { flag: a.flag });
|
||||
case "stop": return t("stop");
|
||||
}
|
||||
});
|
||||
|
||||
return t("{tests} → {actions}", {
|
||||
tests: tests.length ? formatList(tests, r.join === "allof" ? "conjunction" : "disjunction") : t("always"),
|
||||
actions: formatList(actions, "conjunction"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1282,8 +1282,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "Diese Nachricht wurde automatisch versendet, daher wird keine Lesebestätigung angeboten.",
|
||||
"This server will not hold a message longer than {span}.": "Dieser Server hält eine Nachricht nicht länger als {span} zurück.",
|
||||
"Upload failed": "Hochladen fehlgeschlagen",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "Alle {n} {frequency}",
|
||||
"Monthly": "Monatlich",
|
||||
"Monthly on day {days}": "Monatlich am Tag {days}",
|
||||
"Monthly on the {ordinal} {weekday}": "Monatlich am {ordinal} {weekday}",
|
||||
"Monthly on {weekday}": "Monatlich am {weekday}",
|
||||
"Weekly": "Wöchentlich",
|
||||
"Weekly on {days}": "Wöchentlich am {days}",
|
||||
"add {flag}": "{flag} hinzufügen",
|
||||
"always": "immer",
|
||||
"body contains \"{value}\"": "Text enthält \"{value}\"",
|
||||
"body does not contain \"{value}\"": "Text enthält nicht \"{value}\"",
|
||||
"delete it": "löschen",
|
||||
"fifth": "fünften",
|
||||
"first": "ersten",
|
||||
"forward to {address}": "weiterleiten an {address}",
|
||||
"fourth": "vierten",
|
||||
"keep it": "behalten",
|
||||
"last": "letzten",
|
||||
"mark it read": "als gelesen markieren",
|
||||
"move to {folder}": "verschieben nach {folder}",
|
||||
"reject it": "abweisen",
|
||||
"remove {flag}": "{flag} entfernen",
|
||||
"second": "zweiten",
|
||||
"size is over {n} KB": "Größe über {n} KB",
|
||||
"size is under {n} KB": "Größe unter {n} KB",
|
||||
"star it": "markieren",
|
||||
"stop": "anhalten",
|
||||
"third": "dritten",
|
||||
"{header} address {op} \"{value}\"": "{header}-Adresse {op} \"{value}\"",
|
||||
"{header} {op} \"{value}\"": "{header} {op} \"{value}\"",
|
||||
"{rule}, until {date}": "{rule}, bis {date}",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { one: "{n} Element löschen", other: "{n} Elemente löschen" },
|
||||
"Delete {n} items?": { one: "{n} Element löschen?", other: "{n} Elemente löschen?" },
|
||||
"Move {n} items": { one: "{n} Element verschieben", other: "{n} Elemente verschieben" },
|
||||
"Move {n} items…": { one: "{n} Element verschieben…", other: "{n} Elemente verschieben…" },
|
||||
"The event runs {n} days longer than this shows.": { one: "Der Termin dauert {n} Tag länger, als hier angezeigt wird.", other: "Der Termin dauert {n} Tage länger, als hier angezeigt wird." },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { one: "{n} Gast ist nicht auf diesem Server, daher gibt es dafür keine Frei/Gebucht-Informationen.", other: "{n} Gäste sind nicht auf diesem Server, daher gibt es dafür keine Frei/Gebucht-Informationen." },
|
||||
"{n} items selected": { one: "{n} Element ausgewählt", other: "{n} Elemente ausgewählt" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { one: "Jeden Tag", other: "Alle {n} Tage" },
|
||||
"Every {n} months": { one: "Jeden Monat", other: "Alle {n} Monate" },
|
||||
"Every {n} months on day {days}": { one: "Jeden Monat am Tag {days}", other: "Alle {n} Monate am Tag {days}" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { one: "Jeden Monat am {ordinal} {weekday}", other: "Alle {n} Monate am {ordinal} {weekday}" },
|
||||
"Every {n} months on {weekday}": { one: "Jeden Monat am {weekday}", other: "Alle {n} Monate am {weekday}" },
|
||||
"Every {n} weeks": { one: "Jede Woche", other: "Alle {n} Wochen" },
|
||||
"Every {n} weeks on {days}": { one: "Jede Woche am {days}", other: "Alle {n} Wochen am {days}" },
|
||||
"Every {n} years": { one: "Jedes Jahr", other: "Alle {n} Jahre" },
|
||||
"{rule}, {n} times": { one: "{rule}, {n}-mal", other: "{rule}, {n}-mal" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { one: "{n} Nachricht in den Papierkorb verschieben?", other: "{n} Nachrichten in den Papierkorb verschieben?" },
|
||||
"{n} days": { one: "{n} Tag", other: "{n} Tage" },
|
||||
|
||||
@@ -1255,8 +1255,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "Este mensaje se envió automáticamente, así que no se ofrece confirmación de lectura.",
|
||||
"This server will not hold a message longer than {span}.": "Este servidor no retiene un mensaje más de {span}.",
|
||||
"Upload failed": "Error al subir",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "Cada {n} {frequency}",
|
||||
"Monthly": "Mensualmente",
|
||||
"Monthly on day {days}": "Mensualmente el día {days}",
|
||||
"Monthly on the {ordinal} {weekday}": "Mensualmente el {ordinal} {weekday}",
|
||||
"Monthly on {weekday}": "Mensualmente el {weekday}",
|
||||
"Weekly": "Semanalmente",
|
||||
"Weekly on {days}": "Semanalmente los {days}",
|
||||
"add {flag}": "añadir {flag}",
|
||||
"always": "siempre",
|
||||
"body contains \"{value}\"": "el cuerpo contiene \"{value}\"",
|
||||
"body does not contain \"{value}\"": "el cuerpo no contiene \"{value}\"",
|
||||
"delete it": "eliminarlo",
|
||||
"fifth": "quinto",
|
||||
"first": "primer",
|
||||
"forward to {address}": "reenviar a {address}",
|
||||
"fourth": "cuarto",
|
||||
"keep it": "conservarlo",
|
||||
"last": "último",
|
||||
"mark it read": "marcarlo como leído",
|
||||
"move to {folder}": "mover a {folder}",
|
||||
"reject it": "rechazarlo",
|
||||
"remove {flag}": "quitar {flag}",
|
||||
"second": "segundo",
|
||||
"size is over {n} KB": "el tamaño supera {n} KB",
|
||||
"size is under {n} KB": "el tamaño es inferior a {n} KB",
|
||||
"star it": "destacarlo",
|
||||
"stop": "detener",
|
||||
"third": "tercer",
|
||||
"{header} address {op} \"{value}\"": "la dirección de {header} {op} \"{value}\"",
|
||||
"{header} {op} \"{value}\"": "{header} {op} \"{value}\"",
|
||||
"{rule}, until {date}": "{rule}, hasta el {date}",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { one: "Eliminar {n} elemento", other: "Eliminar {n} elementos" },
|
||||
"Delete {n} items?": { one: "¿Eliminar {n} elemento?", other: "¿Eliminar {n} elementos?" },
|
||||
"Move {n} items": { one: "Mover {n} elemento", other: "Mover {n} elementos" },
|
||||
"Move {n} items…": { one: "Mover {n} elemento…", other: "Mover {n} elementos…" },
|
||||
"The event runs {n} days longer than this shows.": { one: "El evento dura {n} día más de lo que se muestra aquí.", other: "El evento dura {n} días más de lo que se muestra aquí." },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { one: "{n} invitado no está en este servidor, así que no hay información de libre/ocupado para él.", other: "{n} invitados no están en este servidor, así que no hay información de libre/ocupado para ellos." },
|
||||
"{n} items selected": { one: "{n} elemento seleccionado", other: "{n} elementos seleccionados" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { one: "Cada día", other: "Cada {n} días" },
|
||||
"Every {n} months": { one: "Cada mes", other: "Cada {n} meses" },
|
||||
"Every {n} months on day {days}": { one: "Cada mes el día {days}", other: "Cada {n} meses el día {days}" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { one: "Cada mes el {ordinal} {weekday}", other: "Cada {n} meses el {ordinal} {weekday}" },
|
||||
"Every {n} months on {weekday}": { one: "Cada mes el {weekday}", other: "Cada {n} meses el {weekday}" },
|
||||
"Every {n} weeks": { one: "Cada semana", other: "Cada {n} semanas" },
|
||||
"Every {n} weeks on {days}": { one: "Cada semana los {days}", other: "Cada {n} semanas los {days}" },
|
||||
"Every {n} years": { one: "Cada año", other: "Cada {n} años" },
|
||||
"{rule}, {n} times": { one: "{rule}, {n} vez", other: "{rule}, {n} veces" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { one: "¿Mover {n} mensaje a la Papelera?", other: "¿Mover {n} mensajes a la Papelera?" },
|
||||
"{n} days": { one: "{n} día", other: "{n} días" },
|
||||
|
||||
@@ -1260,8 +1260,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "Ce message a été envoyé automatiquement, aucun accusé de lecture n'est donc proposé.",
|
||||
"This server will not hold a message longer than {span}.": "Ce serveur ne retient pas un message plus de {span}.",
|
||||
"Upload failed": "Échec de l'envoi",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "Tous les {n} {frequency}",
|
||||
"Monthly": "Chaque mois",
|
||||
"Monthly on day {days}": "Chaque mois le {days}",
|
||||
"Monthly on the {ordinal} {weekday}": "Chaque mois le {ordinal} {weekday}",
|
||||
"Monthly on {weekday}": "Chaque mois le {weekday}",
|
||||
"Weekly": "Chaque semaine",
|
||||
"Weekly on {days}": "Chaque semaine le {days}",
|
||||
"add {flag}": "ajouter {flag}",
|
||||
"always": "toujours",
|
||||
"body contains \"{value}\"": "le corps contient \"{value}\"",
|
||||
"body does not contain \"{value}\"": "le corps ne contient pas \"{value}\"",
|
||||
"delete it": "le supprimer",
|
||||
"fifth": "cinquième",
|
||||
"first": "premier",
|
||||
"forward to {address}": "transférer à {address}",
|
||||
"fourth": "quatrième",
|
||||
"keep it": "le conserver",
|
||||
"last": "dernier",
|
||||
"mark it read": "le marquer comme lu",
|
||||
"move to {folder}": "déplacer vers {folder}",
|
||||
"reject it": "le rejeter",
|
||||
"remove {flag}": "retirer {flag}",
|
||||
"second": "deuxième",
|
||||
"size is over {n} KB": "la taille dépasse {n} Ko",
|
||||
"size is under {n} KB": "la taille est inférieure à {n} Ko",
|
||||
"star it": "le suivre",
|
||||
"stop": "arrêter",
|
||||
"third": "troisième",
|
||||
"{header} address {op} \"{value}\"": "l'adresse {header} {op} \"{value}\"",
|
||||
"{header} {op} \"{value}\"": "{header} {op} \"{value}\"",
|
||||
"{rule}, until {date}": "{rule}, jusqu'au {date}",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { one: "Supprimer {n} élément", other: "Supprimer {n} éléments" },
|
||||
"Delete {n} items?": { one: "Supprimer {n} élément ?", other: "Supprimer {n} éléments ?" },
|
||||
"Move {n} items": { one: "Déplacer {n} élément", other: "Déplacer {n} éléments" },
|
||||
"Move {n} items…": { one: "Déplacer {n} élément…", other: "Déplacer {n} éléments…" },
|
||||
"The event runs {n} days longer than this shows.": { one: "L'événement dure {n} jour de plus que ce qui est affiché ici.", other: "L'événement dure {n} jours de plus que ce qui est affiché ici." },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { one: "{n} invité n'est pas sur ce serveur, il n'y a donc pas de disponibilité à lire pour lui.", other: "{n} invités ne sont pas sur ce serveur, il n'y a donc pas de disponibilité à lire pour eux." },
|
||||
"{n} items selected": { one: "{n} élément sélectionné", other: "{n} éléments sélectionnés" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { one: "Chaque jour", other: "Tous les {n} jours" },
|
||||
"Every {n} months": { one: "Chaque mois", other: "Tous les {n} mois" },
|
||||
"Every {n} months on day {days}": { one: "Chaque mois le {days}", other: "Tous les {n} mois le {days}" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { one: "Chaque mois le {ordinal} {weekday}", other: "Tous les {n} mois le {ordinal} {weekday}" },
|
||||
"Every {n} months on {weekday}": { one: "Chaque mois le {weekday}", other: "Tous les {n} mois le {weekday}" },
|
||||
"Every {n} weeks": { one: "Chaque semaine", other: "Toutes les {n} semaines" },
|
||||
"Every {n} weeks on {days}": { one: "Chaque semaine le {days}", other: "Toutes les {n} semaines le {days}" },
|
||||
"Every {n} years": { one: "Chaque année", other: "Tous les {n} ans" },
|
||||
"{rule}, {n} times": { one: "{rule}, {n} fois", other: "{rule}, {n} fois" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { one: "Déplacer {n} message vers la Corbeille ?", other: "Déplacer {n} messages vers la Corbeille ?" },
|
||||
"{n} days": { one: "{n} jour", other: "{n} jours" },
|
||||
|
||||
@@ -1263,8 +1263,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "このメールは自動送信されたため、開封確認は行いません。",
|
||||
"This server will not hold a message longer than {span}.": "このサーバーはメールを {span} を超えて保留しません。",
|
||||
"Upload failed": "アップロードに失敗しました",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "{n} {frequency} ごと",
|
||||
"Monthly": "毎月",
|
||||
"Monthly on day {days}": "毎月 {days} 日",
|
||||
"Monthly on the {ordinal} {weekday}": "毎月第{ordinal} {weekday}",
|
||||
"Monthly on {weekday}": "毎月 {weekday}",
|
||||
"Weekly": "毎週",
|
||||
"Weekly on {days}": "毎週 {days}",
|
||||
"add {flag}": "{flag} を付ける",
|
||||
"always": "常に",
|
||||
"body contains \"{value}\"": "本文が \"{value}\" を含む",
|
||||
"body does not contain \"{value}\"": "本文が \"{value}\" を含まない",
|
||||
"delete it": "削除",
|
||||
"fifth": "5",
|
||||
"first": "1",
|
||||
"forward to {address}": "{address} に転送",
|
||||
"fourth": "4",
|
||||
"keep it": "保持",
|
||||
"last": "最終",
|
||||
"mark it read": "既読にする",
|
||||
"move to {folder}": "{folder} に移動",
|
||||
"reject it": "拒否",
|
||||
"remove {flag}": "{flag} を外す",
|
||||
"second": "2",
|
||||
"size is over {n} KB": "サイズが {n} KB を超える",
|
||||
"size is under {n} KB": "サイズが {n} KB 未満",
|
||||
"star it": "スターを付ける",
|
||||
"stop": "停止",
|
||||
"third": "3",
|
||||
"{header} address {op} \"{value}\"": "{header} のアドレスが \"{value}\" を{op}",
|
||||
"{header} {op} \"{value}\"": "{header} が \"{value}\" を{op}",
|
||||
"{rule}, until {date}": "{rule}({date} まで)",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { other: "{n} 件を削除" },
|
||||
"Delete {n} items?": { other: "{n} 件を削除しますか?" },
|
||||
"Move {n} items": { other: "{n} 件を移動" },
|
||||
"Move {n} items…": { other: "{n} 件を移動…" },
|
||||
"The event runs {n} days longer than this shows.": { other: "この予定は表示よりも {n} 日長く続きます。" },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { other: "{n} 名の参加者はこのサーバーにいないため、空き情報を取得できません。" },
|
||||
"{n} items selected": { other: "{n} 件を選択中" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { other: "{n} 日ごと" },
|
||||
"Every {n} months": { other: "{n} か月ごと" },
|
||||
"Every {n} months on day {days}": { other: "{n} か月ごと {days} 日" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { other: "{n} か月ごと第{ordinal} {weekday}" },
|
||||
"Every {n} months on {weekday}": { other: "{n} か月ごと {weekday}" },
|
||||
"Every {n} weeks": { other: "{n} 週ごと" },
|
||||
"Every {n} weeks on {days}": { other: "{n} 週ごと {days}" },
|
||||
"Every {n} years": { other: "{n} 年ごと" },
|
||||
"{rule}, {n} times": { other: "{rule}({n} 回)" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { other: "{n} 通のメールをゴミ箱に移動しますか?" },
|
||||
"{n} days": { other: "{n} 日" },
|
||||
|
||||
@@ -1251,8 +1251,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "Dit bericht is automatisch verzonden, dus er wordt geen leesbevestiging aangeboden.",
|
||||
"This server will not hold a message longer than {span}.": "Deze server houdt een bericht niet langer dan {span} vast.",
|
||||
"Upload failed": "Uploaden mislukt",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "Elke {n} {frequency}",
|
||||
"Monthly": "Maandelijks",
|
||||
"Monthly on day {days}": "Maandelijks op dag {days}",
|
||||
"Monthly on the {ordinal} {weekday}": "Maandelijks op de {ordinal} {weekday}",
|
||||
"Monthly on {weekday}": "Maandelijks op {weekday}",
|
||||
"Weekly": "Wekelijks",
|
||||
"Weekly on {days}": "Wekelijks op {days}",
|
||||
"add {flag}": "{flag} toevoegen",
|
||||
"always": "altijd",
|
||||
"body contains \"{value}\"": "tekst bevat \"{value}\"",
|
||||
"body does not contain \"{value}\"": "tekst bevat niet \"{value}\"",
|
||||
"delete it": "verwijderen",
|
||||
"fifth": "vijfde",
|
||||
"first": "eerste",
|
||||
"forward to {address}": "doorsturen naar {address}",
|
||||
"fourth": "vierde",
|
||||
"keep it": "behouden",
|
||||
"last": "laatste",
|
||||
"mark it read": "als gelezen markeren",
|
||||
"move to {folder}": "verplaatsen naar {folder}",
|
||||
"reject it": "weigeren",
|
||||
"remove {flag}": "{flag} verwijderen",
|
||||
"second": "tweede",
|
||||
"size is over {n} KB": "grootte boven {n} KB",
|
||||
"size is under {n} KB": "grootte onder {n} KB",
|
||||
"star it": "een ster geven",
|
||||
"stop": "stoppen",
|
||||
"third": "derde",
|
||||
"{header} address {op} \"{value}\"": "{header}-adres {op} \"{value}\"",
|
||||
"{header} {op} \"{value}\"": "{header} {op} \"{value}\"",
|
||||
"{rule}, until {date}": "{rule}, tot {date}",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { one: "{n} item verwijderen", other: "{n} items verwijderen" },
|
||||
"Delete {n} items?": { one: "{n} item verwijderen?", other: "{n} items verwijderen?" },
|
||||
"Move {n} items": { one: "{n} item verplaatsen", other: "{n} items verplaatsen" },
|
||||
"Move {n} items…": { one: "{n} item verplaatsen…", other: "{n} items verplaatsen…" },
|
||||
"The event runs {n} days longer than this shows.": { one: "De gebeurtenis duurt {n} dag langer dan hier wordt getoond.", other: "De gebeurtenis duurt {n} dagen langer dan hier wordt getoond." },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { one: "{n} gast zit niet op deze server, dus er is geen vrij/bezet voor die persoon te lezen.", other: "{n} gasten zitten niet op deze server, dus er is geen vrij/bezet voor hen te lezen." },
|
||||
"{n} items selected": { one: "{n} item geselecteerd", other: "{n} items geselecteerd" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { one: "Elke dag", other: "Elke {n} dagen" },
|
||||
"Every {n} months": { one: "Elke maand", other: "Elke {n} maanden" },
|
||||
"Every {n} months on day {days}": { one: "Elke maand op dag {days}", other: "Elke {n} maanden op dag {days}" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { one: "Elke maand op de {ordinal} {weekday}", other: "Elke {n} maanden op de {ordinal} {weekday}" },
|
||||
"Every {n} months on {weekday}": { one: "Elke maand op {weekday}", other: "Elke {n} maanden op {weekday}" },
|
||||
"Every {n} weeks": { one: "Elke week", other: "Elke {n} weken" },
|
||||
"Every {n} weeks on {days}": { one: "Elke week op {days}", other: "Elke {n} weken op {days}" },
|
||||
"Every {n} years": { one: "Elk jaar", other: "Elke {n} jaar" },
|
||||
"{rule}, {n} times": { one: "{rule}, {n} keer", other: "{rule}, {n} keer" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { one: "{n} bericht naar de Prullenbak verplaatsen?", other: "{n} berichten naar de Prullenbak verplaatsen?" },
|
||||
"{n} days": { one: "{n} dag", other: "{n} dagen" },
|
||||
|
||||
@@ -1258,8 +1258,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "Esta mensagem foi enviada automaticamente, então não há confirmação de leitura a oferecer.",
|
||||
"This server will not hold a message longer than {span}.": "Este servidor não retém uma mensagem por mais de {span}.",
|
||||
"Upload failed": "Falha no envio",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "A cada {n} {frequency}",
|
||||
"Monthly": "Mensalmente",
|
||||
"Monthly on day {days}": "Mensalmente no dia {days}",
|
||||
"Monthly on the {ordinal} {weekday}": "Mensalmente na {ordinal} {weekday}",
|
||||
"Monthly on {weekday}": "Mensalmente em {weekday}",
|
||||
"Weekly": "Semanalmente",
|
||||
"Weekly on {days}": "Semanalmente em {days}",
|
||||
"add {flag}": "adicionar {flag}",
|
||||
"always": "sempre",
|
||||
"body contains \"{value}\"": "o corpo contém \"{value}\"",
|
||||
"body does not contain \"{value}\"": "o corpo não contém \"{value}\"",
|
||||
"delete it": "excluir",
|
||||
"fifth": "quinta",
|
||||
"first": "primeira",
|
||||
"forward to {address}": "encaminhar para {address}",
|
||||
"fourth": "quarta",
|
||||
"keep it": "manter",
|
||||
"last": "última",
|
||||
"mark it read": "marcar como lida",
|
||||
"move to {folder}": "mover para {folder}",
|
||||
"reject it": "rejeitar",
|
||||
"remove {flag}": "remover {flag}",
|
||||
"second": "segunda",
|
||||
"size is over {n} KB": "o tamanho passa de {n} KB",
|
||||
"size is under {n} KB": "o tamanho é menor que {n} KB",
|
||||
"star it": "favoritar",
|
||||
"stop": "parar",
|
||||
"third": "terceira",
|
||||
"{header} address {op} \"{value}\"": "o endereço de {header} {op} \"{value}\"",
|
||||
"{header} {op} \"{value}\"": "{header} {op} \"{value}\"",
|
||||
"{rule}, until {date}": "{rule}, até {date}",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { one: "Excluir {n} item", other: "Excluir {n} itens" },
|
||||
"Delete {n} items?": { one: "Excluir {n} item?", other: "Excluir {n} itens?" },
|
||||
"Move {n} items": { one: "Mover {n} item", other: "Mover {n} itens" },
|
||||
"Move {n} items…": { one: "Mover {n} item…", other: "Mover {n} itens…" },
|
||||
"The event runs {n} days longer than this shows.": { one: "O evento dura {n} dia a mais do que é exibido aqui.", other: "O evento dura {n} dias a mais do que é exibido aqui." },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { one: "{n} convidado não está neste servidor, então não há livre/ocupado a consultar para ele.", other: "{n} convidados não estão neste servidor, então não há livre/ocupado a consultar para eles." },
|
||||
"{n} items selected": { one: "{n} item selecionado", other: "{n} itens selecionados" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { one: "Todo dia", other: "A cada {n} dias" },
|
||||
"Every {n} months": { one: "Todo mês", other: "A cada {n} meses" },
|
||||
"Every {n} months on day {days}": { one: "Todo mês no dia {days}", other: "A cada {n} meses no dia {days}" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { one: "Todo mês na {ordinal} {weekday}", other: "A cada {n} meses na {ordinal} {weekday}" },
|
||||
"Every {n} months on {weekday}": { one: "Todo mês em {weekday}", other: "A cada {n} meses em {weekday}" },
|
||||
"Every {n} weeks": { one: "Toda semana", other: "A cada {n} semanas" },
|
||||
"Every {n} weeks on {days}": { one: "Toda semana em {days}", other: "A cada {n} semanas em {days}" },
|
||||
"Every {n} years": { one: "Todo ano", other: "A cada {n} anos" },
|
||||
"{rule}, {n} times": { one: "{rule}, {n} vez", other: "{rule}, {n} vezes" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { one: "Mover {n} mensagem para a Lixeira?", other: "Mover {n} mensagens para a Lixeira?" },
|
||||
"{n} days": { one: "{n} dia", other: "{n} dias" },
|
||||
|
||||
@@ -1257,8 +1257,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "Это письмо отправлено автоматически, поэтому уведомление о прочтении не предлагается.",
|
||||
"This server will not hold a message longer than {span}.": "Этот сервер не удерживает письмо дольше чем {span}.",
|
||||
"Upload failed": "Не удалось загрузить",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "Каждые {n} {frequency}",
|
||||
"Monthly": "Ежемесячно",
|
||||
"Monthly on day {days}": "Ежемесячно {days}-го числа",
|
||||
"Monthly on the {ordinal} {weekday}": "Ежемесячно в {ordinal} {weekday}",
|
||||
"Monthly on {weekday}": "Ежемесячно в {weekday}",
|
||||
"Weekly": "Еженедельно",
|
||||
"Weekly on {days}": "Еженедельно в {days}",
|
||||
"add {flag}": "добавить {flag}",
|
||||
"always": "всегда",
|
||||
"body contains \"{value}\"": "текст содержит \"{value}\"",
|
||||
"body does not contain \"{value}\"": "текст не содержит \"{value}\"",
|
||||
"delete it": "удалить",
|
||||
"fifth": "пятый",
|
||||
"first": "первый",
|
||||
"forward to {address}": "переслать на {address}",
|
||||
"fourth": "четвёртый",
|
||||
"keep it": "оставить",
|
||||
"last": "последний",
|
||||
"mark it read": "пометить прочитанным",
|
||||
"move to {folder}": "переместить в {folder}",
|
||||
"reject it": "отклонить",
|
||||
"remove {flag}": "убрать {flag}",
|
||||
"second": "второй",
|
||||
"size is over {n} KB": "размер больше {n} КБ",
|
||||
"size is under {n} KB": "размер меньше {n} КБ",
|
||||
"star it": "отметить",
|
||||
"stop": "остановить",
|
||||
"third": "третий",
|
||||
"{header} address {op} \"{value}\"": "адрес {header} {op} \"{value}\"",
|
||||
"{header} {op} \"{value}\"": "{header} {op} \"{value}\"",
|
||||
"{rule}, until {date}": "{rule}, до {date}",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { one: "Удалить {n} объект", few: "Удалить {n} объекта", many: "Удалить {n} объектов", other: "Удалить {n} объекта" },
|
||||
"Delete {n} items?": { one: "Удалить {n} объект?", few: "Удалить {n} объекта?", many: "Удалить {n} объектов?", other: "Удалить {n} объекта?" },
|
||||
"Move {n} items": { one: "Переместить {n} объект", few: "Переместить {n} объекта", many: "Переместить {n} объектов", other: "Переместить {n} объекта" },
|
||||
"Move {n} items…": { one: "Переместить {n} объект…", few: "Переместить {n} объекта…", many: "Переместить {n} объектов…", other: "Переместить {n} объекта…" },
|
||||
"The event runs {n} days longer than this shows.": { one: "Событие длится на {n} день дольше, чем показано здесь.", few: "Событие длится на {n} дня дольше, чем показано здесь.", many: "Событие длится на {n} дней дольше, чем показано здесь.", other: "Событие длится на {n} дня дольше, чем показано здесь." },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { one: "{n} участник не на этом сервере, поэтому сведений о занятости для него нет.", few: "{n} участника не на этом сервере, поэтому сведений о занятости для них нет.", many: "{n} участников не на этом сервере, поэтому сведений о занятости для них нет.", other: "{n} участника не на этом сервере, поэтому сведений о занятости для них нет." },
|
||||
"{n} items selected": { one: "Выбран {n} объект", few: "Выбрано {n} объекта", many: "Выбрано {n} объектов", other: "Выбрано {n} объекта" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { one: "Каждый день", few: "Каждые {n} дня", many: "Каждые {n} дней", other: "Каждые {n} дня" },
|
||||
"Every {n} months": { one: "Каждый месяц", few: "Каждые {n} месяца", many: "Каждые {n} месяцев", other: "Каждые {n} месяца" },
|
||||
"Every {n} months on day {days}": { one: "Каждый месяц {days}-го числа", few: "Каждые {n} месяца {days}-го числа", many: "Каждые {n} месяцев {days}-го числа", other: "Каждые {n} месяца {days}-го числа" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { one: "Каждый месяц в {ordinal} {weekday}", few: "Каждые {n} месяца в {ordinal} {weekday}", many: "Каждые {n} месяцев в {ordinal} {weekday}", other: "Каждые {n} месяца в {ordinal} {weekday}" },
|
||||
"Every {n} months on {weekday}": { one: "Каждый месяц в {weekday}", few: "Каждые {n} месяца в {weekday}", many: "Каждые {n} месяцев в {weekday}", other: "Каждые {n} месяца в {weekday}" },
|
||||
"Every {n} weeks": { one: "Каждую неделю", few: "Каждые {n} недели", many: "Каждые {n} недель", other: "Каждые {n} недели" },
|
||||
"Every {n} weeks on {days}": { one: "Каждую неделю в {days}", few: "Каждые {n} недели в {days}", many: "Каждые {n} недель в {days}", other: "Каждые {n} недели в {days}" },
|
||||
"Every {n} years": { one: "Каждый год", few: "Каждые {n} года", many: "Каждые {n} лет", other: "Каждые {n} года" },
|
||||
"{rule}, {n} times": { one: "{rule}, {n} раз", few: "{rule}, {n} раза", many: "{rule}, {n} раз", other: "{rule}, {n} раза" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { one: "Переместить {n} письмо в корзину?", few: "Переместить {n} письма в корзину?", many: "Переместить {n} писем в корзину?", other: "Переместить {n} письма в корзину?" },
|
||||
"{n} days": { one: "{n} день", few: "{n} дня", many: "{n} дней", other: "{n} дня" },
|
||||
|
||||
@@ -1251,8 +1251,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "Цей лист надіслано автоматично, тому сповіщення про прочитання не пропонується.",
|
||||
"This server will not hold a message longer than {span}.": "Цей сервер не утримує лист довше ніж {span}.",
|
||||
"Upload failed": "Не вдалося завантажити",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "Кожні {n} {frequency}",
|
||||
"Monthly": "Щомісяця",
|
||||
"Monthly on day {days}": "Щомісяця {days}-го числа",
|
||||
"Monthly on the {ordinal} {weekday}": "Щомісяця у {ordinal} {weekday}",
|
||||
"Monthly on {weekday}": "Щомісяця у {weekday}",
|
||||
"Weekly": "Щотижня",
|
||||
"Weekly on {days}": "Щотижня у {days}",
|
||||
"add {flag}": "додати {flag}",
|
||||
"always": "завжди",
|
||||
"body contains \"{value}\"": "текст містить \"{value}\"",
|
||||
"body does not contain \"{value}\"": "текст не містить \"{value}\"",
|
||||
"delete it": "видалити",
|
||||
"fifth": "п’ятий",
|
||||
"first": "перший",
|
||||
"forward to {address}": "переслати на {address}",
|
||||
"fourth": "четвертий",
|
||||
"keep it": "залишити",
|
||||
"last": "останній",
|
||||
"mark it read": "позначити прочитаним",
|
||||
"move to {folder}": "перемістити до {folder}",
|
||||
"reject it": "відхилити",
|
||||
"remove {flag}": "прибрати {flag}",
|
||||
"second": "другий",
|
||||
"size is over {n} KB": "розмір більший за {n} КБ",
|
||||
"size is under {n} KB": "розмір менший за {n} КБ",
|
||||
"star it": "позначити",
|
||||
"stop": "зупинити",
|
||||
"third": "третій",
|
||||
"{header} address {op} \"{value}\"": "адреса {header} {op} \"{value}\"",
|
||||
"{header} {op} \"{value}\"": "{header} {op} \"{value}\"",
|
||||
"{rule}, until {date}": "{rule}, до {date}",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { one: "Видалити {n} об’єкт", few: "Видалити {n} об’єкти", many: "Видалити {n} об’єктів", other: "Видалити {n} об’єкта" },
|
||||
"Delete {n} items?": { one: "Видалити {n} об’єкт?", few: "Видалити {n} об’єкти?", many: "Видалити {n} об’єктів?", other: "Видалити {n} об’єкта?" },
|
||||
"Move {n} items": { one: "Перемістити {n} об’єкт", few: "Перемістити {n} об’єкти", many: "Перемістити {n} об’єктів", other: "Перемістити {n} об’єкта" },
|
||||
"Move {n} items…": { one: "Перемістити {n} об’єкт…", few: "Перемістити {n} об’єкти…", many: "Перемістити {n} об’єктів…", other: "Перемістити {n} об’єкта…" },
|
||||
"The event runs {n} days longer than this shows.": { one: "Подія триває на {n} день довше, ніж показано тут.", few: "Подія триває на {n} дні довше, ніж показано тут.", many: "Подія триває на {n} днів довше, ніж показано тут.", other: "Подія триває на {n} дня довше, ніж показано тут." },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { one: "{n} гість не на цьому сервері, тому відомостей про зайнятість для нього немає.", few: "{n} гості не на цьому сервері, тому відомостей про зайнятість для них немає.", many: "{n} гостей не на цьому сервері, тому відомостей про зайнятість для них немає.", other: "{n} гостя не на цьому сервері, тому відомостей про зайнятість для них немає." },
|
||||
"{n} items selected": { one: "Вибрано {n} об’єкт", few: "Вибрано {n} об’єкти", many: "Вибрано {n} об’єктів", other: "Вибрано {n} об’єкта" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { one: "Щодня", few: "Кожні {n} дні", many: "Кожні {n} днів", other: "Кожні {n} дня" },
|
||||
"Every {n} months": { one: "Щомісяця", few: "Кожні {n} місяці", many: "Кожні {n} місяців", other: "Кожні {n} місяця" },
|
||||
"Every {n} months on day {days}": { one: "Щомісяця {days}-го числа", few: "Кожні {n} місяці {days}-го числа", many: "Кожні {n} місяців {days}-го числа", other: "Кожні {n} місяця {days}-го числа" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { one: "Щомісяця у {ordinal} {weekday}", few: "Кожні {n} місяці у {ordinal} {weekday}", many: "Кожні {n} місяців у {ordinal} {weekday}", other: "Кожні {n} місяця у {ordinal} {weekday}" },
|
||||
"Every {n} months on {weekday}": { one: "Щомісяця у {weekday}", few: "Кожні {n} місяці у {weekday}", many: "Кожні {n} місяців у {weekday}", other: "Кожні {n} місяця у {weekday}" },
|
||||
"Every {n} weeks": { one: "Щотижня", few: "Кожні {n} тижні", many: "Кожні {n} тижнів", other: "Кожні {n} тижня" },
|
||||
"Every {n} weeks on {days}": { one: "Щотижня у {days}", few: "Кожні {n} тижні у {days}", many: "Кожні {n} тижнів у {days}", other: "Кожні {n} тижня у {days}" },
|
||||
"Every {n} years": { one: "Щороку", few: "Кожні {n} роки", many: "Кожні {n} років", other: "Кожні {n} року" },
|
||||
"{rule}, {n} times": { one: "{rule}, {n} раз", few: "{rule}, {n} рази", many: "{rule}, {n} разів", other: "{rule}, {n} раза" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { one: "Перемістити {n} лист до кошика?", few: "Перемістити {n} листи до кошика?", many: "Перемістити {n} листів до кошика?", other: "Перемістити {n} листа до кошика?" },
|
||||
"{n} days": { one: "{n} день", few: "{n} дні", many: "{n} днів", other: "{n} дня" },
|
||||
|
||||
@@ -1262,8 +1262,66 @@ export const catalog: Catalog = {
|
||||
"This message was sent automatically, so no read receipt is offered.": "此邮件为自动发送,因此不提供已读回执。",
|
||||
"This server will not hold a message longer than {span}.": "此服务器保留邮件的时间不会超过 {span}。",
|
||||
"Upload failed": "上传失败",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
"Every {n} {frequency}": "每 {n} {frequency}",
|
||||
"Monthly": "每月",
|
||||
"Monthly on day {days}": "每月 {days} 日",
|
||||
"Monthly on the {ordinal} {weekday}": "每月第{ordinal}个{weekday}",
|
||||
"Monthly on {weekday}": "每月 {weekday}",
|
||||
"Weekly": "每周",
|
||||
"Weekly on {days}": "每周 {days}",
|
||||
"add {flag}": "添加 {flag}",
|
||||
"always": "始终",
|
||||
"body contains \"{value}\"": "正文包含 \"{value}\"",
|
||||
"body does not contain \"{value}\"": "正文不包含 \"{value}\"",
|
||||
"delete it": "删除",
|
||||
"fifth": "五",
|
||||
"first": "一",
|
||||
"forward to {address}": "转发到 {address}",
|
||||
"fourth": "四",
|
||||
"keep it": "保留",
|
||||
"last": "最后一",
|
||||
"mark it read": "标记为已读",
|
||||
"move to {folder}": "移动到 {folder}",
|
||||
"reject it": "拒收",
|
||||
"remove {flag}": "移除 {flag}",
|
||||
"second": "二",
|
||||
"size is over {n} KB": "大小超过 {n} KB",
|
||||
"size is under {n} KB": "大小小于 {n} KB",
|
||||
"star it": "加星标",
|
||||
"stop": "停止",
|
||||
"third": "三",
|
||||
"{header} address {op} \"{value}\"": "{header} 地址 {op} \"{value}\"",
|
||||
"{header} {op} \"{value}\"": "{header} {op} \"{value}\"",
|
||||
"{rule}, until {date}": "{rule},直到 {date}",
|
||||
"{tests} → {actions}": "{tests} → {actions}",
|
||||
// ── Third pass ──────────────────────────────────────────────────────
|
||||
// Sentences that lib/ and store/ were building in English, and the two
|
||||
// swipe labels that reach t() through a variable and so were invisible
|
||||
// to a scan for t("literal"). See #259.
|
||||
},
|
||||
plurals: {
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Delete {n} items": { other: "删除 {n} 个项目" },
|
||||
"Delete {n} items?": { other: "要删除 {n} 个项目吗?" },
|
||||
"Move {n} items": { other: "移动 {n} 个项目" },
|
||||
"Move {n} items…": { other: "移动 {n} 个项目…" },
|
||||
"The event runs {n} days longer than this shows.": { other: "此活动比这里显示的时间长 {n} 天。" },
|
||||
"{n} guests are not on this server, so there is no free/busy to read for them.": { other: "有 {n} 位与会者不在此服务器上,因此无法读取他们的空闲/忙碌信息。" },
|
||||
"{n} items selected": { other: "已选择 {n} 个项目" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Every {n} days": { other: "每 {n} 天" },
|
||||
"Every {n} months": { other: "每 {n} 个月" },
|
||||
"Every {n} months on day {days}": { other: "每 {n} 个月的 {days} 日" },
|
||||
"Every {n} months on the {ordinal} {weekday}": { other: "每 {n} 个月的第{ordinal}个{weekday}" },
|
||||
"Every {n} months on {weekday}": { other: "每 {n} 个月的 {weekday}" },
|
||||
"Every {n} weeks": { other: "每 {n} 周" },
|
||||
"Every {n} weeks on {days}": { other: "每 {n} 周的 {days}" },
|
||||
"Every {n} years": { other: "每 {n} 年" },
|
||||
"{rule}, {n} times": { other: "{rule},共 {n} 次" },
|
||||
// ── Third pass ─────────────────────────────────────────────────────
|
||||
"Move {n} messages to Trash?": { other: "要将 {n} 封邮件移到已删除邮件吗?" },
|
||||
"{n} days": { other: "{n} 天" },
|
||||
|
||||
@@ -13,7 +13,7 @@ import { RecipientInput } from "../compose/RecipientInput";
|
||||
import { DateField, DateTimeField } from "@/ui/datefield";
|
||||
import { browserTimeZone, dateToZonedLocal, formatDuration, fromInputDateTime, listTimeZones, parseDuration, toInputDateTime, toLocalDateOnly, zonedToDate, DAY_MS, humanDuration } from "@/lib/dates";
|
||||
import { formatClock, formatNumericDate, formatWeekday, formatWeekdayDate } from "@/lib/datetime";
|
||||
import { WEEKDAYS, describeRule, presetFor, ruleFromPreset, type RecurrencePreset } from "@/lib/recurrence";
|
||||
import { WEEKDAY_KEYS, weekdayOptions, describeRule, presetFor, ruleFromPreset, type RecurrencePreset } from "@/lib/recurrence";
|
||||
import { newKey } from "@/lib/contacts";
|
||||
import { availabilityWindow } from "@/lib/availabilityWindow";
|
||||
import { askEditScope, droppedMessage, runScoped } from "./scope";
|
||||
@@ -359,7 +359,7 @@ function EventForm({ init, base, scope, editing, onClose, settingsTz, defaultAle
|
||||
</select>
|
||||
)}
|
||||
{!oneDate && (
|
||||
<select className="select" style={{ width: "auto", height: 32 }} value={preset} onChange={(e) => { const p = e.target.value as RecurrencePreset; setPreset(p); if (p === "custom") setRule(rule ?? { "@type": "RecurrenceRule", frequency: "weekly", byDay: [{ "@type": "NDay", day: WEEKDAYS[(start.getDay() + 6) % 7]!.key }] }); else setRule(ruleFromPreset(p, start)); }}>
|
||||
<select className="select" style={{ width: "auto", height: 32 }} value={preset} onChange={(e) => { const p = e.target.value as RecurrencePreset; setPreset(p); if (p === "custom") setRule(rule ?? { "@type": "RecurrenceRule", frequency: "weekly", byDay: [{ "@type": "NDay", day: WEEKDAY_KEYS[(start.getDay() + 6) % 7]! }] }); else setRule(ruleFromPreset(p, start)); }}>
|
||||
<option value="none">{translate("Does not repeat")}</option>
|
||||
<option value="daily">{translate("Daily")}</option>
|
||||
<option value="weekly">{translate("Weekly on {weekday}", { weekday: formatWeekday(start, "long") })}</option>
|
||||
@@ -381,7 +381,7 @@ function EventForm({ init, base, scope, editing, onClose, settingsTz, defaultAle
|
||||
</div>
|
||||
{customRule.frequency === "weekly" && (
|
||||
<div className="row" style={{ gap: 4, marginTop: 8 }}>
|
||||
{WEEKDAYS.map((w) => {
|
||||
{weekdayOptions().map((w) => {
|
||||
const on = customRule.byDay?.some((d) => d.day === w.key);
|
||||
return <button key={w.key} type="button" className={`btn btn-sm btn-pill ${on ? "btn-primary" : ""}`} style={{ width: 36, padding: 0 }} title={w.label} onClick={() => { const cur = customRule.byDay ?? []; const next: JSCalendarNDay[] = on ? cur.filter((d) => d.day !== w.key) : [...cur, { "@type": "NDay", day: w.key }]; setRule({ ...customRule, byDay: next.length ? next : undefined }); }}>{w.short}</button>;
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user