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>
114 lines
2.8 KiB
Go
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
|
|
}
|