download-languages.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import json
  2. import os
  3. import requests
  4. from collections import OrderedDict
  5. from datetime import datetime
  6. API_KEY = os.getenv("POEDITOR_API_KEY")
  7. PROJECT_ID = os.getenv("POEDITOR_PROJECT_ID")
  8. GITHUB_OUTPUT = os.getenv('GITHUB_OUTPUT')
  9. # POEditor API URLs
  10. EXPORT_API_URL = "https://api.poeditor.com/v2/projects/export"
  11. LANGUAGES_LIST_URL = "https://api.poeditor.com/v2/languages/list"
  12. # Paths
  13. LOCALIZATION_DATA_PATH = "src/PixiEditor/Data/Localization/LocalizationData.json"
  14. LOCALES_DIR = "src/PixiEditor/Data/Localization/Languages/"
  15. def load_ordered_json(file_path):
  16. """Load JSON preserving order (as an OrderedDict) and handle UTF-8 BOM."""
  17. try:
  18. with open(file_path, "r", encoding="utf-8-sig") as f:
  19. return json.load(f, object_pairs_hook=OrderedDict)
  20. except FileNotFoundError:
  21. print(f"::error::File not found: {file_path}")
  22. return OrderedDict()
  23. except json.JSONDecodeError as e:
  24. print(f"::error::Failed to parse JSON in {file_path}: {e}")
  25. return OrderedDict()
  26. def write_ordered_json(file_path, data):
  27. """Write an OrderedDict to a JSON file in UTF-8 (without BOM)."""
  28. with open(file_path, "w", encoding="utf-8") as f:
  29. json.dump(data, f, indent=2, ensure_ascii=False)
  30. def fetch_poeditor_language_json(language_code):
  31. """
  32. Fetch the latest key-value JSON for the given language from POEditor.
  33. Returns a dictionary if successful, otherwise None.
  34. """
  35. if not API_KEY or not PROJECT_ID:
  36. print("::error::Missing API_KEY or PROJECT_ID in environment variables.")
  37. return None
  38. response = requests.post(EXPORT_API_URL, data={
  39. "api_token": API_KEY,
  40. "id": PROJECT_ID,
  41. "type": "key_value_json",
  42. "language": language_code
  43. })
  44. if response.status_code == 200:
  45. data = response.json()
  46. if "result" in data and "url" in data["result"]:
  47. download_url = data["result"]["url"]
  48. remote_response = requests.get(download_url)
  49. if remote_response.status_code == 200:
  50. return remote_response.json()
  51. print(f"::error::Failed to fetch POEditor data for language '{language_code}'")
  52. return None
  53. def update_locale_file(language):
  54. """
  55. For a given language (dict from LocalizationData.json), update its locale file:
  56. - Only keep keys that exist in the POEditor (remote) file.
  57. - For keys present both locally and remotely, update with the remote value (preserving the original local order).
  58. - Append new keys from POEditor at the bottom.
  59. """
  60. # Use "remote-code" if available, otherwise default to "code"
  61. lang_code = language.get("remoteCode", language["code"])
  62. if language["code"].lower() == "en":
  63. return # Skip English (do not update)
  64. file_name = language["localeFileName"]
  65. file_path = os.path.join(LOCALES_DIR, file_name)
  66. local_data = load_ordered_json(file_path)
  67. remote_data = fetch_poeditor_language_json(lang_code)
  68. if remote_data is None:
  69. print(f"::error::Skipping update for {language['name']} ({lang_code}) due to fetch error.")
  70. return
  71. # Build new ordered data:
  72. # 1. Start with keys from local file that exist in remote.
  73. updated_data = OrderedDict()
  74. for key in local_data:
  75. if key in remote_data:
  76. updated_data[key] = remote_data[key]
  77. # 2. Append keys from remote that are missing locally.
  78. for key in remote_data:
  79. if key not in updated_data:
  80. updated_data[key] = remote_data[key]
  81. # Write file if changes exist (or if file was missing)
  82. if updated_data != local_data:
  83. write_ordered_json(file_path, updated_data)
  84. print(f"✅ Updated locale file for {language['name']} ({language['code']}).")
  85. return False
  86. else:
  87. print(f"✅ No changes for {language['name']} ({language['code']}).")
  88. return False
  89. def fetch_languages_list():
  90. """
  91. Fetch the languages list from POEditor and return a mapping of language codes to
  92. updated dates (formatted as "YYYY-MM-DD hh:MM:ss").
  93. """
  94. if not API_KEY or not PROJECT_ID:
  95. print("::error::Missing API_KEY or PROJECT_ID in environment variables.")
  96. return {}
  97. response = requests.post(LANGUAGES_LIST_URL, data={
  98. "api_token": API_KEY,
  99. "id": PROJECT_ID
  100. })
  101. languages_updates = {}
  102. if response.status_code == 200:
  103. data = response.json()
  104. if "result" in data and "languages" in data["result"]:
  105. for lang in data["result"]["languages"]:
  106. code = lang.get("code")
  107. updated_iso = lang.get("updated")
  108. if code and updated_iso:
  109. try:
  110. # Parse ISO8601 format (example: "2015-05-04T14:21:41+0000")
  111. dt = datetime.strptime(updated_iso, "%Y-%m-%dT%H:%M:%S%z")
  112. formatted = dt.strftime("%Y-%m-%d %H:%M:%S")
  113. languages_updates[code.lower()] = formatted
  114. except Exception as e:
  115. print(f"::error::Failed to parse date for language '{code}': {e}")
  116. else:
  117. print("::error::Failed to fetch languages list from POEditor.")
  118. return languages_updates
  119. def update_localization_data(languages_updates):
  120. """
  121. Update the lastUpdated field for each language (except English) in LocalizationData.json.
  122. """
  123. localization_data = load_ordered_json(LOCALIZATION_DATA_PATH)
  124. if "Languages" not in localization_data:
  125. print("::error::'Languages' key not found in LocalizationData.json")
  126. return
  127. for language in localization_data["Languages"]:
  128. code = language.get("code", "").lower()
  129. if code == "en":
  130. continue # Do not update English
  131. if code in languages_updates:
  132. language["lastUpdated"] = languages_updates[code]
  133. write_ordered_json(LOCALIZATION_DATA_PATH, localization_data)
  134. print("✅ Updated LocalizationData.json with new lastUpdated values.")
  135. def main():
  136. # Fetch updated dates for languages from POEditor
  137. languages_updates = fetch_languages_list()
  138. # Load LocalizationData.json and update each language file (except English)
  139. localization_data = load_ordered_json(LOCALIZATION_DATA_PATH)
  140. if "Languages" not in localization_data:
  141. print("::error::'Languages' key not found in LocalizationData.json")
  142. exit(1)
  143. return
  144. has_changes = False
  145. for language in localization_data["Languages"]:
  146. if language.get("code", "").lower() == "en":
  147. continue
  148. if update_locale_file(language):
  149. has_changes = True
  150. with open(GITHUB_OUTPUT, "a") as f:
  151. f.write(f"HAS_CHANGES={str(has_changes).lower()}")
  152. # Update lastUpdated field in LocalizationData.json
  153. update_localization_data(languages_updates)
  154. print("🎉 All language updates complete.")
  155. if __name__ == "__main__":
  156. main()