feat(design): the Copy rule — names, verbs, dates and units (E28 S3g)
The owner's picks (all the recommended options) become DESIGN.md's Copy chapter and a sweep of both languages (742 values, no key drift): - One name per feature: the rail's label, which may shorten it (Alerts -> Notifications, Fiókok -> Felhasználók, Download Center -> Downloads, Channel manager -> Channels); one "sync from YouTube" verb. - Four verbs, four meanings (Delete / Remove / Dismiss / Discard, and their Hungarian pairs), checked against what each action does. - Sentence case, the app's places capitalised; "…" on a command that asks first (a popover or form title got its own key without it); one-sentence messages without a stop or a tick, "Couldn't …"; curly quotes. - Dates only from lib/format.ts, in the UI language: formatDateTime names the month and stops at the minute (it printed "9/27/2026, 8:30:14 AM"), formatDateTimeSec for the log, the audit and the recorder; ui/RelativeTime pairs "3d ago" with the exact minute on hover. local/date-format refuses toLocale*String and Intl date formatters elsewhere. - Units from Intl, short (formatUnit): runtimes, totals, the Scheduler countdown, the stats range, the player's tap skip. "2h 13m" was English in both languages. - src/i18n/copyRule.test.ts holds the machine-readable part (quotes, "...", ticks, bare unit letters).
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
RELATIVE_UNITS,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
formatDateTimeSec,
|
||||
formatNumber,
|
||||
formatRuntime,
|
||||
formatTotalHours,
|
||||
@@ -161,7 +162,7 @@ describe("formatNumber", () => {
|
||||
|
||||
it("carries the UI language into the channel total-hours label", () => {
|
||||
i18nStub.language = "hu";
|
||||
expect(formatTotalHours(12345 * 3600)).toBe(`${new Intl.NumberFormat("hu").format(12345)} h`);
|
||||
expect(formatTotalHours(12345 * 3600)).toBe(`${new Intl.NumberFormat("hu").format(12345)} ó`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,26 +171,32 @@ describe("formatRuntime", () => {
|
||||
[null, ""],
|
||||
[undefined, ""],
|
||||
[0, ""], // nothing to show — the call sites suppress their separator on the empty string
|
||||
[1, "1s"],
|
||||
[45, "45s"], // one Short: the regression case
|
||||
[45.7, "45s"], // truncates, so the seconds tier cannot overstate either
|
||||
[59, "59s"], // last second before the minute tier
|
||||
[60, "1m"],
|
||||
[90, "1m"], // floors, never rounds up — a total must not overstate
|
||||
[90.9, "1m"],
|
||||
[3599, "59m"],
|
||||
[3600, "1h 0m"], // the zero minutes stay: "1h" alone reads as an approximation
|
||||
[5025, "1h 23m"],
|
||||
[360000, "100h 0m"], // a large aggregate keeps counting hours rather than rolling to days
|
||||
[1, "1 sec"],
|
||||
[45, "45 sec"], // one Short: the regression case
|
||||
[45.7, "45 sec"], // truncates, so the seconds tier cannot overstate either
|
||||
[59, "59 sec"], // last second before the minute tier
|
||||
[60, "1 min"],
|
||||
[90, "1 min"], // floors, never rounds up — a total must not overstate
|
||||
[90.9, "1 min"],
|
||||
[3599, "59 min"],
|
||||
[3600, "1 hr 0 min"], // the zero minutes stay: "1 hr" alone reads as an approximation
|
||||
[5025, "1 hr 23 min"],
|
||||
[360000, "100 hr 0 min"], // a large aggregate keeps counting hours rather than rolling to days
|
||||
];
|
||||
|
||||
it.each(CASES)("formats %o as %o", (sec, expected) => {
|
||||
expect(formatRuntime(sec)).toBe(expected);
|
||||
});
|
||||
|
||||
it("speaks the UI language's units (DESIGN Copy: Intl short), not English letters", () => {
|
||||
i18nStub.language = "hu";
|
||||
expect(formatRuntime(5025)).toBe("1 ó 23 p");
|
||||
i18nStub.language = "en";
|
||||
});
|
||||
|
||||
it("never reports a non-zero duration as zero", () => {
|
||||
for (let sec = 1; sec < 3600; sec++) {
|
||||
expect(formatRuntime(sec), `${sec}s`).not.toBe("0m");
|
||||
expect(formatRuntime(sec), `${sec}s`).not.toBe("0 min");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -212,12 +219,20 @@ describe("formatDateTime / formatDate", () => {
|
||||
const asEnglish = formatDateTime(iso);
|
||||
i18nStub.language = "hu";
|
||||
const asHungarian = formatDateTime(iso);
|
||||
expect(asEnglish).toBe(new Date(iso).toLocaleString("en"));
|
||||
expect(asHungarian).toBe(new Date(iso).toLocaleString("hu"));
|
||||
expect(asEnglish).not.toBe(asHungarian);
|
||||
expect(formatDate(iso, "en")).not.toBe(formatDate(iso, "hu"));
|
||||
});
|
||||
|
||||
it("names the month and stops at the minute; seconds only in the log form (DESIGN Copy)", () => {
|
||||
process.env.TZ = "UTC";
|
||||
const iso = "2026-09-17T08:30:14Z";
|
||||
expect(formatDateTime(iso)).toBe("Sep 17, 2026, 08:30 AM");
|
||||
expect(formatDateTimeSec(iso)).toBe("Sep 17, 2026, 08:30:14 AM");
|
||||
i18nStub.language = "hu";
|
||||
expect(formatDateTime(iso)).toBe("2026. szept. 17. 08:30");
|
||||
expect(formatDate(iso, "hu")).toBe("2026. szept. 17.");
|
||||
});
|
||||
|
||||
it("shows a date-only value as that calendar day in every time zone", () => {
|
||||
process.env.TZ = "America/New_York";
|
||||
expect(formatDate("2026-09-17", "en")).toBe("Sep 17, 2026");
|
||||
|
||||
Reference in New Issue
Block a user