fix(i18n): en gentagelse siges nu på læserens sprog
`recurrence.Describe` skriver dansk. Fladen kunne navngive de fem almindelige regler selv, og alt andet — "hver mandag", "hverdage", "hver anden uge tirsdag" — faldt tilbage til serverens tekst, som er dansk uanset hvad kontoen er sat til. Serveren kan ikke bare tage en locale med, og det er ikke dovenskab: `toTaskJSON` har niogtyve kaldesteder, og mange af dem er WebSocket-udsendelser, hvor én nyttelast når alle i et projekt på én gang. Der findes ikke ét sprog at skrive den i. Men reglen er den samme, uanset hvem der læser den. Den ligger allerede i nyttelasten, så beskrivelsen hører hjemme dér, hvor læseren er — i fladen. Serverens tekst bliver stående som reserve. Den er stadig den, et API-kald og en MCP-forbindelse får, og de har ingen flade at oversætte i. Ugedagen kommer fra `Intl` og ikke fra en tabel — ikke for at spare syv strenge pr. sprog, men for at få de navne, sproget faktisk bruger. Dagene sorteres, som ugen går, frem for som reglen tilfældigvis skrev dem: "mandag og torsdag" og "torsdag og mandag" er den samme regel, og den ene af dem er den, folk siger. Og to har sit eget ord på begge sprog — "hver anden", "every other". Det er ikke tallet to sat ind i den samme sætning, så det er sin egen nøgle. Et sprog, der bøjer enheden efter tallet, kan ikke sættes sammen af stumper; derfor står tallet inde i teksten frem for at blive limet på. Prøven bruger `hver mandag` og `hverdage`, fordi de netop *ikke* står på listen over de fem, fladen altid har kunnet. En tredje vending, jeg først skrev prøven med — "hver anden tirsdag" — viste sig ikke at være en, hurtig tilføjelse overhovedet læser: den lagde "hver anden" i titlen og gjorde "tirsdag" til en dato. Det er en anden sag, og den står nu skrevet ét sted, hvor den kan tages op. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f64b7c1f01
commit
ef49f404db
@@ -3423,3 +3423,50 @@ test('et projekts kalender kan vises som en uge', async ({ page }) => {
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* En gentagelse siges på læserens sprog, også dem serveren kun kan på dansk.
|
||||
*
|
||||
* `recurrence.Describe` skriver dansk og kan ikke bare tage en locale med:
|
||||
* `toTaskJSON` har niogtyve kaldesteder, og mange er WebSocket-udsendelser, hvor én
|
||||
* nyttelast når alle i et projekt på én gang — der findes ikke ét sprog at skrive
|
||||
* den i. Reglen er den samme uanset hvem der læser den, så beskrivelsen hører
|
||||
* hjemme dér, hvor læseren er.
|
||||
*
|
||||
* De to her er valgt, fordi de *ikke* står på listen over de fem almindelige, som
|
||||
* fladen altid har kunnet navngive. Før det her faldt de tilbage til serverens
|
||||
* tekst — og den er dansk, hvad end kontoen er sat til.
|
||||
*/
|
||||
test('en gentagelse, serveren kun kan på dansk, siges alligevel i fladen', async ({ page }) => {
|
||||
const trouble = watchForTrouble(page);
|
||||
await page.goto('/');
|
||||
|
||||
for (const [text, says] of [
|
||||
['vande planterne hver mandag', 'hver mandag'],
|
||||
['tømme opvasker hverdage', 'hverdage']
|
||||
]) {
|
||||
const box = page.getByLabel('Ny opgave');
|
||||
await box.fill(text);
|
||||
await box.press('Enter');
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
// I indbakken og ikke på I dag: en ugentlig regel sætter den første
|
||||
// forfaldsdag til den næste sådan dag, som sjældent er i dag.
|
||||
await page
|
||||
.getByRole('navigation', { name: 'Hovedmenu' })
|
||||
.getByRole('link', { name: /^Indbakke/ })
|
||||
.click();
|
||||
|
||||
const name = text.split(' ').slice(0, 2).join(' ');
|
||||
const row = page.locator('.row', { hasText: name }).first();
|
||||
await expect(row).toBeVisible();
|
||||
await expect(
|
||||
row.locator('.repeat'),
|
||||
`"${text}" blev ikke læst som en gentagelse`
|
||||
).toContainText(says);
|
||||
|
||||
await page.goto('/');
|
||||
}
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -152,6 +152,25 @@ export const da = {
|
||||
'task.yesterday': 'I går',
|
||||
'task.overdue': '{n} dage forsinket',
|
||||
'task.repeats': 'Gentages {rule}',
|
||||
'repeat.weekdays': 'hverdage',
|
||||
'repeat.weekends': 'hver weekend',
|
||||
'repeat.and': 'og',
|
||||
'repeat.everyDay': 'hver {day}',
|
||||
'repeat.everyNthWeekOn': 'hver {n}. uge {day}',
|
||||
'repeat.everyOtherWeekOn': 'hver anden uge {day}',
|
||||
'repeat.monthlyOn': 'den {day}. hver måned',
|
||||
'repeat.every.day': 'hver dag',
|
||||
'repeat.every.week': 'hver uge',
|
||||
'repeat.every.month': 'hver måned',
|
||||
'repeat.every.year': 'hvert år',
|
||||
'repeat.everyOther.day': 'hver anden dag',
|
||||
'repeat.everyOther.week': 'hver anden uge',
|
||||
'repeat.everyOther.month': 'hver anden måned',
|
||||
'repeat.everyOther.year': 'hvert andet år',
|
||||
'repeat.everyN.day': 'hver {n}. dag',
|
||||
'repeat.everyN.week': 'hver {n}. uge',
|
||||
'repeat.everyN.month': 'hver {n}. måned',
|
||||
'repeat.everyN.year': 'hvert {n}. år',
|
||||
'task.assignee': 'Ansvarlig: {name}',
|
||||
'task.subtasks': '{done} af {total} undertasks er lukket',
|
||||
'task.attachments': '{n} vedhæftet',
|
||||
|
||||
@@ -148,6 +148,25 @@ export const en = {
|
||||
'task.yesterday': 'Yesterday',
|
||||
'task.overdue': '{n} days overdue',
|
||||
'task.repeats': 'Repeats {rule}',
|
||||
'repeat.weekdays': 'weekdays',
|
||||
'repeat.weekends': 'every weekend',
|
||||
'repeat.and': 'and',
|
||||
'repeat.everyDay': 'every {day}',
|
||||
'repeat.everyNthWeekOn': 'every {n} weeks on {day}',
|
||||
'repeat.everyOtherWeekOn': 'every other week on {day}',
|
||||
'repeat.monthlyOn': 'the {day}th of every month',
|
||||
'repeat.every.day': 'every day',
|
||||
'repeat.every.week': 'every week',
|
||||
'repeat.every.month': 'every month',
|
||||
'repeat.every.year': 'every year',
|
||||
'repeat.everyOther.day': 'every other day',
|
||||
'repeat.everyOther.week': 'every other week',
|
||||
'repeat.everyOther.month': 'every other month',
|
||||
'repeat.everyOther.year': 'every other year',
|
||||
'repeat.everyN.day': 'every {n} days',
|
||||
'repeat.everyN.week': 'every {n} weeks',
|
||||
'repeat.everyN.month': 'every {n} months',
|
||||
'repeat.everyN.year': 'every {n} years',
|
||||
'task.assignee': 'Assigned to {name}',
|
||||
'task.subtasks': '{done} of {total} sub-tasks are done',
|
||||
'task.attachments': '{n} attached',
|
||||
|
||||
+108
-2
@@ -73,8 +73,114 @@ export const REPEATS = [
|
||||
{ rule: 'FREQ=YEARLY', label: 'detail.yearly' }
|
||||
];
|
||||
|
||||
/** What a repeating task says on its row. */
|
||||
/**
|
||||
* What a repeating task says on its row.
|
||||
*
|
||||
* Beskrevet her og ikke på serveren, og det er ikke en smagssag.
|
||||
*
|
||||
* `recurrence.Describe` skriver dansk. Den kan ikke bare tage en locale med:
|
||||
* `toTaskJSON` har niogtyve kaldesteder, og mange af dem er WebSocket-udsendelser,
|
||||
* hvor én nyttelast når alle i et projekt på én gang — der findes ikke ét sprog at
|
||||
* skrive den i. Reglen ligger allerede i nyttelasten, og den er den samme uanset
|
||||
* hvem der læser den. Så beskrivelsen hører hjemme dér, hvor læseren er.
|
||||
*
|
||||
* Serverens tekst bliver som reserve. Den er stadig den, et API-kald og en
|
||||
* MCP-forbindelse får, og de har ingen flade at oversætte i.
|
||||
*/
|
||||
export function repeatLabel(rule, fallback = '') {
|
||||
if (!rule) return '';
|
||||
const known = REPEATS.find((option) => option.rule && option.rule === rule);
|
||||
return known ? t(known.label) : fallback;
|
||||
if (known) return t(known.label);
|
||||
return describeRule(rule) || fallback;
|
||||
}
|
||||
|
||||
/** Ugedagene i den rækkefølge RRULE navngiver dem. */
|
||||
const RRULE_DAYS = { MO: 0, TU: 1, WE: 2, TH: 3, FR: 4, SA: 5, SU: 6 };
|
||||
|
||||
/**
|
||||
* Ugedagens navn på læserens sprog, fra Intl frem for fra en tabel.
|
||||
*
|
||||
* Ikke for at spare syv strenge pr. sprog — for at få de navne, sproget faktisk
|
||||
* bruger. Den 1. januar 2024 var en mandag; enhver mandag ville gøre det, og en
|
||||
* fast en holder det ude af tegningen.
|
||||
*/
|
||||
function dayName(code) {
|
||||
const index = RRULE_DAYS[code];
|
||||
if (index === undefined) return code;
|
||||
return new Intl.DateTimeFormat(tag(), { weekday: 'long' }).format(
|
||||
new Date(Date.UTC(2024, 0, 1 + index))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* En RRULE sagt med ord.
|
||||
*
|
||||
* Dækker det, verdandes egen hurtig tilføjelse kan lave, og falder tilbage til
|
||||
* reglen selv for alt andet — en kalenderklient kan sende ting, ingen her har
|
||||
* skrevet, og en halv oversættelse af dem ville være værre end den rå regel.
|
||||
*/
|
||||
export function describeRule(rule) {
|
||||
if (!rule) return '';
|
||||
const parts = {};
|
||||
for (const pair of rule.replace(/^RRULE:/i, '').split(';')) {
|
||||
const [key, value] = pair.split('=');
|
||||
if (key && value !== undefined) parts[key.toUpperCase()] = value;
|
||||
}
|
||||
|
||||
const interval = Number(parts.INTERVAL) > 0 ? Number(parts.INTERVAL) : 1;
|
||||
|
||||
if (parts.BYDAY) {
|
||||
if (parts.BYDAY === 'MO,TU,WE,TH,FR') return t('repeat.weekdays');
|
||||
if (parts.BYDAY === 'SA,SU') return t('repeat.weekends');
|
||||
|
||||
// Sorteret som ugen går, ikke som reglen tilfældigvis skrev dem: "mandag og
|
||||
// torsdag" og "torsdag og mandag" er den samme regel, og den ene af dem er
|
||||
// den, folk siger.
|
||||
const names = parts.BYDAY.split(',')
|
||||
.filter((code) => code in RRULE_DAYS)
|
||||
.sort((a, b) => RRULE_DAYS[a] - RRULE_DAYS[b])
|
||||
.map(dayName);
|
||||
if (!names.length) return '';
|
||||
|
||||
const joined =
|
||||
names.length === 1
|
||||
? names[0]
|
||||
: names.slice(0, -1).join(', ') + ' ' + t('repeat.and') + ' ' + names.at(-1);
|
||||
|
||||
// "hver mandag", ikke "hver uge mandag". En ugentlig regel, der navngiver
|
||||
// sine dage, har ikke også brug for ordet for intervallet.
|
||||
if (interval <= 1) return t('repeat.everyDay', { day: joined });
|
||||
// To har sit eget ord på begge sprog — "hver anden", "every other" — og det
|
||||
// er ikke tallet to sat ind i den samme sætning. Et sprog, der bøjer, kan
|
||||
// ikke sættes sammen af stumper.
|
||||
if (interval === 2) return t('repeat.everyOtherWeekOn', { day: joined });
|
||||
return t('repeat.everyNthWeekOn', { n: interval, day: joined });
|
||||
}
|
||||
|
||||
switch (parts.FREQ?.toUpperCase()) {
|
||||
case 'DAILY':
|
||||
return every(interval, 'day');
|
||||
case 'WEEKLY':
|
||||
return every(interval, 'week');
|
||||
case 'MONTHLY':
|
||||
if (parts.BYMONTHDAY) return t('repeat.monthlyOn', { day: parts.BYMONTHDAY });
|
||||
return every(interval, 'month');
|
||||
case 'YEARLY':
|
||||
return every(interval, 'year');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "hver dag", "hver anden uge", "hver tredje måned".
|
||||
*
|
||||
* Tallet står i teksten frem for at blive sat sammen af stumper: "every other
|
||||
* week" og "hver anden uge" er ikke to ord byttet om, og et sprog, der bøjer
|
||||
* enheden efter tallet, kan ikke sættes sammen af to nøgler.
|
||||
*/
|
||||
function every(interval, unit) {
|
||||
if (interval <= 1) return t(`repeat.every.${unit}`);
|
||||
if (interval === 2) return t(`repeat.everyOther.${unit}`);
|
||||
return t(`repeat.everyN.${unit}`, { n: interval });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user