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