validator.go

  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}