Translation Workflow & CI
Di production, mengelola terjemahan secara manual tidak scalable. Translation Management Systems (TMS) dan CI/CD integration memastikan terjemahan selalu sinkron dan berkualitas.
Translation Management Platforms
- Crowdin — Populer di open-source, GitHub integration kuat
- Lokalise — Developer-focused, API yang powerful
- Phrase (Memsource) — Enterprise-grade, machine translation built-in
- Transifex — Tradisional, banyak format support
- Weblate — Open-source, self-hosted option
Workflow Tipikal
// 1. Developer menambah/mengubah source string
// en/common.json: { "new_feature": "Try our new feature!" }
// 2. CI otomatis push source strings ke TMS
// GitHub Action / CI pipeline
// 3. Translator menerjemahkan di TMS UI
// id: "Coba fitur baru kami!"
// ja: "新機能をお試しください!"
// 4. TMS membuat PR otomatis dengan terjemahan baru
// 5. CI validasi → merge → deploy
GitHub Action: Crowdin Integration
# .github/workflows/crowdin.yml
name: Crowdin Sync
on:
push:
branches: [main]
paths: ["public/locales/en/**"] # Hanya trigger saat source berubah
jobs:
upload-sources:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Upload sources to Crowdin
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
download_translations: false
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
# Crowdin akan membuat PR otomatis saat terjemahan selesai
CI: Validasi Terjemahan
# .github/workflows/i18n-check.yml
name: i18n Validation
on: pull_request
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
# Cek missing keys
- name: Check missing translations
run: node scripts/check-translations.js
# Cek format ICU valid
- name: Validate ICU messages
run: npx @formatjs/cli compile public/locales/id/common.json
# Cek tidak ada hardcoded strings
- name: Lint for literal strings
run: npx eslint src/ --rule 'i18next/no-literal-string: error'
Script Check Missing Translations
// scripts/check-translations.js
const fs = require("fs");
const path = require("path");
const sourceLocale = "en";
const targetLocales = ["id", "ja", "ar"];
const localesDir = path.join(__dirname, "../public/locales");
function getAllKeys(obj, prefix = "") {
return Object.entries(obj).flatMap(([key, val]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
return typeof val === "object" ? getAllKeys(val, fullKey) : [fullKey];
});
}
let hasErrors = false;
const namespaces = fs.readdirSync(path.join(localesDir, sourceLocale))
.filter(f => f.endsWith(".json"));
for (const ns of namespaces) {
const sourceKeys = getAllKeys(
JSON.parse(fs.readFileSync(path.join(localesDir, sourceLocale, ns)))
);
for (const locale of targetLocales) {
const targetFile = path.join(localesDir, locale, ns);
if (!fs.existsSync(targetFile)) {
console.error(`Missing file: ${locale}/${ns}`);
hasErrors = true;
continue;
}
const targetKeys = getAllKeys(JSON.parse(fs.readFileSync(targetFile)));
const missing = sourceKeys.filter(k => !targetKeys.includes(k));
if (missing.length) {
console.error(`[${locale}/${ns}] Missing: ${missing.join(", ")}`);
hasErrors = true;
}
}
}
process.exit(hasErrors ? 1 : 0);