refactor(i18n): slim locale files using composable grammar

Reduce locale files from ~4600 to 338 total lines (93% reduction):
- en_GB.json: 1520 → 262 lines (83% reduction)
- en_US.json: ~1500 → 10 lines (US spelling overrides only)
- en_AU.json: ~1500 → 2 lines (inherits from en_GB)
- de.json: 120 → 64 lines

Removed redundant entries that can now be composed via core.* patterns:
- Labels: T("core.label.status") → "Status:"
- Counts: T("core.count.item", 5) → "5 items"
- Progress: T("core.progress.build") → "Building..."
- Done/Fail: T("core.done.build", subj) → "Build completed"

Updated tests to use new key patterns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 14:24:23 +00:00
parent 621540433b
commit 39de3c2836
7 changed files with 263 additions and 4124 deletions

View file

@ -23,8 +23,8 @@ func TestTranslate(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Basic translation // Basic translation
result := svc.T("cli.success") result := svc.T("cmd.dev.short")
assert.Equal(t, "Success", result) assert.Equal(t, "Multi-repo development workflow", result)
// Missing key returns the key // Missing key returns the key
result = svc.T("nonexistent.key") result = svc.T("nonexistent.key")
@ -36,11 +36,11 @@ func TestTranslateWithArgs(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Translation with template data // Translation with template data
result := svc.T("error.not_found", map[string]string{"Item": "config.yaml"}) result := svc.T("error.repo_not_found", map[string]string{"Name": "config.yaml"})
assert.Equal(t, "Not found: config.yaml", result) assert.Equal(t, "Repository 'config.yaml' not found", result)
result = svc.T("cli.time.minutes_ago", map[string]int{"Count": 5}) result = svc.T("cmd.ai.task_pr.branch_error", map[string]string{"Branch": "main"})
assert.Equal(t, "5 minutes ago", result) assert.Equal(t, "cannot create PR from main branch; create a feature branch first", result)
} }
func TestSetLanguage(t *testing.T) { func TestSetLanguage(t *testing.T) {
@ -71,8 +71,8 @@ func TestDefaultService(t *testing.T) {
require.NotNil(t, svc) require.NotNil(t, svc)
// Global T function should work // Global T function should work
result := T("cli.success") result := T("cmd.dev.short")
assert.Equal(t, "Success", result) assert.Equal(t, "Multi-repo development workflow", result)
} }
func TestAddMessages(t *testing.T) { func TestAddMessages(t *testing.T) {
@ -138,17 +138,18 @@ func TestDetectLanguage(t *testing.T) {
func TestPluralization(t *testing.T) { func TestPluralization(t *testing.T) {
svc, err := New() svc, err := New()
require.NoError(t, err) require.NoError(t, err)
SetDefault(svc)
// Singular // Singular - uses core.count.* magic
result := svc.T("cli.count.items", map[string]any{"Count": 1}) result := svc.T("core.count.item", 1)
assert.Equal(t, "1 item", result) assert.Equal(t, "1 item", result)
// Plural // Plural
result = svc.T("cli.count.items", map[string]any{"Count": 5}) result = svc.T("core.count.item", 5)
assert.Equal(t, "5 items", result) assert.Equal(t, "5 items", result)
// Zero uses plural // Zero uses plural
result = svc.T("cli.count.items", map[string]any{"Count": 0}) result = svc.T("core.count.item", 0)
assert.Equal(t, "0 items", result) assert.Equal(t, "0 items", result)
} }
@ -156,13 +157,13 @@ func TestNestedKeys(t *testing.T) {
svc, err := New() svc, err := New()
require.NoError(t, err) require.NoError(t, err)
// Deeply nested key // Nested key
result := svc.T("cmd.dev.work.short") result := svc.T("cmd.dev.short")
assert.Equal(t, "Multi-repo git operations", result) assert.Equal(t, "Multi-repo development workflow", result)
// Nested with flag // Deeper nested key (flat key with dots)
result = svc.T("cmd.dev.work.flag.status") result = svc.T("cmd.dev.push.short")
assert.Equal(t, "Show status only, don't push", result) assert.Equal(t, "Push commits across all repos", result)
} }
func TestMessage_ForCategory(t *testing.T) { func TestMessage_ForCategory(t *testing.T) {
@ -260,21 +261,21 @@ func TestDebugMode(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Without debug // Without debug
result := svc.T("cli.success") result := svc.T("cmd.dev.short")
assert.Equal(t, "Success", result) assert.Equal(t, "Multi-repo development workflow", result)
// Enable debug // Enable debug
svc.SetDebug(true) svc.SetDebug(true)
assert.True(t, svc.Debug()) assert.True(t, svc.Debug())
// With debug - shows key prefix // With debug - shows key prefix
result = svc.T("cli.success") result = svc.T("cmd.dev.short")
assert.Equal(t, "[cli.success] Success", result) assert.Equal(t, "[cmd.dev.short] Multi-repo development workflow", result)
// Disable debug // Disable debug
svc.SetDebug(false) svc.SetDebug(false)
result = svc.T("cli.success") result = svc.T("cmd.dev.short")
assert.Equal(t, "Success", result) assert.Equal(t, "Multi-repo development workflow", result)
}) })
t.Run("C with debug mode", func(t *testing.T) { t.Run("C with debug mode", func(t *testing.T) {
@ -311,8 +312,8 @@ func TestDebugMode(t *testing.T) {
assert.True(t, Default().Debug()) assert.True(t, Default().Debug())
// Translate // Translate
result := T("cli.success") result := T("cmd.dev.short")
assert.Equal(t, "[cli.success] Success", result) assert.Equal(t, "[cmd.dev.short] Multi-repo development workflow", result)
// Cleanup // Cleanup
SetDebug(false) SetDebug(false)

View file

@ -270,8 +270,8 @@ func TestIntentT_Integration(t *testing.T) {
assert.Equal(t, "Delete config.yaml?", result) assert.Equal(t, "Delete config.yaml?", result)
// Using T with regular key should work normally // Using T with regular key should work normally
result = svc.T("cli.success") result = svc.T("cmd.dev.short")
assert.Equal(t, "Success", result) assert.Equal(t, "Multi-repo development workflow", result)
} }
func TestIntent_EmptyTemplates(t *testing.T) { func TestIntent_EmptyTemplates(t *testing.T) {

View file

@ -19,7 +19,7 @@ func TestServiceImplementsTranslator(t *testing.T) {
translator = svc translator = svc
// Test interface methods // Test interface methods
assert.Equal(t, "Success", translator.T("cli.success")) assert.Equal(t, "Multi-repo development workflow", translator.T("cmd.dev.short"))
assert.NotEmpty(t, translator.Language()) assert.NotEmpty(t, translator.Language())
assert.NotNil(t, translator.Direction()) assert.NotNil(t, translator.Direction())
assert.NotNil(t, translator.Formality()) assert.NotNil(t, translator.Formality())

View file

@ -1,120 +1,64 @@
{ {
"gram.verb.delete": { "base": "löschen", "past": "gelöscht", "gerund": "löschend" }, "gram": {
"gram.verb.save": { "base": "speichern", "past": "gespeichert", "gerund": "speichernd" }, "verb": {
"gram.verb.create": { "base": "erstellen", "past": "erstellt", "gerund": "erstellend" }, "delete": { "base": "löschen", "past": "gelöscht", "gerund": "löschend" },
"gram.verb.update": { "base": "aktualisieren", "past": "aktualisiert", "gerund": "aktualisierend" }, "save": { "base": "speichern", "past": "gespeichert", "gerund": "speichernd" },
"gram.verb.build": { "base": "bauen", "past": "gebaut", "gerund": "bauend" }, "create": { "base": "erstellen", "past": "erstellt", "gerund": "erstellend" },
"gram.verb.run": { "base": "laufen", "past": "gelaufen", "gerund": "laufend" }, "update": { "base": "aktualisieren", "past": "aktualisiert", "gerund": "aktualisierend" },
"gram.verb.check": { "base": "prüfen", "past": "geprüft", "gerund": "prüfend" }, "build": { "base": "bauen", "past": "gebaut", "gerund": "bauend" },
"gram.verb.install": { "base": "installieren", "past": "installiert", "gerund": "installierend" }, "run": { "base": "laufen", "past": "gelaufen", "gerund": "laufend" },
"gram.verb.push": { "base": "pushen", "past": "gepusht", "gerund": "pushend" }, "check": { "base": "prüfen", "past": "geprüft", "gerund": "prüfend" },
"gram.verb.pull": { "base": "pullen", "past": "gepullt", "gerund": "pullend" }, "install": { "base": "installieren", "past": "installiert", "gerund": "installierend" },
"gram.verb.commit": { "base": "committen", "past": "committet", "gerund": "committend" }, "push": { "base": "pushen", "past": "gepusht", "gerund": "pushend" },
"pull": { "base": "pullen", "past": "gepullt", "gerund": "pullend" },
"gram.noun.file": { "one": "Datei", "other": "Dateien", "gender": "feminine" }, "commit": { "base": "committen", "past": "committet", "gerund": "committend" }
"gram.noun.repo": { "one": "Repository", "other": "Repositories", "gender": "neuter" },
"gram.noun.commit": { "one": "Commit", "other": "Commits", "gender": "masculine" },
"gram.noun.branch": { "one": "Branch", "other": "Branches", "gender": "masculine" },
"gram.noun.change": { "one": "Änderung", "other": "Änderungen", "gender": "feminine" },
"gram.noun.item": { "one": "Element", "other": "Elemente", "gender": "neuter" },
"gram.article.indefinite.masculine": "ein",
"gram.article.indefinite.feminine": "eine",
"gram.article.indefinite.neuter": "ein",
"gram.article.definite.masculine": "der",
"gram.article.definite.feminine": "die",
"gram.article.definite.neuter": "das",
"cli.success": "Erfolg",
"cli.error": "Fehler",
"cli.warning": "Warnung",
"cli.info": "Info",
"cli.done": "Fertig",
"cli.failed": "Fehlgeschlagen",
"cli.pass": "BESTANDEN",
"cli.fail": "FEHLGESCHLAGEN",
"cli.ok": "OK",
"cli.skip": "Übersprungen",
"cli.pending": "Ausstehend",
"cli.running": "Läuft",
"cli.completed": "Abgeschlossen",
"cli.cancelled": "Abgebrochen",
"cli.aborted": "Abgebrochen",
"cli.confirm.yes": "Ja",
"cli.confirm.no": "Nein",
"cli.confirm.proceed": "Fortfahren?",
"cli.confirm.continue": "Weiter?",
"cli.confirm.abort": "Vorgang abgebrochen",
"cli.progress.checking": "Prüfe",
"cli.progress.fetching": "Lade",
"cli.progress.loading": "Lade",
"cli.progress.processing": "Verarbeite",
"cli.progress.installing": "Installiere",
"cli.progress.building": "Baue",
"cli.progress.deploying": "Deploye",
"cli.progress.testing": "Teste",
"cli.time.just_now": "gerade eben",
"cli.time.seconds_ago": "vor {{.Count}} Sekunden",
"cli.time.minute_ago": "vor 1 Minute",
"cli.time.minutes_ago": "vor {{.Count}} Minuten",
"cli.time.hour_ago": "vor 1 Stunde",
"cli.time.hours_ago": "vor {{.Count}} Stunden",
"cli.time.day_ago": "vor 1 Tag",
"cli.time.days_ago": "vor {{.Count}} Tagen",
"cli.time.week_ago": "vor 1 Woche",
"cli.time.weeks_ago": "vor {{.Count}} Wochen",
"cli.count.items": {
"one": "{{.Count}} Element",
"other": "{{.Count}} Elemente"
}, },
"cli.count.files": { "noun": {
"one": "{{.Count}} Datei", "file": { "one": "Datei", "other": "Dateien", "gender": "feminine" },
"other": "{{.Count}} Dateien" "repo": { "one": "Repository", "other": "Repositories", "gender": "neuter" },
"commit": { "one": "Commit", "other": "Commits", "gender": "masculine" },
"branch": { "one": "Branch", "other": "Branches", "gender": "masculine" },
"change": { "one": "Änderung", "other": "Änderungen", "gender": "feminine" },
"item": { "one": "Element", "other": "Elemente", "gender": "neuter" }
}, },
"cli.count.repos": { "article": {
"one": "{{.Count}} Repository", "indefinite": { "masculine": "ein", "feminine": "eine", "neuter": "ein" },
"other": "{{.Count}} Repositories" "definite": { "masculine": "der", "feminine": "die", "neuter": "das" }
}, },
"cli.count.commits": { "punct": {
"one": "{{.Count}} Commit", "label": ":",
"other": "{{.Count}} Commits" "progress": "..."
}
}, },
"prompt": {
"cmd.dev.short": "Multi-Repository-Entwicklung", "yes": "j",
"cmd.dev.status.dirty": "geändert", "no": "n",
"cmd.dev.status.clean": "sauber", "continue": "Weiter?",
"cmd.dev.status.ahead": "voraus", "proceed": "Fortfahren?",
"cmd.dev.status.behind": "zurück", "confirm": "Sind Sie sicher?"
"cmd.dev.status.synced": "synchronisiert", },
"time": {
"cmd.dev.push.confirm": "Alle pushen?", "just_now": "gerade eben",
"cmd.dev.commit.committing": "Committe geänderte Repos mit Claude...", "ago": {
"second": { "one": "vor {{.Count}} Sekunde", "other": "vor {{.Count}} Sekunden" },
"cmd.doctor.short": "Entwicklungsumgebung prüfen", "minute": { "one": "vor {{.Count}} Minute", "other": "vor {{.Count}} Minuten" },
"cmd.doctor.checking": "Prüfe Entwicklungsumgebung...", "hour": { "one": "vor {{.Count}} Stunde", "other": "vor {{.Count}} Stunden" },
"cmd.doctor.required": "Erforderlich", "day": { "one": "vor {{.Count}} Tag", "other": "vor {{.Count}} Tagen" },
"cmd.doctor.optional": "Optional", "week": { "one": "vor {{.Count}} Woche", "other": "vor {{.Count}} Wochen" }
"cmd.doctor.ready": "Umgebung bereit", }
},
"error.not_found": "Nicht gefunden: {{.Item}}", "cmd": {
"error.invalid": "Ungültig: {{.Item}}", "dev.short": "Multi-Repository-Entwicklung",
"error.permission": "Zugriff verweigert: {{.Item}}", "doctor.short": "Entwicklungsumgebung prüfen"
"error.timeout": "Zeitüberschreitung", },
"error.gh_not_found": "'gh' CLI nicht gefunden. Installieren von https://cli.github.com/", "error": {
"gh_not_found": "'gh' CLI nicht gefunden. Installieren von https://cli.github.com/"
"label.status": "Status", },
"label.branch": "Branch", "lang": {
"label.commit": "Commit", "de": "Deutsch",
"label.coverage": "Abdeckung", "en": "Englisch",
"label.total": "Gesamt", "es": "Spanisch",
"fr": "Französisch",
"lang.en": "Englisch", "zh": "Chinesisch"
"lang.de": "Deutsch", }
"lang.es": "Spanisch",
"lang.fr": "Französisch",
"lang.zh": "Chinesisch"
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff