diff --git a/.github/workflows/check_for_crowdin_updates.yml b/.github/workflows/check_for_crowdin_updates.yml deleted file mode 100644 index 80d6e81..0000000 --- a/.github/workflows/check_for_crowdin_updates.yml +++ /dev/null @@ -1,388 +0,0 @@ -name: Check for Crowdin Updates - -# Not sure why yet, but uploading artefacts after creating the pull requests -# seems to only include a part of what should be in. -# As a dirty fix, we upload the artefacts first, and then make the pull request - -on: - schedule: - - cron: '0 0 * * 1' # Every Monday at 12:00 AM UTC, which is 10:00 AM Melbourne time (AEST) - workflow_dispatch: - inputs: - UPDATE_PULL_REQUESTS: - description: 'Create/update PRs for all platforms' - required: true - type: boolean - default: true - SKIP_VALIDATION_ERRORS: - description: 'Continue even if string validation fails' - required: false - type: boolean - default: false - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - PR_TITLE: "[Automated] Update translations from Crowdin" - PR_DESCRIPTION: | - [Automated] - This PR includes the latest translations from Crowdin - - Session uses the community-driven translation platform Crowdin for localization, anyone can contribute at https://getsession.org/translate - -jobs: - fetch_translations: - name: Download translations from crowdin - runs-on: ubuntu-latest - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - # don't provide a branch (ref) so it uses the default for that event - - name: Debug inputs - run: | - echo "UPDATE_PULL_REQUESTS: '${{ inputs.UPDATE_PULL_REQUESTS }}'" - echo "Event name: ${{ github.event_name }}" - echo "All inputs: ${{ toJSON(inputs) }}" - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.12 - cache: 'pip' # caching pip dependencies - - name: Install Dependencies - shell: bash - run: | - pip install -r ${{ github.workspace }}/scripts/crowdin/requirements.txt - - name: Download Translations - env: - CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }} - run: | - python "${{ github.workspace }}/scripts/crowdin/download_translations_from_crowdin.py" \ - "$CROWDIN_API_TOKEN" \ - 618696 \ - "${{ github.workspace }}/raw_translations" \ - --glossary_id 407522 \ - --concept_id 36 \ - --skip-untranslated-strings - - name: Upload download artefacts - uses: actions/upload-artifact@v7 - with: - name: session-download - path: | - ${{ github.workspace }}/raw_translations/*.xliff - ${{ github.workspace }}/raw_translations/_non_translatable_strings.json - ${{ github.workspace }}/raw_translations/_project_info.json - overwrite: true - if-no-files-found: warn - retention-days: 7 - - parse_translations: - name: Parse and validate translations - runs-on: ubuntu-latest - needs: [fetch_translations] - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.12 - cache: 'pip' - - - name: Install Dependencies - shell: bash - run: | - pip install -r ${{ github.workspace }}/scripts/crowdin/requirements.txt - - - name: Download raw translations - uses: actions/download-artifact@v8 - with: - name: session-download - path: "${{ github.workspace }}/raw_translations" - - - name: Parse and validate XLIFF files - run: | - python "${{ github.workspace }}/scripts/crowdin/parse_xliff.py" \ - "${{ github.workspace }}/raw_translations" \ - "${{ github.workspace }}/parsed_translations.json" \ - ${{ inputs.SKIP_VALIDATION_ERRORS != true && '--error-on-validation-failure' || '' }} \ - --validation-report "${{ github.workspace }}/validation_report.json" - - - name: Upload parsed translations - uses: actions/upload-artifact@v7 - with: - name: session-parsed - path: | - ${{ github.workspace }}/parsed_translations.json - ${{ github.workspace }}/validation_report.json - overwrite: true - if-no-files-found: error - retention-days: 7 - - build_ios: - name: Build iOS strings - runs-on: ubuntu-latest - needs: [parse_translations] - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - # don't provide a branch (ref) so it uses the default for that event - - name: Checkout iOS - uses: ./scripts/actions/checkout_ios - - - name: Setup shared - uses: ./scripts/actions/setup_shared - - - name: Download parsed translations - uses: actions/download-artifact@v8 - with: - name: session-parsed - path: "${{ github.workspace }}" - - - name: Prepare iOS Strings - run: | - python "${{ github.workspace }}/scripts/crowdin/generate_ios_strings.py" \ - "${{ github.workspace }}/parsed_translations.json" \ - "${{ github.workspace }}/ios/Session/Meta/Translations" \ - "${{ github.workspace }}/ios/SessionUIKit/Style Guide/Constants.swift" - - name: Upload iOS artefacts - uses: actions/upload-artifact@v7 - with: - name: session-ios - path: | - ${{ github.workspace }}/ios/Session/Meta/Translations/Localizable.xcstrings - ${{ github.workspace }}/ios/SessionUIKit/Style Guide/Constants.swift - overwrite: true - if-no-files-found: warn - retention-days: 7 - - build_localization_module: - name: Build localization module strings - needs: [parse_translations] - runs-on: ubuntu-latest - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - # don't provide a branch (ref) so it uses the default for that event - - name: Setup shared - uses: ./scripts/actions/setup_shared - - - name: Download parsed translations - uses: actions/download-artifact@v8 - with: - name: session-parsed - path: "${{ github.workspace }}" - - - name: Generate TypeScript - run: | - python "${{ github.workspace }}/scripts/crowdin/codegen_localization.py" \ - "${{ github.workspace }}/parsed_translations.json" \ - "${{ github.workspace }}/output/generated" - - - name: Generate the language list - run: | - python "${{ github.workspace }}/scripts/crowdin/generate_language_list.py" \ - "${{ github.workspace }}/parsed_translations.json" \ - "${{ github.workspace }}/output/generated" - - - name: Upload artifacts - uses: actions/upload-artifact@v7 - with: - name: session-localization - path: | - ${{ github.workspace }}/output/generated/locales.ts - ${{ github.workspace }}/output/generated/english.ts - ${{ github.workspace }}/output/generated/translations.ts - ${{ github.workspace }}/output/generated/constants.ts - ${{ github.workspace }}/output/generated/languages.ts - overwrite: true - if-no-files-found: warn - retention-days: 7 - - build_android: - name: Build Android strings - runs-on: ubuntu-latest - needs: [parse_translations] - - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - # don't provide a branch (ref) so it uses the default for that event - - name: Checkout Android - uses: ./scripts/actions/checkout_android - - - name: Setup shared - uses: ./scripts/actions/setup_shared - - - name: Setup Java - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: 17 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - with: - gradle-version: wrapper - cache-read-only: false - - - name: Download parsed translations - uses: actions/download-artifact@v8 - with: - name: session-parsed - path: "${{ github.workspace }}" - - - name: Prepare Android Strings - run: | - rm -rf ${{ github.workspace }}/android/app/src/main/res/values*/strings.xml - python "${{ github.workspace }}/scripts/crowdin/generate_android_strings.py" \ - "${{ github.workspace }}/parsed_translations.json" \ - "${{ github.workspace }}/android/app/src/main/res" \ - "${{ github.workspace }}/android/app/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt" - - name: Upload Android artefacts - uses: actions/upload-artifact@v7 - with: - name: session-android - path: | - ${{ github.workspace }}/android/app/src/main/res/values*/strings.xml - ${{ github.workspace }}/android/app/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt - overwrite: true - if-no-files-found: warn - retention-days: 7 - - - name: Validate strings for Android - run: | - cd ${{ github.workspace }}/android - ./gradlew app:generatePlayDebugResources \ - --parallel \ - --build-cache \ - --configuration-cache \ - -Dorg.gradle.jvmargs="-Xmx2g -XX:+UseParallelGC" \ - -Dorg.gradle.caching=true \ - -x lint \ - -x test - - jobs_sync: - name: Waiting for build jobs - - needs: [build_android, build_ios, build_localization_module] - runs-on: ubuntu-latest - - steps: - - name: Nothing to do - shell: bash - run: echo "Nothing to do here" - - make_android_pr: - env: - PR_TARGET_BRANCH: feature/update-crowdin-translations - - name: Make Android PR - needs: [jobs_sync] - runs-on: ubuntu-latest - if: ${{ github.event_name == 'schedule' || inputs.UPDATE_PULL_REQUESTS == true }} - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - # don't provide a branch (ref) so it uses the default for that event - - name: Checkout Android - uses: ./scripts/actions/checkout_android - - - uses: actions/download-artifact@v8 - with: - name: session-android - # this has to be the first shared parent on the upload artefact task for Android - path: "${{ github.workspace }}/android/app/src/main" - - - name: Create Android Pull Request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - path: 'android' - token: ${{ secrets.CROWDIN_PR_TOKEN }} - title: ${{ env.PR_TITLE }} - body: ${{ env.PR_DESCRIPTION }} - branch: ${{ env.PR_TARGET_BRANCH }} - commit-message: ${{ env.PR_TITLE }} - delete-branch: true - - make_localization_module_pr: - env: - PR_TARGET_BRANCH: main - needs: [jobs_sync] - name: Make Localization Module PR - runs-on: ubuntu-latest - if: ${{ github.event_name == 'schedule' || inputs.UPDATE_PULL_REQUESTS == true }} - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - # don't provide a branch (ref) so it uses the default for that event - - name: Checkout Localization Module - uses: ./scripts/actions/checkout_localization_module - with: - token: ${{ secrets.CROWDIN_PR_TOKEN }} - - - uses: actions/download-artifact@v8 - with: - name: session-localization - # Download to /generated folder in localization module repo - path: "${{ github.workspace }}/module/generated" - - - name: Push directly to ${{ env.PR_TARGET_BRANCH }} - uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0 - with: - repository: module - commit_message: ${{ env.PR_TITLE }} - branch: ${{ env.PR_TARGET_BRANCH }} - commit_author: "github-actions[bot] " - commit_user_name: github-actions[bot] - commit_user_email: github-actions[bot]@users.noreply.github.com - - make_ios_pr: - env: - PR_TARGET_BRANCH: feature/update-crowdin-translations - needs: [jobs_sync] - name: Make iOS PR - runs-on: ubuntu-latest - if: ${{ github.event_name == 'schedule' || inputs.UPDATE_PULL_REQUESTS == true }} - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - # don't provide a branch (ref) so it uses the default for that event - - name: Checkout iOS - uses: ./scripts/actions/checkout_ios - - - uses: actions/download-artifact@v8 - with: - name: session-ios - # this has to be the first shared parent on the upload artefact task for iOS - path: "${{ github.workspace }}/ios" - - - name: Create iOS Pull Request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - path: "ios" - token: ${{ secrets.CROWDIN_PR_TOKEN }} - title: ${{ env.PR_TITLE }} - body: ${{ env.PR_DESCRIPTION }} - branch: ${{ env.PR_TARGET_BRANCH }} - commit-message: ${{ env.PR_TITLE }} - delete-branch: true diff --git a/.github/workflows/crowdin_multiple_translations_report.yml b/.github/workflows/crowdin_multiple_translations_report.yml deleted file mode 100644 index 1ac6fe1..0000000 --- a/.github/workflows/crowdin_multiple_translations_report.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Crowdin Multiple Translations Report - -# Reports Crowdin string slots (per plural form) that have more than one -# translation, so a human can pick the keeper and delete the rest. -# -# Detecting per-plural-form duplicates needs one API request per (string, locale) -# and Crowdin rate-limits at ~40 req/s, so scanning all ~80 locales at once takes -# ~40 min. To stay under a ~10 min daily budget the script ROTATES: each run scans -# one shard of the locales (default 1/8, ~10 locales, ~8 min) and the shard is -# picked from the date, so every locale is covered over an 8-day cycle. - -on: - schedule: - # Every day at 06:00 UTC (rotates through one locale shard per run) - - cron: "0 6 * * *" - workflow_dispatch: - inputs: - locales: - description: "Scan exactly these locales (space-separated; disables rotation)" - required: false - all_locales: - description: "Scan every target locale in one run (~40 min)" - required: false - type: boolean - default: false - shards: - description: "Number of daily rotation shards (default 8)" - required: false - default: "8" - dry_run: - description: "Scan and print the payload instead of posting to Discord" - required: false - type: boolean - default: false - -permissions: - contents: read - -concurrency: - group: crowdin-multiple-translations - cancel-in-progress: false - -jobs: - report: - runs-on: ubuntu-latest - timeout-minutes: 90 - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - cache: "pip" - - - name: Install dependencies - run: pip install -r crowdin/requirements.txt - - - name: Report strings with multiple translations - env: - CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }} - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - # Free-text dispatch inputs go through env (never inlined into the shell) - # so a value can't be interpreted as shell syntax. - SHARDS: ${{ github.event.inputs.shards || '8' }} - LOCALES: ${{ github.event.inputs.locales }} - run: | - args=(--shards "$SHARDS") - if [ -n "$LOCALES" ]; then - # Whitespace-split into separate --locales values without glob/metachar - # interpretation (env vars are not re-scanned for $(...) either). - read -ra locale_args <<< "$LOCALES" - args+=(--locales "${locale_args[@]}") - fi - python crowdin/report_multiple_translations.py \ - "${args[@]}" \ - ${{ github.event.inputs.all_locales == 'true' && '--all-locales' || '' }} \ - ${{ github.event.inputs.dry_run == 'true' && '--dry-run' || '' }} diff --git a/.github/workflows/fetch-release-stats.yml b/.github/workflows/fetch-release-stats.yml deleted file mode 100644 index 626f780..0000000 --- a/.github/workflows/fetch-release-stats.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Fetch Release Stats - -on: - workflow_dispatch: - -jobs: - fetch-stats: - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: "20" - - - name: Install dependencies - run: | - cd release_stats && npm install -D typescript @types/node tsx - - - name: Run stats script - run: | - cd release_stats && npx tsx fetch-release-stats.ts - - - name: Display Desktop CSV in summary - run: | - echo "# Desktop Release Download Statistics" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Snapshot Date:** $(date +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Convert CSV to markdown table - awk -F',' 'NR==1 { - print "| " $1 " | " $2 " | " $3 " | " $4 " | " $5 " | " $6 " | " $7 " | " $8 " | " $9 " | " $10 " | " $11 " |" - print "|---|---|---|---|---|---|---|---|---|---|---|" - } NR>1 { - print "| " $1 " | " $2 " | " $3 " | " $4 " | " $5 " | " $6 " | " $7 " | " $8 " | " $9 " | " $10 " | " $11 " |" - }' release_stats/session-desktop-release-stats.csv >> $GITHUB_STEP_SUMMARY - - - name: Display Android CSV in summary - run: | - echo "# Android Release Download Statistics" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Snapshot Date:** $(date +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Convert CSV to markdown table - awk -F',' 'NR==1 { - print "| " $1 " | " $2 " | " $3 " | " $4 " | " $5 " | " $6 " | " $7 " | " $8 " | " $9 " | " $10 " | " $11 " |" - print "|---|---|---|---|---|---|---|---|---|---|---|" - } NR>1 { - print "| " $1 " | " $2 " | " $3 " | " $4 " | " $5 " | " $6 " | " $7 " | " $8 " | " $9 " | " $10 " | " $11 " |" - }' release_stats/session-android-release-stats.csv >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/notify_failure.yml b/.github/workflows/notify_failure.yml deleted file mode 100644 index 7a62c49..0000000 --- a/.github/workflows/notify_failure.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Discord Failure Notification - -on: - workflow_run: - workflows: - - "Check for Crowdin Updates" - - "Test Failure Notification" - - "Crowdin Multiple Translations Report" - types: - - completed - -jobs: - notify: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'failure' }} - steps: - - name: Send Discord notification - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - DISCORD_ROLE_ID: ${{ secrets.DISCORD_ROLE_ID }} - WORKFLOW_NAME: ${{ github.event.workflow_run.name }} - run: | - if [ "$WORKFLOW_NAME" = "Test Failure Notification" ]; then - FLAGS=4096 - else - FLAGS=0 - fi - curl -H "Content-Type: application/json" \ - -d '{ - "content": "<@&'"$DISCORD_ROLE_ID"'> ⚠️ GitHub Action failed!", - "flags": '"$FLAGS"', - "embeds": [{ - "title": "Workflow Failed: '"$WORKFLOW_NAME"'", - "url": "${{ github.event.workflow_run.html_url }}", - "color": 15158332, - "fields": [ - {"name": "Repository", "value": "${{ github.repository }}", "inline": true}, - {"name": "Branch", "value": "${{ github.event.workflow_run.head_branch }}", "inline": true} - ] - }] - }' \ - "$DISCORD_WEBHOOK_URL" diff --git a/.github/workflows/test_failure_notification.yml b/.github/workflows/test_failure_notification.yml deleted file mode 100644 index d22440c..0000000 --- a/.github/workflows/test_failure_notification.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Test Failure Notification - -on: - workflow_dispatch: - -jobs: - fail: - runs-on: ubuntu-latest - steps: - - name: Fail on purpose - run: exit 1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..0a2eae7 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,70 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - run: pip install uv==0.12.19 + + - run: uv run --locked --only-dev ruff check --output-format=github . + + unittest: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - run: pip install uv==0.12.19 + + - run: uv sync --locked + + # tests/sogs skips itself here: session_util is not installable from PyPI. + - run: uv run --locked python -m unittest discover -s tests -t . -v + + sogs_moderation: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + # session_util is published as a deb built against the system interpreter, so + # this suite runs on that interpreter with a venv that can see it. + - name: Install python3-session-util + run: | + sudo curl -so /usr/share/keyrings/session-foundation.gpg https://deb.session.foundation/pub.gpg + printf 'Types: deb\nURIs: https://deb.session.foundation\nSuites: %s\nComponents: main\nSigned-By: /usr/share/keyrings/session-foundation.gpg\n' \ + "$(lsb_release -sc)" | sudo tee /etc/apt/sources.list.d/session.sources + sudo apt-get update + sudo apt-get install -y python3-session-util + + # pipx, since the system pip refuses to install outside a venv (PEP 668). + - run: pipx install uv==0.12.19 + + - name: Install dependencies + run: | + uv venv --system-site-packages --python /usr/bin/python3 + uv sync --locked + + # Fails here rather than letting tests/sogs skip itself. + - run: uv run --locked python -c "import session_util" + + - run: uv run --locked python -m unittest discover -s tests/sogs -t . -v diff --git a/.github/workflows/update_static_snode_list.yml b/.github/workflows/update_static_snode_list.yml deleted file mode 100644 index 68712bf..0000000 --- a/.github/workflows/update_static_snode_list.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Update Static Snode List - -on: - schedule: - - cron: '30 10 * * *' # Runs daily at 10:30am UTC (after source repo updates at 10am UTC) - workflow_dispatch: # Allow manual trigger - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - sync: - name: Copy service-nodes-cache.json and open PR if changed - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout Repo Content - uses: actions/checkout@v7 - with: - path: 'scripts' - # don't provide a branch (ref) so it uses the default for that event - - name: Checkout iOS - uses: ./scripts/actions/checkout_ios - - name: Fetch latest service-nodes-cache.json from dynamic assets repo - run: | - curl -fsSL \ - "https://raw.githubusercontent.com/session-foundation/session-desktop-dynamic-assets/main/service-nodes-cache.json" \ - -o ${{ github.workspace }}/ios/Session/Meta/service-nodes-cache.json - - name: Create Pull Request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - path: "ios" - token: ${{ secrets.CROWDIN_PR_TOKEN }} - title: "[Automated] Update fallback static snode list" - body: | - [Automated] - This PR updates the static service node list which is used as a fallback when a new client is unable to contact the seed nodes - branch: feature/update-static-snode-list - commit-message: "[Automated] Update fallback static snode list" - delete-branch: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 792c7cf..f0edb99 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,11 @@ do_not_commit.sh .crowdin_audit/ # Local Zendesk triage debugging artifacts — these contain ticket content -zendesk_triage/*.json +/*.json # Zendesk triage dedup state (ticket ids + timestamps; restored from CI cache) .triage-state/ .claude/ + +.venv/ diff --git a/README.md b/README.md index e486156..7345e3e 100644 --- a/README.md +++ b/README.md @@ -1,670 +1,37 @@ # Session Shared Scripts -This repo houses scripts which are shared between the different platform repos for Session, it also contains a number of Actions used to automatically sync some shared elements across the repos. +Session Foundation's scheduled jobs and webhooks, and the scripts the platform repos +share for translations. One package, `session_ops`, deployed to one self-hosted box: +[deploy/README.md](deploy/README.md) installs it. -## Crowdin Translation Workflow +## Jobs -Automated workflow that downloads translations from Crowdin, validates them, and creates PRs for iOS and Android platforms and for the Typescript Localization Module for Desktop and QA. - -### Required Secrets - -| Secret | Description | -| ------------------- | ------------------------------------------------------- | -| `CROWDIN_API_TOKEN` | Crowdin personal access token (see scopes below) | -| `CROWDIN_PR_TOKEN` | GitHub token with PR creation permissions | - -#### Crowdin token scopes - -Crowdin scopes personal access tokens per endpoint family, so a token missing one -scope returns `403 Forbidden` on just those endpoints while every other call keeps -working. The scripts in this repo need: - -| Scope | Value | Needed for | -| ---------------------- | -------------------- | ----------------------------------------------------------------------- | -| Projects | `project` | Project details and the target-language list | -| Source files & strings | `project.source` | Listing source strings (`approve_strings.py`, multiple-translations report) | -| Translations | `project.translation` | Translation exports, plus reading/adding approvals and translations | -| Glossaries | `glossary` | Non-translatable strings (glossary terms) | - -> **Note:** Scopes only cap what a token may do — they don't grant anything the -> token's Crowdin account can't already do, so the account also needs a project -> role that allows it (manager/proofreader for anything that writes, e.g. the -> approvals `POST` in `approve_strings.py`). - -### Workflow Inputs - -| Input | Default | Description | -| ------------------------ | ------- | ---------------------------------------- | -| `UPDATE_PULL_REQUESTS` | `true` | Create/update PRs for all platforms | -| `SKIP_VALIDATION_ERRORS` | `false` | Continue even if string validation fails | - -### Schedule - -Runs automatically every Monday at 00:00 UTC. - -### Validation Rules - -#### All Strings (including plurals) - -- **Valid `{variable}` syntax** - No broken braces (`{`, `}`, `{}`, `{ space }`) -- **Allowed HTML tags only** - Only ``, `
`, `` -- **Valid tag syntax** - No malformed `<` (e.g., `