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
@@ -16,6 +16,8 @@
|
||||
import { api, humanMessage } from '$lib/api.js';
|
||||
import { app } from '$lib/stores.svelte.js';
|
||||
import { eventName, eventDetail } from '$lib/events.js';
|
||||
import { t } from '$lib/i18n.svelte.js';
|
||||
import { ago } from '$lib/when.js';
|
||||
|
||||
let entries = $state([]);
|
||||
let cursor = $state(null);
|
||||
@@ -94,19 +96,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function when(iso) {
|
||||
const then = new Date(iso);
|
||||
const seconds = Math.round((Date.now() - then) / 1000);
|
||||
if (seconds < 60) return 'lige nu';
|
||||
if (seconds < 3600) return `for ${Math.floor(seconds / 60)} min. siden`;
|
||||
if (seconds < 86400) return `for ${Math.floor(seconds / 3600)} timer siden`;
|
||||
return then.toLocaleString('da-DK', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
let filtered = $derived(Boolean(filterUser || filterProject || filterEvent));
|
||||
|
||||
@@ -119,21 +108,17 @@
|
||||
|
||||
<section class="panel">
|
||||
<header>
|
||||
<h2>Historik</h2>
|
||||
<h2>{t('history.title')}</h2>
|
||||
<p class="hint">
|
||||
Alt, der er blevet gjort på denne server, på tværs af alle projekter — også
|
||||
dem, du ikke selv er med i. Projekternes egne historik-paneler viser det
|
||||
samme, ét projekt ad gangen; det her er spørgsmålet, de ikke kan svare på.
|
||||
Rækkerne bliver stående, når en konto slettes: det, der blev gjort, holder op
|
||||
med at have et navn, ikke med at være sket.
|
||||
{t('history.hint')}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="filters">
|
||||
<label>
|
||||
<span>Person</span>
|
||||
<span>{t('history.person')}</span>
|
||||
<select bind:value={filterUser}>
|
||||
<option value="">Alle</option>
|
||||
<option value="">{t('history.all')}</option>
|
||||
{#each people as person (person.id)}
|
||||
<option value={person.id}>{person.name}</option>
|
||||
{/each}
|
||||
@@ -141,9 +126,9 @@
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Projekt</span>
|
||||
<span>{t('history.project')}</span>
|
||||
<select bind:value={filterProject}>
|
||||
<option value="">Alle</option>
|
||||
<option value="">{t('history.all')}</option>
|
||||
{#each projects as project (project.id)}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
@@ -151,9 +136,9 @@
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Hændelse</span>
|
||||
<span>{t('history.event')}</span>
|
||||
<select bind:value={filterEvent}>
|
||||
<option value="">Alle</option>
|
||||
<option value="">{t('history.all')}</option>
|
||||
{#each events as event (event.event)}
|
||||
<option value={event.event}>{eventName(event.event)} ({event.count})</option>
|
||||
{/each}
|
||||
@@ -161,7 +146,7 @@
|
||||
</label>
|
||||
|
||||
{#if filtered}
|
||||
<button class="secondary" onclick={clearFilters}>Ryd</button>
|
||||
<button class="secondary" onclick={clearFilters}>{t('history.clear')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -172,7 +157,7 @@
|
||||
<div class="what">
|
||||
<span class="primary-line">
|
||||
<span class="who" class:gone={!entry.user_name}
|
||||
>{entry.user_name || 'En slettet konto'}</span
|
||||
>{entry.user_name || t('history.deletedAccount')}</span
|
||||
>
|
||||
<span class="event"
|
||||
>{eventName(entry.event)}{#if eventDetail(entry)}
|
||||
@@ -181,7 +166,7 @@
|
||||
</span>
|
||||
<span class="secondary">{entry.project_name}</span>
|
||||
</div>
|
||||
<span class="when">{when(entry.at)}</span>
|
||||
<span class="when">{ago(entry.at)}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -189,15 +174,15 @@
|
||||
{#if cursor}
|
||||
<div class="row">
|
||||
<button class="secondary" onclick={more} disabled={loading}>
|
||||
{loading ? 'Henter…' : 'Hent flere'}
|
||||
{loading ? t('history.loading') : t('history.more')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if loaded}
|
||||
<p class="hint">
|
||||
{filtered
|
||||
? 'Ingen hændelser matcher de valgte filtre.'
|
||||
: 'Der er ikke registreret noget endnu.'}
|
||||
? t('history.noMatch')
|
||||
: t('history.nothing')}
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user