manage_locales.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import json
  2. import os
  3. import sys
  4. from pathlib import Path
  5. LOCALES_DIR = Path("src/locales")
  6. USER_MASTER = LOCALES_DIR / "translations.user.json"
  7. ADMIN_MASTER = LOCALES_DIR / "translations.admin.json"
  8. LANGUAGES = ["en", "me", "ru", "ua"]
  9. def get_nested_keys(data, prefix=""):
  10. keys = {}
  11. for k, v in data.items():
  12. new_prefix = f"{prefix}.{k}" if prefix else k
  13. if isinstance(v, dict):
  14. if any(lang in v for lang in LANGUAGES):
  15. keys[new_prefix] = v
  16. else:
  17. keys.update(get_nested_keys(v, new_prefix))
  18. else:
  19. keys[new_prefix] = v
  20. return keys
  21. def set_nested_key(data, key_path, value):
  22. parts = key_path.split('.')
  23. for part in parts[:-1]:
  24. data = data.setdefault(part, {})
  25. data[parts[-1]] = value
  26. def deep_merge(dict1, dict2):
  27. """Recursive merge of two dictionaries."""
  28. for key, value in dict2.items():
  29. if key in dict1 and isinstance(dict1[key], dict) and isinstance(value, dict):
  30. # If both are dicts and it's NOT a leaf node with languages
  31. if not any(lang in value for lang in LANGUAGES):
  32. deep_merge(dict1[key], value)
  33. else:
  34. dict1[key] = value
  35. else:
  36. dict1[key] = value
  37. def assemble_master(fragment_dir, master_file, root_key=None):
  38. """Merge all fragments in a directory into one master file"""
  39. if not fragment_dir.exists():
  40. print(f"Warning: {fragment_dir} not found.")
  41. return
  42. combined_data = {}
  43. # Sort files to ensure deterministic merging
  44. for item in sorted(fragment_dir.glob("*.json")):
  45. with open(item, "r", encoding="utf-8") as f:
  46. try:
  47. data = json.load(f)
  48. deep_merge(combined_data, data)
  49. except Exception as e:
  50. print(f"Error reading {item}: {e}")
  51. # Wrap in root key if needed (e.g. "admin": {...})
  52. final_output = combined_data
  53. if root_key:
  54. final_output = {root_key: combined_data}
  55. with open(master_file, "w", encoding="utf-8") as f:
  56. json.dump(final_output, f, ensure_ascii=False, indent=2)
  57. print(f"Assembled fragments from {fragment_dir} into {master_file}")
  58. def _merge_files(master_file, suffix):
  59. """Generic merge from {lang}{suffix} into master_file"""
  60. master_data = {}
  61. all_keys = set()
  62. locale_data = {}
  63. for lang in LANGUAGES:
  64. path = LOCALES_DIR / f"{lang}{suffix}"
  65. if path.exists():
  66. with open(path, "r", encoding="utf-8") as f:
  67. data = json.load(f)
  68. flat = {}
  69. def flatten(d, prefix=""):
  70. for k, v in d.items():
  71. new_prefix = f"{prefix}.{k}" if prefix else k
  72. if isinstance(v, dict):
  73. flatten(v, new_prefix)
  74. else:
  75. flat[new_prefix] = v
  76. flatten(data)
  77. locale_data[lang] = flat
  78. all_keys.update(flat.keys())
  79. for key in sorted(all_keys):
  80. translations = {}
  81. for lang in LANGUAGES:
  82. translations[lang] = locale_data.get(lang, {}).get(key, "")
  83. set_nested_key(master_data, key, translations)
  84. with open(master_file, "w", encoding="utf-8") as f:
  85. json.dump(master_data, f, ensure_ascii=False, indent=2)
  86. print(f"Merged {suffix} files into {master_file}")
  87. def _split_master(master_file, suffix):
  88. """Generic split from master_file into {lang}{suffix}"""
  89. if not master_file.exists():
  90. print(f"Warning: {master_file} not found, skipping split.")
  91. return
  92. with open(master_file, "r", encoding="utf-8") as f:
  93. master_data = json.load(f)
  94. for lang in LANGUAGES:
  95. lang_data = {}
  96. def unflatten(d, target, current_lang):
  97. for k, v in d.items():
  98. if isinstance(v, dict):
  99. if any(l in v for l in LANGUAGES):
  100. target[k] = v.get(current_lang, "")
  101. else:
  102. target[k] = {}
  103. unflatten(v, target[k], current_lang)
  104. else:
  105. target[k] = v
  106. unflatten(master_data, lang_data, lang)
  107. output_path = LOCALES_DIR / f"{lang}{suffix}"
  108. with open(output_path, "w", encoding="utf-8") as f:
  109. json.dump(lang_data, f, ensure_ascii=False, indent=2)
  110. print(f"Generated {output_path}")
  111. def assemble():
  112. """Assemble fragments into masters"""
  113. assemble_master(LOCALES_DIR / "master_user", USER_MASTER)
  114. assemble_master(LOCALES_DIR / "master_admin", ADMIN_MASTER, root_key="admin")
  115. def merge():
  116. """Merge individual JSON files into respective master files"""
  117. _merge_files(USER_MASTER, ".json")
  118. _merge_files(ADMIN_MASTER, ".admin.json")
  119. def split():
  120. """Assemble and then split master files"""
  121. assemble()
  122. _split_master(USER_MASTER, ".json")
  123. _split_master(ADMIN_MASTER, ".admin.json")
  124. def list_missing():
  125. """List all keys with missing translations in both master files"""
  126. for name, path in [("User", USER_MASTER), ("Admin", ADMIN_MASTER)]:
  127. if not path.exists():
  128. continue
  129. print(f"\n--- Missing in {name} ---")
  130. with open(path, "r", encoding="utf-8") as f:
  131. data = json.load(f)
  132. flat_keys = get_nested_keys(data)
  133. missing_found = False
  134. for key, trans in sorted(flat_keys.items()):
  135. if isinstance(trans, dict):
  136. missing = [l for l in LANGUAGES if not trans.get(l)]
  137. if missing:
  138. print(f"Key: {key} - Missing: {', '.join(missing)}")
  139. missing_found = True
  140. if not missing_found:
  141. print("Everything translated!")
  142. if __name__ == "__main__":
  143. if len(sys.argv) < 2:
  144. print("Usage: python manage_locales.py [assemble|merge|split|missing]")
  145. sys.exit(1)
  146. cmd = sys.argv[1].lower()
  147. if cmd == "assemble":
  148. assemble()
  149. elif cmd == "merge":
  150. merge()
  151. elif cmd == "split":
  152. split()
  153. elif cmd == "missing":
  154. list_missing()
  155. else:
  156. print(f"Unknown command: {cmd}")