main.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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. err := writeTranslationFiles(sortedContent.Messages)
  24. if err != nil {
  25. log.Fatalf("Error writing language content: %v", err)
  26. }
  27. }
  28. func writeTranslationFiles(messages map[string]map[string]string) error {
  29. err := os.RemoveAll("generated")
  30. if err != nil {
  31. return err
  32. }
  33. err = os.Mkdir("generated", 0o755)
  34. if err != nil {
  35. return err
  36. }
  37. for languageName, translations := range messages {
  38. err = writeTranslationFile(languageName, translations)
  39. if err != nil {
  40. return err
  41. }
  42. }
  43. log.Infof("Language content saved to generated/")
  44. return nil
  45. }
  46. func writeTranslationFile(languageName string, translations map[string]string) error {
  47. jsonData, err := json.MarshalIndent(translations, "", " ")
  48. if err != nil {
  49. return err
  50. }
  51. jsonData = append(jsonData, '\n')
  52. filename := filepath.Join("generated", languageName+".json")
  53. return os.WriteFile(filename, jsonData, 0o644)
  54. }
  55. // sortTranslations creates a new structure with sorted keys for deterministic output.
  56. func sortTranslations(input *CombinedTranslationsOutput) *CombinedTranslationsOutput {
  57. sorted := &CombinedTranslationsOutput{
  58. Comment: input.Comment,
  59. Messages: make(map[string]map[string]string),
  60. }
  61. // Sort language names
  62. langNames := make([]string, 0, len(input.Messages))
  63. for langName := range input.Messages {
  64. langNames = append(langNames, langName)
  65. }
  66. sort.Strings(langNames)
  67. // For each language, sort the translation keys
  68. for _, langName := range langNames {
  69. translations := input.Messages[langName]
  70. sortedTranslations := make(map[string]string)
  71. keys := make([]string, 0, len(translations))
  72. for key := range translations {
  73. keys = append(keys, key)
  74. }
  75. sort.Strings(keys)
  76. for _, key := range keys {
  77. sortedTranslations[key] = translations[key]
  78. }
  79. sorted.Messages[langName] = sortedTranslations
  80. }
  81. return sorted
  82. }
  83. func getLanguageDir() string {
  84. dirsToSearch := []string{
  85. "../lang",
  86. "../../../../lang/", // Relative to this file, for unit tests
  87. "/app/lang/",
  88. }
  89. dir, _ := dirs.GetFirstExistingDirectory("lang", dirsToSearch)
  90. return dir
  91. }
  92. func getCombinedLanguageContent() *CombinedTranslationsOutput {
  93. output := &CombinedTranslationsOutput{
  94. Comment: "This file is generated. Please re-generate this file using 'make' when you update a translation.",
  95. Messages: make(map[string]map[string]string),
  96. }
  97. languageDir := getLanguageDir()
  98. files, err := os.ReadDir(languageDir)
  99. if err != nil {
  100. log.Errorf("Error reading language directory %s: %v", languageDir, err)
  101. return output
  102. }
  103. for _, file := range filterLanguageFiles(files) {
  104. languageName := strings.Replace(file.Name(), ".yaml", "", 1)
  105. fullPath := filepath.Join(languageDir, file.Name())
  106. log.Infof("Loading language file: %s", fullPath)
  107. content, err := os.ReadFile(fullPath)
  108. if err != nil {
  109. log.Errorf("Error reading language file %s: %v", fullPath, err)
  110. continue
  111. }
  112. var yamlData LanguageFilev1
  113. err = yaml.Unmarshal(content, &yamlData)
  114. if err != nil {
  115. log.Errorf("Error reading language file %s: %v", fullPath, err)
  116. continue
  117. }
  118. output.Messages[languageName] = yamlData.Translations
  119. }
  120. validateTranslations(output)
  121. return output
  122. }
  123. // getReferenceKeys returns the keys from the "en" translation as the reference set.
  124. func getReferenceKeys(messages map[string]map[string]string) map[string]bool {
  125. enTranslations, exists := messages["en"]
  126. if !exists {
  127. return nil
  128. }
  129. referenceKeys := make(map[string]bool, len(enTranslations))
  130. for key := range enTranslations {
  131. referenceKeys[key] = true
  132. }
  133. return referenceKeys
  134. }
  135. // findMissingKeys returns the keys that are in referenceKeys but not in translations.
  136. func findMissingKeys(referenceKeys map[string]bool, translations map[string]string) []string {
  137. missing := make([]string, 0)
  138. for key := range referenceKeys {
  139. if _, exists := translations[key]; !exists {
  140. missing = append(missing, key)
  141. }
  142. }
  143. return missing
  144. }
  145. // findExtraKeys returns the keys that are in translations but not in referenceKeys.
  146. func findExtraKeys(referenceKeys map[string]bool, translations map[string]string) []string {
  147. extra := make([]string, 0)
  148. for key := range translations {
  149. if !referenceKeys[key] {
  150. extra = append(extra, key)
  151. }
  152. }
  153. return extra
  154. }
  155. // validateTranslations checks all translations against the "en" reference and prints warnings for missing and extra keys.
  156. func validateTranslations(output *CombinedTranslationsOutput) {
  157. referenceKeys := getReferenceKeys(output.Messages)
  158. if referenceKeys == nil {
  159. log.Warnf("No 'en' translation found, skipping validation")
  160. return
  161. }
  162. for langName, translations := range output.Messages {
  163. if langName == "en" {
  164. continue
  165. }
  166. missing := findMissingKeys(referenceKeys, translations)
  167. if len(missing) > 0 {
  168. log.Warnf("Translation '%s' is missing %d key(s): %v", langName, len(missing), missing)
  169. }
  170. extra := findExtraKeys(referenceKeys, translations)
  171. if len(extra) > 0 {
  172. log.Warnf("Translation '%s' has %d extra key(s) not in 'en': %v", langName, len(extra), extra)
  173. }
  174. }
  175. }
  176. func filterLanguageFiles(files []os.DirEntry) []os.DirEntry {
  177. ret := make([]os.DirEntry, 0)
  178. for _, file := range files {
  179. if file.IsDir() {
  180. continue
  181. }
  182. if !strings.HasSuffix(file.Name(), ".yaml") {
  183. continue
  184. }
  185. ret = append(ret, file)
  186. }
  187. return ret
  188. }