validator.go

  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}