Translations update from Hosted Weblate #1623
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI Pipeline | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| push: | |
| branches: | |
| - main | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Detect if changes are translation-only (src/locales/ files) | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| translation_only: ${{ steps.check.outputs.translation_only }} | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check if translation-only | |
| id: check | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) | |
| else | |
| CHANGED=$(git diff --name-only HEAD~1) | |
| fi | |
| echo "Changed files:" | |
| echo "$CHANGED" | |
| # Check if ALL changed files are under src/locales/ | |
| TRANSLATION_ONLY="true" | |
| while IFS= read -r file; do | |
| if [ -z "$file" ]; then | |
| continue | |
| fi | |
| case "$file" in | |
| src/locales/*) ;; | |
| *) TRANSLATION_ONLY="false"; break ;; | |
| esac | |
| done <<< "$CHANGED" | |
| echo "translation_only=$TRANSLATION_ONLY" >> "$GITHUB_OUTPUT" | |
| echo "Translation only: $TRANSLATION_ONLY" | |
| # Job for linting, type checking, unit testing | |
| build-and-test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| # Step 1: Check out the repository | |
| - name: Check out code | |
| uses: actions/checkout@v3 | |
| # Cache node_modules | |
| - name: Cache node_modules | |
| uses: actions/cache@v3 | |
| with: | |
| path: node_modules | |
| key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }} | |
| # Step 2: Set up Node.js environment | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v3 | |
| with: | |
| node-version: '22.21.0' | |
| cache: 'yarn' | |
| - name: Install Yarn | |
| run: npm install -g yarn | |
| # Step 3: Install dependencies and iOS Pods | |
| - name: Install dependencies | |
| run: yarn install | |
| # Step 4: Validate l10n files | |
| - name: Validate l10n files | |
| run: node scripts/validate-l10n.js | |
| # Step 4b: Verify font registration (I8 — every fontFamily must | |
| # be bundled on iOS + Android + listed in Info.plist UIAppFonts). | |
| - name: Verify font registration | |
| run: node scripts/verify-fonts.js | |
| # Step 5: Run linters (ESLint) | |
| - name: Run ESLint | |
| run: yarn lint | |
| # Step 5: Run TypeScript type checks | |
| - name: Run TypeScript type check | |
| run: yarn typecheck | |
| # Step 6: Run unit tests (Jest) | |
| - name: Run unit tests | |
| run: yarn test --coverage | |
| # Job for Android build | |
| build-android: | |
| runs-on: ubuntu-latest | |
| needs: [build-and-test, detect-changes] | |
| if: needs.detect-changes.outputs.translation_only != 'true' | |
| env: | |
| # Declared here rather than in android/gradle.properties: llama.rn's own | |
| # android/gradle.properties sets rnllamaBuildFromSource, and a subproject's | |
| # value beats the root project's, so only an environment property or -P wins. | |
| ORG_GRADLE_PROJECT_rnllamaBuildFromSource: 'true' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v3 | |
| # Must precede setup-node and setup-java: /opt/hostedtoolcache is where | |
| # those install. The Hexagon SDK adds ~3.1 GB on top of a from-source build. | |
| - name: Maximize build space | |
| run: | | |
| df -h / | |
| sudo rm -rf /usr/share/dotnet /opt/ghc /opt/hostedtoolcache | |
| df -h / | |
| # Set up Node.js environment | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v3 | |
| with: | |
| node-version: '22.21.0' | |
| cache: 'yarn' | |
| # Cache node_modules | |
| - name: Cache node_modules | |
| uses: actions/cache@v3 | |
| with: | |
| path: node_modules | |
| key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }} | |
| - name: Install Yarn | |
| run: npm install -g yarn | |
| - name: Install dependencies | |
| run: yarn install | |
| - name: Derive the rnllama variant allowlist from the payload manifest | |
| run: | | |
| set -euo pipefail | |
| VARIANTS=$(node scripts/verify-android-payload.js --print-variants) | |
| echo "Declared allowlist: $VARIANTS" | |
| echo "ORG_GRADLE_PROJECT_rnllamaVariants=$VARIANTS" >> "$GITHUB_ENV" | |
| # Keyed on the manifest as well as yarn.lock: the manifest decides the | |
| # variant set, so changing it must invalidate the configured build tree. | |
| - name: Cache llama.rn native build tree | |
| uses: actions/cache@v4 | |
| with: | |
| path: node_modules/llama.rn/android/.cxx | |
| key: ${{ runner.os }}-cxx-${{ hashFiles('yarn.lock', 'scripts/android-payload-manifest.json') }} | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v3 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: Set up the Hexagon SDK | |
| uses: ./.github/actions/setup-hexagon-sdk | |
| - name: Set up ccache | |
| uses: ./.github/actions/setup-ccache | |
| - name: Create dummy google-services.json for CI | |
| run: | | |
| cat > android/app/google-services.json << 'EOL' | |
| { | |
| "project_info": { | |
| "project_number": "000000000000", | |
| "project_id": "dummy-project-for-ci", | |
| "storage_bucket": "dummy-project-for-ci.appspot.com" | |
| }, | |
| "client": [{ | |
| "client_info": { | |
| "mobilesdk_app_id": "1:000000000000:android:0000000000000000", | |
| "android_client_info": { | |
| "package_name": "com.pocketpalai" | |
| } | |
| }, | |
| "api_key": [{ | |
| "current_key": "dummy-api-key-for-ci-builds" | |
| }] | |
| }] | |
| } | |
| EOL | |
| - name: Create dummy .env file for CI | |
| run: | | |
| cat > .env << 'EOL' | |
| FIREBASE_FUNCTIONS_URL=https://dummy-firebase-url.com | |
| # PalsHub/Supabase Configuration | |
| SUPABASE_URL=https://dummy-supabase-url.supabase.co | |
| SUPABASE_ANON_KEY=dummy-anon-key-for-ci-builds | |
| PALSHUB_API_BASE_URL=https://dummy-palshub-api.com | |
| # App Configuration | |
| APP_URL=pocketpal://app | |
| # Feature Flags (true/false) | |
| ENABLE_PALSHUB_INTEGRATION=true | |
| ENABLE_AUTHENTICATION=true | |
| ENABLE_OFFLINE_MODE=true | |
| # Google Sign-In Configuration | |
| GOOGLE_IOS_CLIENT_ID=dummy-ios-client-id.apps.googleusercontent.com | |
| GOOGLE_WEB_CLIENT_ID=dummy-web-client-id.apps.googleusercontent.com | |
| EOL | |
| - name: Create dummy release keystore for CI | |
| working-directory: android/app | |
| run: | | |
| # Generate a dummy keystore for CI builds | |
| keytool -genkeypair -v \ | |
| -storetype PKCS12 \ | |
| -keystore pocketpal-release-key.keystore \ | |
| -alias pocketpal_key_alias \ | |
| -keyalg RSA \ | |
| -keysize 2048 \ | |
| -validity 10000 \ | |
| -storepass dummy-ci-password \ | |
| -keypass dummy-ci-password \ | |
| -dname "CN=CI Build, OU=CI, O=PocketPal, L=CI, S=CI, C=US" | |
| - name: Build Android Release | |
| working-directory: android | |
| env: | |
| APP_RELEASE_STORE_PASSWORD: dummy-ci-password | |
| APP_RELEASE_KEY_PASSWORD: dummy-ci-password | |
| run: | | |
| set -euo pipefail | |
| # Compared against "true" rather than merely checked for emptiness: | |
| # the payload gate cannot tell the modes apart (upstream's prebuilt | |
| # exports the same symbols), so this guard is the only thing holding | |
| # the declaration. | |
| if [ "${ORG_GRADLE_PROJECT_rnllamaBuildFromSource:-}" != "true" ]; then | |
| echo "::error::ORG_GRADLE_PROJECT_rnllamaBuildFromSource is '${ORG_GRADLE_PROJECT_rnllamaBuildFromSource:-<unset>}', expected 'true'. Building from prebuilts is the documented escape hatch, but it must be a deliberate edit to this workflow." | |
| exit 1 | |
| fi | |
| ./gradlew assembleProdRelease 2>&1 | tee android-build.log | |
| - name: ccache statistics | |
| if: always() | |
| run: ccache --show-stats || echo "ccache unavailable (its setup step did not complete)" | |
| # COUPLING: the string grepped for below is upstream's, printed by | |
| # node_modules/llama.rn/android/build.gradle next to its rnllamaVariants | |
| # lookup. A llama.rn upgrade that rewords it fails this step. That is the | |
| # intended direction — it is the only live proof the environment reaches | |
| # gradle — but the fix is to update the literal, not to delete the check. | |
| - name: Verify the variant allowlist reached the llama.rn build | |
| run: | | |
| set -euo pipefail | |
| VARIANTS=$(node scripts/verify-android-payload.js --print-variants) | |
| if ! grep -qF "Building rnllama variants: $VARIANTS" android/android-build.log; then | |
| echo "::error::llama.rn did not report building the declared variant set." | |
| echo "Expected: $VARIANTS" | |
| echo "In the build log:" | |
| grep -F "rnllama variants" android/android-build.log || echo " (no such line at all)" | |
| echo "" | |
| echo "Two different causes look identical here. Either ORG_GRADLE_PROJECT_rnllamaVariants" | |
| echo "did not reach the llama.rn subproject, or upstream changed the wording of the line" | |
| echo "this step greps for (android/build.gradle, the println next to rnllamaVariants)." | |
| echo "Check the wording first — a llama.rn upgrade is the more common reason." | |
| exit 1 | |
| fi | |
| echo "OK: llama.rn reported the declared variant list (extras, if any, are permitted and are reported by the payload check)." | |
| - name: Verify the Android payload | |
| run: | | |
| node scripts/verify-android-payload.js \ | |
| --apk android/app/build/outputs/apk/prod/release/app-prod-release.apk \ | |
| --report payload-report.txt | |
| # The report is evidence about the artifact, not the artifact, so it | |
| # uploads whether or not the check passed. The APK below does not. | |
| - name: Upload payload report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: payload-report | |
| path: payload-report.txt | |
| if-no-files-found: ignore | |
| - name: Verify prod APK has no automation-bridge code (DCE sanity check) | |
| run: | | |
| set -euo pipefail | |
| APK=android/app/build/outputs/apk/prod/release/app-prod-release.apk | |
| echo "Checking bundle inside $APK for E2E markers..." | |
| WORK=$(mktemp -d) | |
| unzip -p "$APK" assets/index.android.bundle > "$WORK/bundle" || { | |
| echo "FAIL: could not extract assets/index.android.bundle from APK" | |
| exit 1 | |
| } | |
| MARKERS=( | |
| 'AUTOMATION_BRIDGE' | |
| 'memory-snapshot-label' | |
| 'memory-snapshot-result' | |
| 'BENCH_RUN_MATRIX' | |
| 'bench-runner-screen-status' | |
| ) | |
| FAIL=0 | |
| for m in "${MARKERS[@]}"; do | |
| if grep -q -F "$m" "$WORK/bundle"; then | |
| echo "FAIL: prod bundle contains automation marker: $m" | |
| FAIL=1 | |
| fi | |
| done | |
| if [ "$FAIL" -ne 0 ]; then | |
| echo "" | |
| echo "The automation bridge was not DCE-stripped from the prod build." | |
| echo "Check that __E2E__ is being replaced at build time by babel-plugin-transform-define." | |
| echo "See src/__automation__/README.md for the DCE contract." | |
| exit 1 | |
| fi | |
| echo "OK: prod bundle contains zero automation markers." | |
| - name: Upload Android APK | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-release-apk | |
| path: android/app/build/outputs/apk/prod/release/app-prod-release.apk | |
| # Job for iOS build | |
| build-ios: | |
| runs-on: macos-26 # Keep CI on the same Xcode major as the release workflow | |
| needs: [build-and-test, detect-changes] | |
| if: needs.detect-changes.outputs.translation_only != 'true' | |
| env: | |
| NODE_OPTIONS: '--max-old-space-size=8192' | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v3 | |
| # Set up Node.js environment | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v3 | |
| with: | |
| node-version: '22.21.0' | |
| cache: 'yarn' | |
| # Cache node_modules | |
| - name: Cache node_modules | |
| uses: actions/cache@v3 | |
| with: | |
| path: node_modules | |
| key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }} | |
| - name: Install Yarn | |
| run: npm install -g yarn | |
| - name: Install dependencies | |
| run: yarn install | |
| - name: Create Env.xcconfig | |
| run: | | |
| cat > ios/Config/Env.xcconfig << 'EOL' | |
| // This file contains environment-specific build settings. | |
| GOOGLE_WEB_CLIENT_ID = dummy-web-client-id.apps.googleusercontent.com | |
| REVERSED_GOOGLE_IOS_CLIENT_ID = com.googleusercontent.apps.dummy-ios-client-id | |
| EOL | |
| - name: Create dummy .env file for CI | |
| run: | | |
| cat > .env << 'EOL' | |
| FIREBASE_FUNCTIONS_URL=https://dummy-firebase-url.com | |
| # PalsHub/Supabase Configuration | |
| SUPABASE_URL=https://dummy-supabase-url.supabase.co | |
| SUPABASE_ANON_KEY=dummy-anon-key-for-ci-builds | |
| PALSHUB_API_BASE_URL=https://dummy-palshub-api.com | |
| # App Configuration | |
| APP_URL=pocketpal://app | |
| # Feature Flags (true/false) | |
| ENABLE_PALSHUB_INTEGRATION=true | |
| ENABLE_AUTHENTICATION=true | |
| ENABLE_OFFLINE_MODE=true | |
| # Google Sign-In Configuration | |
| GOOGLE_IOS_CLIENT_ID=dummy-ios-client-id.apps.googleusercontent.com | |
| GOOGLE_WEB_CLIENT_ID=dummy-web-client-id.apps.googleusercontent.com | |
| EOL | |
| - name: Create dummy GoogleService-Info.plist for CI | |
| run: | | |
| cat > ios/GoogleService-Info.plist << 'EOL' | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>CLIENT_ID</key> | |
| <string>dummy-client-id.apps.googleusercontent.com</string> | |
| <key>REVERSED_CLIENT_ID</key> | |
| <string>com.googleusercontent.apps.dummy-client-id</string> | |
| <key>API_KEY</key> | |
| <string>dummy-api-key-for-ci-builds</string> | |
| <key>GCM_SENDER_ID</key> | |
| <string>000000000000</string> | |
| <key>PLIST_VERSION</key> | |
| <string>1</string> | |
| <key>BUNDLE_ID</key> | |
| <string>ai.pocketpal</string> | |
| <key>PROJECT_ID</key> | |
| <string>dummy-project-for-ci</string> | |
| <key>STORAGE_BUCKET</key> | |
| <string>dummy-project-for-ci.appspot.com</string> | |
| <key>IS_ADS_ENABLED</key> | |
| <false/> | |
| <key>IS_ANALYTICS_ENABLED</key> | |
| <false/> | |
| <key>IS_APPINVITE_ENABLED</key> | |
| <false/> | |
| <key>IS_GCM_ENABLED</key> | |
| <true/> | |
| <key>IS_SIGNIN_ENABLED</key> | |
| <true/> | |
| <key>GOOGLE_APP_ID</key> | |
| <string>1:000000000000:ios:0000000000000000</string> | |
| </dict> | |
| </plist> | |
| EOL | |
| - name: Install CocoaPods dependencies | |
| run: | | |
| cd ios | |
| pod install | |
| cd .. | |
| - name: Select default Xcode | |
| run: | | |
| sudo xcode-select -s /Applications/Xcode.app | |
| xcodebuild -version | |
| - name: Build iOS Release | |
| run: | | |
| cd ios | |
| # List available simulators for debugging | |
| xcrun simctl list devices available | |
| xcodebuild \ | |
| -workspace PocketPal.xcworkspace \ | |
| -scheme PocketPal \ | |
| -configuration Release \ | |
| -sdk iphonesimulator \ | |
| -destination 'generic/platform=iOS Simulator' \ | |
| CODE_SIGN_IDENTITY="" \ | |
| CODE_SIGNING_REQUIRED=NO \ | |
| CODE_SIGNING_ALLOWED=NO \ | |
| ONLY_ACTIVE_ARCH=NO \ | |
| 2>&1 | tee ../ios-build.log | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "::error::iOS build failed (exit $EXIT_CODE)" | |
| echo "::group::Error context" | |
| grep -B 5 -A 20 "error:" ../ios-build.log | tail -200 || tail -100 ../ios-build.log | |
| echo "::endgroup::" | |
| exit $EXIT_CODE | |
| fi |