← Back to Blog

Automating Flutter Localization in CI/CD Pipelines

flutterlocalizationci-cdautomationdevops

Automating Flutter Localization in CI/CD Pipelines

Manual translation management doesn't scale. When you're shipping features weekly, waiting days for translations creates bottlenecks. This guide shows you how to integrate Flutter localization into your CI/CD pipeline for seamless, automated deployments.

The CI/CD Localization Challenge

Your team pushes code multiple times daily, but translations lag behind:

  • ❌ Developers add new keys manually
  • ❌ Translators work in separate spreadsheets
  • ❌ ARB files get out of sync
  • ❌ Builds fail due to missing translations
  • ❌ Releases get blocked waiting for translations

Solution: Automated Localization Pipeline

Here's how to build a robust localization CI/CD workflow:

1. Detect Missing Translation Keys

Create a script to validate translations before builds:

#!/bin/bash
# validate-translations.sh

set -e

echo "🔍 Validating translation files..."

# Get template keys
template_keys=$(jq -r 'keys[]' assets/l10n/app_en.arb | grep -v "^@")

# Check each locale
for file in assets/l10n/app_*.arb; do
    locale=$(basename "$file" .arb | sed 's/app_//')

    echo "Checking $locale..."

    # Get locale keys
    locale_keys=$(jq -r 'keys[]' "$file" | grep -v "^@")

    # Find missing keys
    missing=$(comm -23 <(echo "$template_keys" | sort) <(echo "$locale_keys" | sort))

    if [ -n "$missing" ]; then
        echo "❌ Missing keys in $locale:"
        echo "$missing"
        exit 1
    fi
done

echo "✅ All translations are complete!"

2. GitHub Actions Workflow

.github/workflows/localization.yml:

name: Localization CI

on:
  pull_request:
    paths:
      - 'assets/l10n/**'
  push:
    branches: [main, develop]

