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>
117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
// Package i18n exists only to hold this test.
|
|
//
|
|
// The interface's strings live in the frontend, where they are read; nothing in Go
|
|
// needs them. What Go can do is fail the build when the two dictionaries drift,
|
|
// which is the failure mode a translation always has and which nothing else here
|
|
// would notice: a key present in Danish and missing from English renders as Danish
|
|
// inside an English screen, and a key present only in English is a string nobody
|
|
// will ever see.
|
|
//
|
|
// A scanner rather than a parser, the same approach `colors_test.go` and
|
|
// `openapi_test.go` take: it needs the keys out of two lists this repository owns,
|
|
// and that is worth more than a dependency in a project whose whole shape is "one
|
|
// static binary".
|
|
package i18n
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTheTwoDictionariesHaveTheSameKeys(t *testing.T) {
|
|
dir := filepath.Join("..", "..", "web", "src", "lib", "locales")
|
|
da := readKeys(t, filepath.Join(dir, "da.js"))
|
|
en := readKeys(t, filepath.Join(dir, "en.js"))
|
|
|
|
if len(da) == 0 {
|
|
t.Fatal("no keys found in da.js — the scanner is looking at the wrong shape")
|
|
}
|
|
|
|
var missing, extra []string
|
|
for key := range da {
|
|
if !en[key] {
|
|
missing = append(missing, key)
|
|
}
|
|
}
|
|
for key := range en {
|
|
if !da[key] {
|
|
extra = append(extra, key)
|
|
}
|
|
}
|
|
sort.Strings(missing)
|
|
sort.Strings(extra)
|
|
|
|
if len(missing) > 0 {
|
|
t.Errorf("%d keys are in da.js and not in en.js — they render as Danish inside an "+
|
|
"English screen:\n %s", len(missing), strings.Join(missing, "\n "))
|
|
}
|
|
if len(extra) > 0 {
|
|
t.Errorf("%d keys are in en.js and not in da.js — nothing reads them, because Danish "+
|
|
"is the source:\n %s", len(extra), strings.Join(extra, "\n "))
|
|
}
|
|
}
|
|
|
|
// The placeholders have to match too. `t()` fills `{name}` from what the caller
|
|
// passes, so a translation that spells one differently leaves `{navn}` on the
|
|
// screen — which is worse than an untranslated sentence, because it looks like a
|
|
// bug in the app rather than a gap in the dictionary.
|
|
func TestTheTwoDictionariesUseTheSamePlaceholders(t *testing.T) {
|
|
dir := filepath.Join("..", "..", "web", "src", "lib", "locales")
|
|
da := readEntries(t, filepath.Join(dir, "da.js"))
|
|
en := readEntries(t, filepath.Join(dir, "en.js"))
|
|
|
|
for key, danish := range da {
|
|
english, ok := en[key]
|
|
if !ok {
|
|
continue // reported by the test above
|
|
}
|
|
want := placeholders(danish)
|
|
got := placeholders(english)
|
|
if strings.Join(want, ",") != strings.Join(got, ",") {
|
|
t.Errorf("%s: Danish fills %v, English fills %v", key, want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
var (
|
|
// `'some.key': 'the string',` — single-quoted keys and values, which is what
|
|
// prettier writes for these files.
|
|
reEntry = regexp.MustCompile(`(?m)^\s*'([a-zA-Z0-9._]+)':\s*(.*?),?\s*$`)
|
|
rePlaceholder = regexp.MustCompile(`\{(\w+)\}`)
|
|
)
|
|
|
|
func readKeys(t *testing.T, path string) map[string]bool {
|
|
t.Helper()
|
|
keys := map[string]bool{}
|
|
for key := range readEntries(t, path) {
|
|
keys[key] = true
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func readEntries(t *testing.T, path string) map[string]string {
|
|
t.Helper()
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", filepath.Base(path), err)
|
|
}
|
|
out := map[string]string{}
|
|
for _, m := range reEntry.FindAllStringSubmatch(string(body), -1) {
|
|
out[m[1]] = m[2]
|
|
}
|
|
return out
|
|
}
|
|
|
|
func placeholders(s string) []string {
|
|
var found []string
|
|
for _, m := range rePlaceholder.FindAllStringSubmatch(s, -1) {
|
|
found = append(found, m[1])
|
|
}
|
|
sort.Strings(found)
|
|
return found
|
|
}
|