cli/pkg/i18n/completeness_test.go
Claude 8a102e4476
feat(i18n): complete en_GB translations and add completeness test
Fill all 568 missing translation keys in en_GB.json (765→1357 lines):
- 97 --help description keys (collect, ci, docs, dev, php, pkg, sdk, vm)
- 471 runtime keys (labels, errors, hints, progress, status messages)
- Add common.flag.follow, common.flag.tag, common.prompt.abort

Add completeness_test.go that scans all T() calls in source code and
verifies every cmd.*/common.* key exists using ModeStrict (panics on
missing). Catches translation gaps at test time instead of showing raw
keys in the CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 20:50:55 +00:00

114 lines
2.8 KiB
Go

package i18n
import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// TestTranslationCompleteness_Good verifies every T() key in the source code
// has a translation in en_GB.json. Catches missing keys at test time instead
// of showing raw keys like "cmd.collect.short" in the CLI.
func TestTranslationCompleteness_Good(t *testing.T) {
svc, err := New(WithMode(ModeStrict))
require.NoError(t, err)
// Find repo root (walk up from pkg/i18n/ to find go.mod)
root := findRepoRoot(t)
// Extract all T("key") calls from Go source
keys := extractTranslationKeys(t, root)
require.NotEmpty(t, keys, "should find translation keys in source code")
var missing []string
for _, key := range keys {
// ModeStrict panics on missing — use recover to collect them all
func() {
defer func() {
if r := recover(); r != nil {
missing = append(missing, key)
}
}()
svc.T(key)
}()
}
if len(missing) > 0 {
sort.Strings(missing)
t.Errorf("found %d missing translation keys in en_GB.json:\n %s",
len(missing), strings.Join(missing, "\n "))
}
}
// findRepoRoot walks up from the test directory to find the repo root (containing go.mod).
func findRepoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
require.NoError(t, err)
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("could not find repo root (no go.mod found)")
}
dir = parent
}
}
// tCallRegex matches i18n.T("key"), T("key"), and cli.T("key") patterns.
var tCallRegex = regexp.MustCompile(`(?:i18n|cli)\.T\("([^"]+)"`)
// extractTranslationKeys scans all .go files (excluding tests and vendors)
// for T() calls and returns the unique set of translation keys.
func extractTranslationKeys(t *testing.T, root string) []string {
t.Helper()
seen := make(map[string]bool)
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil // skip errors
}
// Skip vendor, .git, and test files
if info.IsDir() {
base := info.Name()
if base == "vendor" || base == ".git" || base == "node_modules" {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil
}
matches := tCallRegex.FindAllSubmatch(data, -1)
for _, m := range matches {
key := string(m[1])
// Only track cmd.* and common.* keys (skip dynamic/template keys)
if strings.HasPrefix(key, "cmd.") || strings.HasPrefix(key, "common.") {
seen[key] = true
}
}
return nil
})
require.NoError(t, err)
keys := make([]string, 0, len(seen))
for k := range seen {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}