Browse Source

Initial workflow

CPK 5 months ago
parent
commit
8849b33966

+ 168 - 0
.github/workflows/localization/download-languages.py

@@ -0,0 +1,168 @@
+import json
+import os
+import requests
+from collections import OrderedDict
+from datetime import datetime
+
+API_KEY = os.getenv("POEDITOR_API_KEY")
+PROJECT_ID = os.getenv("POEDITOR_PROJECT_ID")
+
+# POEditor API URLs
+EXPORT_API_URL = "https://api.poeditor.com/v2/projects/export"
+LANGUAGES_LIST_URL = "https://api.poeditor.com/v2/languages/list"
+
+# Paths
+LOCALIZATION_DATA_PATH = "src/PixiEditor/Data/Localization/LocalizationData.json"
+LOCALES_DIR = "src/PixiEditor/Data/Localization/Languages/"
+
+def load_ordered_json(file_path):
+    """Load JSON preserving order (as an OrderedDict) and handle UTF-8 BOM."""
+    try:
+        with open(file_path, "r", encoding="utf-8-sig") as f:
+            return json.load(f, object_pairs_hook=OrderedDict)
+    except FileNotFoundError:
+        print(f"::error::File not found: {file_path}")
+        return OrderedDict()
+    except json.JSONDecodeError as e:
+        print(f"::error::Failed to parse JSON in {file_path}: {e}")
+        return OrderedDict()
+
+def write_ordered_json(file_path, data):
+    """Write an OrderedDict to a JSON file in UTF-8 (without BOM)."""
+    with open(file_path, "w", encoding="utf-8") as f:
+        json.dump(data, f, indent=4, ensure_ascii=False)
+
+def fetch_poeditor_language_json(language_code):
+    """
+    Fetch the latest key-value JSON for the given language from POEditor.
+    Returns a dictionary if successful, otherwise None.
+    """
+    if not API_KEY or not PROJECT_ID:
+        print("::error::Missing API_KEY or PROJECT_ID in environment variables.")
+        return None
+
+    response = requests.post(EXPORT_API_URL, data={
+        "api_token": API_KEY,
+        "id": PROJECT_ID,
+        "type": "key_value_json",
+        "language": language_code
+    })
+    if response.status_code == 200:
+        data = response.json()
+        if "result" in data and "url" in data["result"]:
+            download_url = data["result"]["url"]
+            remote_response = requests.get(download_url)
+            if remote_response.status_code == 200:
+                return remote_response.json()
+    print(f"::error::Failed to fetch POEditor data for language '{language_code}'")
+    return None
+
+def update_locale_file(language):
+    """
+    For a given language (dict from LocalizationData.json), update its locale file:
+      - Only keep keys that exist in the POEditor (remote) file.
+      - For keys present both locally and remotely, update with the remote value (preserving the original local order).
+      - Append new keys from POEditor at the bottom.
+    """
+    # Use "remote-code" if available, otherwise default to "code"
+    lang_code = language.get("remote-code", language["code"])
+    if language["code"].lower() == "en":
+        return  # Skip English (do not update)
+
+    file_name = language["localeFileName"]
+    file_path = os.path.join(LOCALES_DIR, file_name)
+    local_data = load_ordered_json(file_path)
+    remote_data = fetch_poeditor_language_json(lang_code)
+    if remote_data is None:
+        print(f"::error::Skipping update for {language['name']} ({lang_code}) due to fetch error.")
+        return
+
+    # Build new ordered data:
+    # 1. Start with keys from local file that exist in remote.
+    updated_data = OrderedDict()
+    for key in local_data:
+        if key in remote_data:
+            updated_data[key] = remote_data[key]
+    # 2. Append keys from remote that are missing locally.
+    for key in remote_data:
+        if key not in updated_data:
+            updated_data[key] = remote_data[key]
+
+    # Write file if changes exist (or if file was missing)
+    if updated_data != local_data:
+        write_ordered_json(file_path, updated_data)
+        print(f"✅ Updated locale file for {language['name']} ({language['code']}).")
+    else:
+        print(f"✅ No changes for {language['name']} ({language['code']}).")
+
+def fetch_languages_list():
+    """
+    Fetch the languages list from POEditor and return a mapping of language codes to
+    updated dates (formatted as "YYYY-MM-DD hh:MM:ss").
+    """
+    if not API_KEY or not PROJECT_ID:
+        print("::error::Missing API_KEY or PROJECT_ID in environment variables.")
+        return {}
+
+    response = requests.post(LANGUAGES_LIST_URL, data={
+        "api_token": API_KEY,
+        "id": PROJECT_ID
+    })
+    languages_updates = {}
+    if response.status_code == 200:
+        data = response.json()
+        if "result" in data and "languages" in data["result"]:
+            for lang in data["result"]["languages"]:
+                code = lang.get("code")
+                updated_iso = lang.get("updated")
+                if code and updated_iso:
+                    try:
+                        # Parse ISO8601 format (example: "2015-05-04T14:21:41+0000")
+                        dt = datetime.strptime(updated_iso, "%Y-%m-%dT%H:%M:%S%z")
+                        formatted = dt.strftime("%Y-%m-%d %H:%M:%S")
+                        languages_updates[code.lower()] = formatted
+                    except Exception as e:
+                        print(f"::error::Failed to parse date for language '{code}': {e}")
+    else:
+        print("::error::Failed to fetch languages list from POEditor.")
+    return languages_updates
+
+def update_localization_data(languages_updates):
+    """
+    Update the lastUpdated field for each language (except English) in LocalizationData.json.
+    """
+    localization_data = load_ordered_json(LOCALIZATION_DATA_PATH)
+    if "Languages" not in localization_data:
+        print("::error::'Languages' key not found in LocalizationData.json")
+        return
+
+    for language in localization_data["Languages"]:
+        code = language.get("code", "").lower()
+        if code == "en":
+            continue  # Do not update English
+        if code in languages_updates:
+            language["lastUpdated"] = languages_updates[code]
+    write_ordered_json(LOCALIZATION_DATA_PATH, localization_data)
+    print("✅ Updated LocalizationData.json with new lastUpdated values.")
+
+def main():
+    # Fetch updated dates for languages from POEditor
+    languages_updates = fetch_languages_list()
+
+    # Load LocalizationData.json and update each language file (except English)
+    localization_data = load_ordered_json(LOCALIZATION_DATA_PATH)
+    if "Languages" not in localization_data:
+        print("::error::'Languages' key not found in LocalizationData.json")
+        return
+
+    for language in localization_data["Languages"]:
+        if language.get("code", "").lower() == "en":
+            continue
+        update_locale_file(language)
+
+    # Update lastUpdated field in LocalizationData.json
+    update_localization_data(languages_updates)
+    print("🎉 All language updates complete.")
+
+if __name__ == "__main__":
+    main()

