feat(i18n): the machinery, and the shell translated

The account has had a `locale` since the beginning, and it only chose which
grammar quick add parsed a line with — so setting it to English changed nothing
anybody could see. This is the other half of that field, started.

`t()` reads `i18n.locale`, which is `$state`, so every component that calls it
redraws when the language changes; that is what makes the setting take effect
without a reload, and why the module is `.svelte.js`. Danish is the source and the
fallback: 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, so dates follow the language instead of being `da-DK` in a dozen files.

`internal/i18n` is a Go package that exists only to fail the build when the two
dictionaries drift — the same trick `colors_test.go` plays on the palette. It
checks the placeholders as well as the keys: a translation that spells `{n}` as
`{antal}` leaves `{antal}` on the screen, which is worse than an untranslated
sentence because it looks like a bug in the app rather than a gap in a dictionary.

Converted so far: the shell. Sidebar, layout, quick add, task row, command
palette — fifty-two keys, and the surfaces people look at constantly. Twenty
files remain, the settings pages being the heavy ones.

The language picker is deliberately not wired up yet. It still says "Sprog i
hurtig tilføjelse" and still only changes the parser. Wiring it before the sweep
is done would hand anybody who picks English a bilingual screen — the
half-finished-theme failure `app.css` already taught us. As it stands Danish is
unchanged and English is not offered, which is the honest state for something
half done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-18 09:25:58 +02:00
co-authored by Claude Opus 5
parent 804963b222
commit e3c7fc792a
9 changed files with 403 additions and 55 deletions
+72
View File
@@ -0,0 +1,72 @@
/**
* English, translated from `da.js`.
*
* Danish is the source: it is the language the app was written in, and the one
* whose phrasing was argued over. A key missing here falls through to Danish
* rather than to the key itself — a Danish sentence in an English screen can be
* read, and `settings.notifications.title` cannot.
*
* Dates are not here. They go through `tag()`, which returns `en-GB` for this
* locale: the app is day-month everywhere, weeks start on Monday, and an American
* date format inside a European layout reads as a bug.
*/
export const en = {
// --- the shell ---------------------------------------------------------------
'nav.today': 'Today',
'nav.upcoming': 'Upcoming',
'nav.delegated': 'Waiting on others',
'nav.inbox': 'Inbox',
'nav.settings': 'Settings',
'nav.signOut': 'Sign out',
'nav.main': 'Main menu',
'nav.search': 'Search',
'nav.searchLong': 'Search tasks and projects',
'nav.noResults': 'Nothing found.',
'nav.showSidebar': 'Show the sidebar',
'nav.hideSidebar': 'Hide the sidebar',
'nav.showMenu': 'Show menu',
'nav.sidebarWidth': 'Sidebar width',
'nav.toggleTheme': 'Switch between light and dark',
'nav.offline': 'Offline',
'nav.offlineHint': 'Changes from other people are not showing right now',
'nav.doItToday': 'Do it today',
// --- the sidebar's own sections ------------------------------------------------
'sidebar.projects': 'Projects',
'sidebar.shared': 'Shared with me',
'sidebar.filters': 'Filters',
'sidebar.labels': 'Labels',
'sidebar.newProject': 'New project',
'sidebar.newGroup': 'New group',
'sidebar.projectName': 'Project name',
'sidebar.groupName': "The group's name",
'sidebar.noProjects': 'No projects yet.',
'sidebar.emptyGroup': 'Empty — drag a project up here.',
'sidebar.rename': 'Rename',
'sidebar.delete': 'Delete',
'sidebar.fold': 'Collapse {name}',
'sidebar.unfold': 'Expand {name}',
'sidebar.deleteGroup': 'Delete the group "{name}"?',
'sidebar.deleteGroupOne': 'The project in it stays, and ends up without a group.',
'sidebar.deleteGroupMany': 'The {n} projects stay, and end up without a group.',
// --- a task in a list ----------------------------------------------------------
'task.add': 'Add',
'task.new': 'New task',
'task.placeholder': 'Add a task — try “pay VAT tomorrow at 10 p1 #Company”',
'task.complete': 'Mark as done',
'task.reopen': 'Reopen task',
'task.today': 'Today',
'task.tomorrow': 'Tomorrow',
'task.yesterday': 'Yesterday',
'task.overdue': '{n} days overdue',
'task.repeats': 'Repeats {rule}',
'task.assignee': 'Assigned to {name}',
'task.subtasks': '{done} of {total} sub-tasks are done',
'task.attachments': '{n} attached',
'task.priority': 'Priority {n}',
'task.noPriority': 'No priority',
// --- a project group -----------------------------------------------------------
'group.colorOf': 'Colour of {name}'
};