jobs:
  validate:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'

      - name: Install dependencies
        run: flutter pub get

      - name: Validate translations
        run: ./scripts/validate-translations.sh

      - name: Generate localizations
        run: flutter gen-l10n

      - name: Check for uncommitted changes
        run: |
          if [[ -n $(git status -s) ]]; then
            echo "❌ Generated files are not committed"
            git diff
            exit 1
          fi

      - name: Run tests with all locales
        run: flutter test test/localization_test.dart

  auto-translate:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    needs: validate

    steps:
      - uses: actions/checkout@v3

      - name: Auto-translate missing keys
        env:
          FLUTTER_LOCALISATION_API_KEY: ${{ secrets.FLUTTER_LOCALISATION_API_KEY }}
        run: |
          # Use FlutterLocalisation API to translate
          curl -X POST https://api.flutterlocalisation.com/translate \
            -H "Authorization: Bearer $FLUTTER_LOCALISATION_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{"project_id": "${{ secrets.PROJECT_ID }}"}'

      - name: Commit translations
        uses: stefanzweifel/git-auto-commit-action@v4
        with:
          commit_message: "🌍 Auto-translate missing keys"
          file_pattern: assets/l10n/*.arb

3. GitLab CI Pipeline

.gitlab-ci.yml:

stages:
  - validate
  - translate
  - build

validate_translations:
  stage: validate
  image: cirrusci/flutter:stable
  script:
    - flutter pub get
    - chmod +x scripts/validate-translations.sh
    - ./scripts/validate-translations.sh
    - flutter gen-l10n
  only:
    changes:
      - assets/l10n/**

auto_translate:
  stage: translate
  image: curlimages/curl:latest
  script:
    - |
      curl -X POST $FLUTTER_LOCALISATION_API_URL \
        -H "Authorization: Bearer $FLUTTER_LOCALISATION_API_KEY" \
        -d "{\"project_id\": \"$PROJECT_ID\"}"
  only:
    - merge_requests

build_app:
  stage: build
  image: cirrusci/flutter:stable
  script:
    - flutter pub get
    - flutter gen-l10n
    - flutter build apk --release
  artifacts:
    paths:
      - build/app/outputs/flutter-apk/

4. Pre-commit Hooks

Catch translation issues before they reach CI:

.git/hooks/pre-commit:

#!/bin/bash

# Check if .arb files were modified
arb_files=$(git diff --cached --name-only --diff-filter=AM | grep "\.arb$")

if [ -n "$arb_files" ]; then
    echo "🔍 Validating translation files..."

    # Run validation
    ./scripts/validate-translations.sh

    if [ $? -ne 0 ]; then
        echo "❌ Translation validation failed"
        echo "Fix the issues or use 'git commit --no-verify' to skip"
        exit 1
    fi

    # Regenerate localizations
    flutter gen-l10n

    # Stage generated files
    git add .dart_tool/flutter_gen/gen_l10n/*.dart

    echo "✅ Translations validated and generated"
fi

Automated Translation Updates

Using FlutterLocalisation CLI

# Install CLI
dart pub global activate flutter_localisation

# Sync translations in CI/CD
flutter_localisation sync \
  --project-key $PROJECT_KEY \
  --pull  # Pull latest translations

# Or push new keys
flutter_localisation sync \
  --project-key $PROJECT_KEY \
  --push  # Push new keys for translation

GitHub Action Example

- name: Sync with FlutterLocalisation
  run: |
    dart pub global activate flutter_localisation
    flutter_localisation sync \
      --project-key ${{ secrets.PROJECT_KEY }} \
      --pull

Translation Memory & Reuse

Avoid retranslating the same strings:

#!/bin/bash
# check-translation-memory.sh

# Check if new keys exist in other files
new_key="$1"
new_value="$2"

echo "🔍 Checking translation memory for: $new_value"

# Search all .arb files
for file in assets/l10n/app_*.arb; do
    matches=$(jq -r "to_entries[] | select(.value == \"$new_value\") | .key" "$file")

    if [ -n "$matches" ]; then
        echo "✨ Found existing translations:"
        echo "$matches"
    fi
done

Monitoring Translation Coverage

Add metrics to track coverage:

- name: Report translation coverage
  run: |
    total_keys=$(jq 'keys | length' assets/l10n/app_en.arb)

    for file in assets/l10n/app_*.arb; do
        locale=$(basename "$file" .arb | sed 's/app_//')
        translated=$(jq 'keys | length' "$file")
        coverage=$(echo "scale=2; $translated / $total_keys * 100" | bc)
        echo "$locale: $coverage% ($translated/$total_keys)"
    done

Rollback Strategy

Handle translation issues in production:

- name: Create translation backup
  run: |
    cp -r assets/l10n/ backups/l10n-$(date +%Y%m%d-%H%M%S)/

- name: Rollback on failure
  if: failure()
  run: |
    latest_backup=$(ls -td backups/l10n-* | head -1)
    cp -r "$latest_backup"/* assets/l10n/

Continuous Deployment with Translations

deploy:
  stage: deploy
  script:
    # Ensure translations are up to date
    - flutter_localisation sync --pull

    # Generate final localizations
    - flutter gen-l10n

    # Build for production
    - flutter build appbundle --release

    # Deploy
    - fastlane deploy
  only:
    - main

Best Practices

  1. Block merges with incomplete translations
  2. Auto-translate in feature branches, review before merge
  3. Version control all .arb files
  4. Generate localizations in CI, not locally
  5. Test all locales in your test suite
  6. Monitor coverage with metrics
  7. Use translation memory to avoid duplicates
  8. Create backups before deployments

Testing Localization in CI

// test/localization_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

void main() {
  testWidgets('All locales load successfully', (tester) async {
    final supportedLocales = AppLocalizations.supportedLocales;

    for (final locale in supportedLocales) {
      await tester.pumpWidget(
        MaterialApp(
          locale: locale,
          localizationsDelegates: AppLocalizations.localizationsDelegates,
          supportedLocales: supportedLocales,
          home: Container(),
        ),
      );

      final l10n = AppLocalizations.of(tester.element(find.byType(Container)))!;

      // Test key strings load
      expect(l10n.appTitle, isNotEmpty);
      expect(l10n.welcomeMessage, isNotEmpty);
    }
  });
}

Conclusion

Automating localization in CI/CD eliminates bottlenecks and ensures translations stay in sync with code. The initial setup investment pays off in faster deployments and fewer translation-related bugs.

Stop blocking releases on manual translation updates. Integrate FlutterLocalisation into your CI/CD pipeline and ship localized features as fast as you write code.


Ready to automate? FlutterLocalisation provides CLI tools, GitHub Actions, and API access for seamless CI/CD integration. Pull translations automatically on every build.