download-languages.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. "filters": "translated"
  44. })
  45. if response.status_code == 200:
  46. data = response.json()
  47. if "result" in data and "url" in data["result"]:
  48. download_url = data["result"]["url"]
  49. remote_response = requests.get(download_url)
  50. if remote_response.status_code == 200:
  51. return remote_response.json()
  52. print(f"::error::Failed to fetch POEditor data for language '{language_code}'")
  53. return None
  54. def update_locale_file(language):
  55. """
  56. For a given language (dict from LocalizationData.json), update its locale file:
  57. - Only keep keys that exist in the POEditor (remote) file.
  58. - For keys present both locally and remotely, update with the remote value (preserving the original local order).
  59. - Append new keys from POEditor at the bottom.
  60. """
  61. # Use "remote-code" if available, otherwise default to "code"
  62. lang_code = language.get("remoteCode", language["code"])
  63. if language["code"].lower() == "en":
  64. return False # Skip English (do not update)
  65. file_name = language["localeFileName"]
  66. file_path = os.path.join(LOCALES_DIR, file_name)
  67. local_data = load_ordered_json(file_path)
  68. remote_data = fetch_poeditor_language_json(lang_code)
  69. if remote_data is None:
  70. print(f"::error::Skipping update for {language['name']} ({lang_code}) due to fetch error.")
  71. return False
  72. # Build new ordered data:
  73. # 1. Start with keys from local file that exist in remote.
  74. updated_data = OrderedDict()
  75. for key in local_data:
  76. if key in remote_data:
  77. updated_data[key] = remote_data[key]
  78. # 2. Append keys from remote that are missing locally.
  79. for key in remote_data:
  80. if key not in updated_data:
  81. updated_data[key] = remote_data[key]
  82. # Write file if changes exist (or if file was missing)
  83. if updated_data != local_data:
  84. write_ordered_json(file_path, updated_data)
  85. print(f"✅ Updated locale file for {language['name']} ({language['code']}).")
  86. return True
  87. else:
  88. print(f"✅ No changes for {language['name']} ({language['code']}).")
  89. return False
  90. def fetch_languages_list():
  91. """
  92. Fetch the languages list from POEditor and return a mapping of language codes to
  93. updated dates (formatted as "YYYY-MM-DD hh:MM:ss").
  94. """
  95. if not API_KEY or not PROJECT_ID:
  96. print("::error::Missing API_KEY or PROJECT_ID in environment variables.")
  97. return {}
  98. response = requests.post(LANGUAGES_LIST_URL, data={
  99. "api_token": API_KEY,
  100. "id": PROJECT_ID
  101. })
  102. languages_updates = {}
  103. if response.status_code == 200:
  104. data = response.json()
  105. if "result" in data and "languages" in data["result"]:
  106. for lang in data["result"]["languages"]:
  107. code = lang.get("code")
  108. updated_iso = lang.get("updated")
  109. if code and updated_iso:
  110. try:
  111. # Parse ISO8601 format (example: "2015-05-04T14:21:41+0000")
  112. dt = datetime.strptime(updated_iso, "%Y-%m-%dT%H:%M:%S%z")
  113. formatted = dt.strftime("%Y-%m-%d %H:%M:%S")
  114. languages_updates[code.lower()] = formatted
  115. except Exception as e:
  116. print(f"::error::Failed to parse date for language '{code}': {e}")
  117. else:
  118. print("::error::Failed to fetch languages list from POEditor.")
  119. return languages_updates
  120. def update_localization_data(languages_updates):
  121. """
  122. Update the lastUpdated field for each language (except English) in LocalizationData.json.
  123. """
  124. localization_data = load_ordered_json(LOCALIZATION_DATA_PATH)
  125. if "Languages" not in localization_data:
  126. print("::error::'Languages' key not found in LocalizationData.json")
  127. return
  128. for language in localization_data["Languages"]:
  129. code = language.get("code", "").lower()
  130. if code == "en":
  131. continue # Do not update English
  132. if code in languages_updates:
  133. language["lastUpdated"] = languages_updates[code]
  134. write_ordered_json(LOCALIZATION_DATA_PATH, localization_data)
  135. print("✅ Updated LocalizationData.json with new lastUpdated values.")
  136. def main():
  137. # Fetch updated dates for languages from POEditor
  138. languages_updates = fetch_languages_list()
  139. # Load LocalizationData.json and update each language file (except English)
  140. localization_data = load_ordered_json(LOCALIZATION_DATA_PATH)
  141. if "Languages" not in localization_data:
  142. print("::error::'Languages' key not found in LocalizationData.json")
  143. exit(1)
  144. return
  145. has_changes = False
  146. for language in localization_data["Languages"]:
  147. if language.get("code", "").lower() == "en":
  148. continue
  149. if update_locale_file(language):
  150. has_changes = True
  151. with open(GITHUB_OUTPUT, "a") as f:
  152. f.write(f"HAS_CHANGES={str(has_changes).lower()}")
  153. # Update lastUpdated field in LocalizationData.json
  154. update_localization_data(languages_updates)
  155. print("🎉 All language updates complete.")
  156. if __name__ == "__main__":
  157. main()