main.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. package main
  2. import (
  3. "encoding/json"
  4. "os"
  5. "path/filepath"
  6. "sort"
  7. "strings"
  8. "gopkg.in/yaml.v3"
  9. "github.com/jamesread/golure/pkg/dirs"
  10. log "github.com/sirupsen/logrus"
  11. )
  12. type LanguageFilev1 struct {
  13. SchemaVersion int `json:"schemaVersion"`
  14. Translations map[string]string `json:"translations"`
  15. }
  16. type CombinedTranslationsOutput struct {
  17. Comment string `json:"_comment"`
  18. Messages map[string]map[string]string `json:"messages"`
  19. }
  20. func main() {
  21. combinedContent := getCombinedLanguageContent()
  22. sortedContent := sortTranslations(combinedContent)
  23. jsonData, err := json.MarshalIndent(sortedContent, "", " ")
  24. if err != nil {
  25. log.Fatalf("Error marshalling combined language content: %v", err)
  26. }
  27. err = os.WriteFile("combined_output.json", jsonData, 0644)
  28. if err != nil {
  29. log.Fatalf("Error saving combined language content to file: %v", err)
  30. }
  31. log.Infof("Combined language content saved to combined_output.json")
  32. }
  33. // sortTranslations creates a new structure with sorted keys for deterministic output.
  34. func sortTranslations(input *CombinedTranslationsOutput) *CombinedTranslationsOutput {
  35. sorted := &CombinedTranslationsOutput{
  36. Comment: input.Comment,
  37. Messages: make(map[string]map[string]string),
  38. }
  39. // Sort language names
  40. langNames := make([]string, 0, len(input.Messages))
  41. for langName := range input.Messages {
  42. langNames = append(langNames, langName)
  43. }
  44. sort.Strings(langNames)
  45. // For each language, sort the translation keys
  46. for _, langName := range langNames {
  47. translations := input.Messages[langName]
  48. sortedTranslations := make(map[string]string)
  49. keys := make([]string, 0, len(translations))
  50. for key := range translations {
  51. keys = append(keys, key)
  52. }
  53. sort.Strings(keys)
  54. for _, key := range keys {
  55. sortedTranslations[key] = translations[key]
  56. }
  57. sorted.Messages[langName] = sortedTranslations
  58. }
  59. return sorted
  60. }
  61. func getLanguageDir() string {
  62. dirsToSearch := []string{
  63. "../lang",
  64. "../../../../lang/", // Relative to this file, for unit tests
  65. "/app/lang/",
  66. }
  67. dir, _ := dirs.GetFirstExistingDirectory("lang", dirsToSearch)
  68. return dir
  69. }
  70. func getCombinedLanguageContent() *CombinedTranslationsOutput {
  71. output := &CombinedTranslationsOutput{
  72. Comment: "This file is generated. Please re-generate this file using 'make' when you update a translation.",
  73. Messages: make(map[string]map[string]string),
  74. }
  75. languageDir := getLanguageDir()
  76. files, err := os.ReadDir(languageDir)
  77. if err != nil {
  78. log.Errorf("Error reading language directory %s: %v", languageDir, err)
  79. return output
  80. }
  81. for _, file := range filterLanguageFiles(files) {
  82. languageName := strings.Replace(file.Name(), ".yaml", "", 1)
  83. fullPath := filepath.Join(languageDir, file.Name())
  84. log.Infof("Loading language file: %s", fullPath)
  85. content, err := os.ReadFile(fullPath)
  86. if err != nil {
  87. log.Errorf("Error reading language file %s: %v", fullPath, err)
  88. continue
  89. }
  90. var yamlData LanguageFilev1
  91. err = yaml.Unmarshal(content, &yamlData)
  92. if err != nil {
  93. log.Errorf("Error reading language file %s: %v", fullPath, err)
  94. continue
  95. }
  96. output.Messages[languageName] = yamlData.Translations
  97. }
  98. validateTranslations(output)
  99. return output
  100. }
  101. // getReferenceKeys returns the keys from the "en" translation as the reference set.
  102. func getReferenceKeys(messages map[string]map[string]string) map[string]bool {
  103. enTranslations, exists := messages["en"]
  104. if !exists {
  105. return nil
  106. }
  107. referenceKeys := make(map[string]bool, len(enTranslations))
  108. for key := range enTranslations {
  109. referenceKeys[key] = true
  110. }
  111. return referenceKeys
  112. }
  113. // findMissingKeys returns the keys that are in referenceKeys but not in translations.
  114. func findMissingKeys(referenceKeys map[string]bool, translations map[string]string) []string {
  115. missing := make([]string, 0)
  116. for key := range referenceKeys {
  117. if _, exists := translations[key]; !exists {
  118. missing = append(missing, key)
  119. }
  120. }
  121. return missing
  122. }
  123. // findExtraKeys returns the keys that are in translations but not in referenceKeys.
  124. func findExtraKeys(referenceKeys map[string]bool, translations map[string]string) []string {
  125. extra := make([]string, 0)
  126. for key := range translations {
  127. if !referenceKeys[key] {
  128. extra = append(extra, key)
  129. }
  130. }
  131. return extra
  132. }
  133. // validateTranslations checks all translations against the "en" reference and prints warnings for missing and extra keys.
  134. func validateTranslations(output *CombinedTranslationsOutput) {
  135. referenceKeys := getReferenceKeys(output.Messages)
  136. if referenceKeys == nil {
  137. log.Warnf("No 'en' translation found, skipping validation")
  138. return
  139. }
  140. for langName, translations := range output.Messages {
  141. if langName == "en" {
  142. continue
  143. }
  144. missing := findMissingKeys(referenceKeys, translations)
  145. if len(missing) > 0 {
  146. log.Warnf("Translation '%s' is missing %d key(s): %v", langName, len(missing), missing)
  147. }
  148. extra := findExtraKeys(referenceKeys, translations)
  149. if len(extra) > 0 {
  150. log.Warnf("Translation '%s' has %d extra key(s) not in 'en': %v", langName, len(extra), extra)
  151. }
  152. }
  153. }
  154. func filterLanguageFiles(files []os.DirEntry) []os.DirEntry {
  155. ret := make([]os.DirEntry, 0)
  156. for _, file := range files {
  157. if file.IsDir() {
  158. continue
  159. }
  160. if !strings.HasSuffix(file.Name(), ".yaml") {
  161. continue
  162. }
  163. ret = append(ret, file)
  164. }
  165. return ret
  166. }