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>
76 lines
1.6 KiB
Svelte
76 lines
1.6 KiB
Svelte
<script>
|
|
/**
|
|
* Everything carrying one label.
|
|
*
|
|
* This runs the filter language rather than a bespoke endpoint: "@indkøb" is
|
|
* already exactly this query, and having one code path means a label page and
|
|
* a saved filter can never disagree about what a label selects.
|
|
*/
|
|
import { page } from '$app/stores';
|
|
import { api } from '$lib/api.js';
|
|
import { app } from '$lib/stores.svelte.js';
|
|
import TaskRow from '$lib/components/TaskRow.svelte';
|
|
import { t } from '$lib/i18n.svelte.js';
|
|
|
|
let loaded = $state(false);
|
|
let name = $derived(decodeURIComponent($page.params.navn ?? ''));
|
|
|
|
$effect(() => {
|
|
const label = name;
|
|
if (!label) return;
|
|
loaded = false;
|
|
api
|
|
.previewFilter(`@${label}`)
|
|
.then((result) => {
|
|
app.tasks = result?.tasks ?? [];
|
|
})
|
|
.catch(() => {
|
|
app.tasks = [];
|
|
})
|
|
.finally(() => (loaded = true));
|
|
});
|
|
|
|
let open = $derived(app.tasks.filter((t) => !t.completed));
|
|
</script>
|
|
|
|
<div class="view">
|
|
<header><h1>@{name}</h1></header>
|
|
|
|
{#each open as task (task.id)}
|
|
<TaskRow {task} />
|
|
{/each}
|
|
|
|
{#if loaded && !open.length}
|
|
<p class="clear">
|
|
<span class="rune" aria-hidden="true">ᚹ</span>
|
|
{t('view.labelEmpty')}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.view {
|
|
max-width: var(--content-max);
|
|
margin: 0 auto;
|
|
padding: var(--s6) var(--s4) var(--s8);
|
|
}
|
|
h1 {
|
|
font-size: var(--text-2xl);
|
|
margin-bottom: var(--s5);
|
|
}
|
|
.clear {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--s3);
|
|
padding: var(--s8) var(--s4);
|
|
color: var(--ink-faint);
|
|
font-size: var(--text-sm);
|
|
}
|
|
.rune {
|
|
font-size: var(--text-2xl);
|
|
color: var(--accent);
|
|
opacity: 0.5;
|
|
}
|
|
</style>
|