1package i18n
2
3import (
4 "fmt"
5 "sort"
6 "strings"
7)
8
9// ValidationResult contains the results of validating translation files.
10type ValidationResult struct {
11 Valid bool
12 Errors []ValidationError
13 Missing map[string][]string // lang -> missing keys
14 Extra map[string][]string // lang -> extra keys
15}
16
17// ValidationError represents a validation issue.
18type ValidationError struct {
19 Language string
20 Key string
21 Message string
22}
23
24// ValidateTranslations validates all translations against a base language.
25// Checks for missing keys, extra keys, and consistency.
26func ValidateTranslations(bundle *Bundle, baseLang string) *ValidationResult {
27 result := &ValidationResult{
28 Valid: true,
29 Errors: []ValidationError{},
30 Missing: make(map[string][]string),
31 Extra: make(map[string][]string),
32 }
33
34 // Get base language messages
35 baseMessages, err := getMessages(bundle, baseLang)
36 if err != nil {
37 result.Valid = false
38 result.Errors = append(result.Errors, ValidationError{
39 Language: baseLang,
40 Message: fmt.Sprintf("Failed to load base language: %v", err),
41 })
42 return result
43 }
44
45 // Get all available languages
46 languages := bundle.AvailableLanguages()
47
48 // Validate each language against base
49 for _, lang := range languages {
50 if lang == baseLang {
51 continue
52 }
53
54 langMessages, err := getMessages(bundle, lang)
55 if err != nil {
56 result.Valid = false
57 result.Errors = append(result.Errors, ValidationError{
58 Language: lang,
59 Message: fmt.Sprintf("Failed to load language: %v", err),
60 })
61 continue
62 }
63
64 // Find missing and extra keys
65 missing, extra := compareKeys(baseMessages, langMessages)
66
67 if len(missing) > 0 {
68 result.Valid = false
69 result.Missing[lang] = missing
70 }
71
72 if len(extra) > 0 {
73 result.Extra[lang] = extra
74 }
75 }
76
77 return result
78}
79
80// getMessages retrieves all message keys for a language.
81func getMessages(bundle *Bundle, lang string) (MessageMap, error) {
82 bundle.mu.RLock()
83 defer bundle.mu.RUnlock()
84
85 messages, ok := bundle.messages[lang]
86 if !ok {
87 return nil, fmt.Errorf("language not found: %s", lang)
88 }
89
90 return messages, nil
91}
92
93// compareKeys compares two message maps and returns missing and extra keys.
94func compareKeys(base, target MessageMap) (missing, extra []string) {
95 // Find missing keys (in base but not in target)
96 for key := range base {
97 if _, ok := target[key]; !ok {
98 missing = append(missing, key)
99 }
100 }
101
102 // Find extra keys (in target but not in base)
103 for key := range target {
104 if _, ok := base[key]; !ok {
105 extra = append(extra, key)
106 }
107 }
108
109 sort.Strings(missing)
110 sort.Strings(extra)
111
112 return missing, extra
113}
114
115// String returns a human-readable validation report.
116func (v *ValidationResult) String() string {
117 if v.Valid {
118 return "✓ All translations are valid"
119 }
120
121 var report strings.Builder
122
123 // Report errors
124 if len(v.Errors) > 0 {
125 report.WriteString("Errors:\n")
126 for _, err := range v.Errors {
127 if err.Key != "" {
128 fmt.Fprintf(&report, " [%s] %s: %s\n", err.Language, err.Key, err.Message)
129 } else {
130 fmt.Fprintf(&report, " [%s] %s\n", err.Language, err.Message)
131 }
132 }
133 report.WriteString("\n")
134 }
135
136 // Report missing keys
137 if len(v.Missing) > 0 {
138 report.WriteString("Missing translations:\n")
139 for lang, keys := range v.Missing {
140 fmt.Fprintf(&report, " [%s] %d missing keys:\n", lang, len(keys))
141 for _, key := range keys {
142 fmt.Fprintf(&report, " - %s\n", key)
143 }
144 }
145 report.WriteString("\n")
146 }
147
148 // Report extra keys
149 if len(v.Extra) > 0 {
150 report.WriteString("Extra translations (not in base):\n")
151 for lang, keys := range v.Extra {
152 fmt.Fprintf(&report, " [%s] %d extra keys:\n", lang, len(keys))
153 for _, key := range keys {
154 fmt.Fprintf(&report, " - %s\n", key)
155 }
156 }
157 }
158
159 return report.String()
160}