feat(i18n): the interface speaks both languages
The account has had a `locale` since the beginning and it only chose which grammar quick add read a line with — so picking English changed nothing anybody could see, and the field said so honestly: "Sprog i hurtig tilføjelse". It says "Sprog" now, because it means it. Every component goes through `t()`. Danish is the source: a key missing from English falls through to Danish rather than to the key, because a Danish sentence in an English screen can be read and `settings.notifications.title` cannot. `tag()` is the single place `Intl` is asked. Before, `'da-DK'` was written into a dozen components, and that is the half of a translation that gets left behind — an English interface still saying "24. aug." is exactly the shape of a job half done. There is a smoke test that switches to English and refuses to see a Danish month name in the calendar's heading. Colour and theme names are keys now. `colors.js` keeps its ids untouched — they are what the database stores, and `internal/store` reads them out of that file — but `name` is `'color.tomato'`, resolved where it is drawn. The Inbox keeps its name. It is a project row the server creates at signup, not a string in the interface, and it can be renamed like any other project. Four copies of "3 hours ago" became one, in `web/src/lib/when.js`. They had already drifted: one said "aldrig logget ind", another "aldrig", and two stopped at days where the others carried on to a date. Routes stay Danish. `/indstillinger`, `/projekt`, `/uddelegeret` are bookmarks and they are in every smoke test; translating them buys nothing and costs both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3c7fc792a
commit
4d8b70918b
+35
-27
@@ -22,46 +22,54 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Danish messages for the codes the API can return. The server sends English
|
||||
* prose for logs; what a person reads is decided here, where the locale is known. */
|
||||
import { t } from './i18n.svelte.js';
|
||||
|
||||
/**
|
||||
* The key for each code the API can return.
|
||||
*
|
||||
* Keys rather than sentences, because the server's own prose is English and
|
||||
* written for a log — what a person reads is decided here, where the locale is
|
||||
* known. `errorcodes_test.go` walks this table against the codes Go can emit, so
|
||||
* a code the server gained and this did not fails the build rather than falling
|
||||
* through to that log prose.
|
||||
*/
|
||||
const MESSAGES = {
|
||||
unauthorized: 'Forkert e-mail eller adgangskode.',
|
||||
totp_required: 'Indtast koden fra din authenticator.',
|
||||
forbidden: 'Det har du ikke adgang til.',
|
||||
not_found: 'Findes ikke.',
|
||||
conflict: 'Det er der allerede.',
|
||||
rate_limited: 'For mange forsøg. Vent et øjeblik.',
|
||||
validation_failed: 'Tjek felterne herunder.',
|
||||
payload_too_large: 'Det er for stort.',
|
||||
internal_error: 'Noget gik galt. Prøv igen.',
|
||||
bad_request: 'Anmodningen kunne ikke læses.',
|
||||
unauthorized: 'error.unauthorized',
|
||||
totp_required: 'error.totpRequired',
|
||||
forbidden: 'error.forbidden',
|
||||
not_found: 'error.notFound',
|
||||
conflict: 'error.conflict',
|
||||
rate_limited: 'error.rateLimited',
|
||||
validation_failed: 'error.validation',
|
||||
payload_too_large: 'error.tooLarge',
|
||||
internal_error: 'error.internal',
|
||||
bad_request: 'error.badRequest',
|
||||
// Client-only: a network failure is not an HTTP status, so the server has no
|
||||
// code for it.
|
||||
offline: 'Ingen forbindelse til serveren.',
|
||||
offline: 'error.offline',
|
||||
|
||||
// Situations that used to arrive as a plain `conflict` and were therefore
|
||||
// shown as "Det er der allerede." — which was not merely vague but wrong. An
|
||||
// shown as "that already exists" — which was not merely vague but wrong. An
|
||||
// unconfigured Gmail is not a Gmail that is already connected.
|
||||
gmail_not_configured:
|
||||
'Gmail er ikke sat op på denne server. Administratoren skal registrere en OAuth-klient hos Google og sætte VERDANDE_GMAIL_CLIENT_ID og _SECRET.',
|
||||
ai_not_configured: 'Der er ikke valgt en AI-udbyder. Sæt den op under Indstillinger → AI.',
|
||||
totp_not_enabled: 'To-faktor er ikke slået til.',
|
||||
inbox_protected: 'Indbakken kan ikke slettes.',
|
||||
last_admin:
|
||||
'Det er den sidste administrator. Gør en anden til administrator først — ellers er der ingen, der kan komme ind på denne side igen.'
|
||||
gmail_not_configured: 'error.gmailNotConfigured',
|
||||
ai_not_configured: 'error.aiNotConfigured',
|
||||
totp_not_enabled: 'error.totpNotEnabled',
|
||||
inbox_protected: 'error.inboxProtected',
|
||||
last_admin: 'error.lastAdmin'
|
||||
};
|
||||
|
||||
/**
|
||||
* What to show a person for a failed request.
|
||||
*
|
||||
* A code with no message here falls back to the server's prose, which is English
|
||||
* and written for a log — not good, but better than swallowing the only
|
||||
* explanation there is. A code that turns up in that fallback is a code this
|
||||
* table is missing.
|
||||
* A code with no key here falls back to the server's prose, which is English and
|
||||
* written for a log — not good, but better than swallowing the only explanation
|
||||
* there is. A code that turns up in that fallback is a code this table is missing.
|
||||
*/
|
||||
export function humanMessage(err) {
|
||||
if (!(err instanceof ApiError)) return MESSAGES.offline;
|
||||
return MESSAGES[err.code] ?? err.message ?? MESSAGES.internal_error;
|
||||
if (!(err instanceof ApiError)) return t(MESSAGES.offline);
|
||||
const key = MESSAGES[err.code];
|
||||
if (key) return t(key);
|
||||
return err.message ?? t(MESSAGES.internal_error);
|
||||
}
|
||||
|
||||
async function request(method, path, body, options = {}) {
|
||||
|
||||
Reference in New Issue
Block a user