package main import ( "encoding/json" "os" "path/filepath" "sort" "strings" "gopkg.in/yaml.v3" "github.com/jamesread/golure/pkg/dirs" log "github.com/sirupsen/logrus" ) type LanguageFilev1 struct { SchemaVersion int `json:"schemaVersion"` Translations map[string]string `json:"translations"` } type CombinedTranslationsOutput struct { Comment string `json:"_comment"` Messages map[string]map[string]string `json:"messages"` } func main() { combinedContent := getCombinedLanguageContent() sortedContent := sortTranslations(combinedContent) jsonData, err := json.MarshalIndent(sortedContent, "", " ") if err != nil { log.Fatalf("Error marshalling combined language content: %v", err) } err = os.WriteFile("combined_output.json", jsonData, 0644) if err != nil { log.Fatalf("Error saving combined language content to file: %v", err) } log.Infof("Combined language content saved to combined_output.json") } // sortTranslations creates a new structure with sorted keys for deterministic output. func sortTranslations(input *CombinedTranslationsOutput) *CombinedTranslationsOutput { sorted := &CombinedTranslationsOutput{ Comment: input.Comment, Messages: make(map[string]map[string]string), } // Sort language names langNames := make([]string, 0, len(input.Messages)) for langName := range input.Messages { langNames = append(langNames, langName) } sort.Strings(langNames) // For each language, sort the translation keys for _, langName := range langNames { translations := input.Messages[langName] sortedTranslations := make(map[string]string) keys := make([]string, 0, len(translations)) for key := range translations { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { sortedTranslations[key] = translations[key] } sorted.Messages[langName] = sortedTranslations } return sorted } func getLanguageDir() string { dirsToSearch := []string{ "../lang", "../../../../lang/", // Relative to this file, for unit tests "/app/lang/", } dir, _ := dirs.GetFirstExistingDirectory("lang", dirsToSearch) return dir } func getCombinedLanguageContent() *CombinedTranslationsOutput { output := &CombinedTranslationsOutput{ Comment: "This file is generated. Please re-generate this file using 'make' when you update a translation.", Messages: make(map[string]map[string]string), } languageDir := getLanguageDir() files, err := os.ReadDir(languageDir) if err != nil { log.Errorf("Error reading language directory %s: %v", languageDir, err) return output } for _, file := range filterLanguageFiles(files) { languageName := strings.Replace(file.Name(), ".yaml", "", 1) fullPath := filepath.Join(languageDir, file.Name()) log.Infof("Loading language file: %s", fullPath) content, err := os.ReadFile(fullPath) if err != nil { log.Errorf("Error reading language file %s: %v", fullPath, err) continue } var yamlData LanguageFilev1 err = yaml.Unmarshal(content, &yamlData) if err != nil { log.Errorf("Error reading language file %s: %v", fullPath, err) continue } output.Messages[languageName] = yamlData.Translations } validateTranslations(output) return output } // getReferenceKeys returns the keys from the "en" translation as the reference set. func getReferenceKeys(messages map[string]map[string]string) map[string]bool { enTranslations, exists := messages["en"] if !exists { return nil } referenceKeys := make(map[string]bool, len(enTranslations)) for key := range enTranslations { referenceKeys[key] = true } return referenceKeys } // findMissingKeys returns the keys that are in referenceKeys but not in translations. func findMissingKeys(referenceKeys map[string]bool, translations map[string]string) []string { missing := make([]string, 0) for key := range referenceKeys { if _, exists := translations[key]; !exists { missing = append(missing, key) } } return missing } // findExtraKeys returns the keys that are in translations but not in referenceKeys. func findExtraKeys(referenceKeys map[string]bool, translations map[string]string) []string { extra := make([]string, 0) for key := range translations { if !referenceKeys[key] { extra = append(extra, key) } } return extra } // validateTranslations checks all translations against the "en" reference and prints warnings for missing and extra keys. func validateTranslations(output *CombinedTranslationsOutput) { referenceKeys := getReferenceKeys(output.Messages) if referenceKeys == nil { log.Warnf("No 'en' translation found, skipping validation") return } for langName, translations := range output.Messages { if langName == "en" { continue } missing := findMissingKeys(referenceKeys, translations) if len(missing) > 0 { log.Warnf("Translation '%s' is missing %d key(s): %v", langName, len(missing), missing) } extra := findExtraKeys(referenceKeys, translations) if len(extra) > 0 { log.Warnf("Translation '%s' has %d extra key(s) not in 'en': %v", langName, len(extra), extra) } } } func filterLanguageFiles(files []os.DirEntry) []os.DirEntry { ret := make([]os.DirEntry, 0) for _, file := range files { if file.IsDir() { continue } if !strings.HasSuffix(file.Name(), ".yaml") { continue } ret = append(ret, file) } return ret }