+ 27 - 0
.github/workflows/localization/localization.yml

@@ -0,0 +1,27 @@
+name: Language Sync
+
+on:
+  workflow_dispatch:
+    inputs:
+      mode:
+        description: "Mode"
+        required: true
+        default: "Full Sync"
+        type: choice
+        options:
+        - Full Sync
+        - Download only
+        - Upload only
+
+jobs:
+  Compare-Upload:
+    runs-on: ubuntu-latest
+    if: ${{ inputs.mode != 'Download only' }}
+    steps:
+      - name: Checkout
+        uses: actions/[email protected]
+      - name: Compare English
+        run: python .github/workflows/localization/upload-comparison.py
+        env:
+          POEDITOR_API_KEY: ${{ secrets.POEDITORKEY }}
+          POEDITOR_PROJECT_ID: ${{ secrets.POEDITORPROJECT }}

+ 92 - 0
.github/workflows/localization/upload-comparison.py

@@ -0,0 +1,92 @@
+import json
+import os
+import requests
+
+API_KEY = os.getenv("POEDITOR_API_KEY")
+PROJECT_ID = os.getenv("POEDITOR_PROJECT_ID")
+
+API_URL = "https://api.poeditor.com/v2/projects/export"
+LANGUAGE = "en"
+LOCAL_FILE_PATH = "src/PixiEditor/Data/Localization/Languages/en.json"
+
+def fetch_poeditor_json():
+    """Fetches the latest en.json from POEditor API (remote data)"""
+    if not API_KEY or not PROJECT_ID:
+        print("::error::Missing API_KEY or PROJECT_ID in environment variables.")
+        return None
+
+    response = requests.post(API_URL, data={
+        "api_token": API_KEY,
+        "id": PROJECT_ID,
+        "type": "key_value_json",
+        "language": LANGUAGE
+    })
+
+    if response.status_code == 200:
+        data = response.json()
+        if "result" in data and "url" in data["result"]:
+            download_url = data["result"]["url"]
+            latest_response = requests.get(download_url)
+            return latest_response.json() if latest_response.status_code == 200 else None
+    return None
+
+def load_local_json():
+    """Loads the local en.json file (authoritative source)"""
+    try:
+        with open(LOCAL_FILE_PATH, "r", encoding="utf-8") as file:
+            return json.load(file)
+    except FileNotFoundError:
+        print("::error::Local en.json file not found!")
+        return {}
+
+def compare_json(local_data, remote_data):
+    """Compares the local and remote JSON data, detecting added, removed, and modified keys"""
+    modifications = []
+    additions = []
+    deletions = []
+    
+    # Check for modified keys (key exists in both, but value changed)
+    for key, local_value in local_data.items():
+        remote_value = remote_data.get(key)
+        if remote_value is not None and local_value != remote_value:
+            modifications.append(f"🔄 {key}: '{remote_value}' → '{local_value}'")
+
+    # Check for added keys (exist in local but missing in POEditor)
+    for key in local_data.keys() - remote_data.keys():
+        additions.append(f"➕ {key}: '{local_data[key]}'")
+
+    # Check for removed keys (exist in POEditor but missing locally)
+    for key in remote_data.keys() - local_data.keys():
+        deletions.append(f"❌ {key}: '{remote_data[key]}'")
+
+    return modifications, additions, deletions
+
+def print_group(title, items):
+    """Prints grouped items using GitHub Actions logging format"""
+    if items:
+        print(f"::group::{len(items)} {title}")
+        for item in items:
+            print(item)
+        print("::endgroup::")
+
+def main():
+    print("");
+
+    remote_json = fetch_poeditor_json()
+    if remote_json is None:
+        print("::error::Failed to fetch POEditor en.json")
+        return
+    
+    local_json = load_local_json()
+    
+    modifications, additions, deletions = compare_json(local_json, remote_json)
+
+    if not (modifications or additions or deletions):
+        print("✅ No changes detected. Local and remote are in sync.")
+    else:
+        print_group("Key(s) Modified", modifications)
+        print_group("Key(s) to be Added", additions)
+        print_group("Key(s) to be Removed", deletions)
+
+if __name__ == "__main__":
+    main()