Crowdin
"Crowdin: translation management, OTA content delivery, API, CLI, GitHub integration, in-context editing, glossaries"
Crowdin is a cloud-based translation management system that centralizes localization workflows. Source strings are uploaded from your codebase, translators work in Crowdin's editor, and completed translations are pushed back via integrations. The OTA (Over-The-Air) content delivery system lets you ship translation updates without redeploying your app. Crowdin's API and CLI automate the upload/download cycle, while the GitHub integration keeps translation branches in sync with your repository. Glossaries and translation memory ensure consistency across projects and languages. ## Key Points - Use `crowdin.yml` at the repo root and commit it — it documents your file mapping for the whole team. - Set `updateOption: KEEP_TRANSLATIONS_AND_APPROVALS` when updating source files to preserve existing translator work. - Use the GitHub integration to auto-create PRs with new translations on every Crowdin build. - Create glossaries before starting translations to enforce consistent terminology across languages. - Use OTA content delivery for mobile or SPA apps where you want translation hotfixes without a full redeploy. - Set `context` on source strings so translators see where each string appears, reducing ambiguity. - Use Crowdin's branching feature to isolate translations for feature branches and merge them when the code merges. - Run `crowdin upload sources` in CI on every push to main to keep Crowdin in sync automatically. - Use translation memory across projects to avoid paying for re-translation of identical strings. - Set `maxLength` on strings that appear in space-constrained UI elements so translators respect length limits. - Do not commit the `CROWDIN_PERSONAL_TOKEN` in `crowdin.yml` — use `api_token_env` to reference an environment variable. - Avoid manually editing downloaded translation files — changes will be overwritten on the next sync.
skilldb get i18n-services-skills/CrowdinFull skill: 319 linesCrowdin
Core Philosophy
Crowdin is a cloud-based translation management system that centralizes localization workflows. Source strings are uploaded from your codebase, translators work in Crowdin's editor, and completed translations are pushed back via integrations. The OTA (Over-The-Air) content delivery system lets you ship translation updates without redeploying your app. Crowdin's API and CLI automate the upload/download cycle, while the GitHub integration keeps translation branches in sync with your repository. Glossaries and translation memory ensure consistency across projects and languages.
Setup
CLI installation and configuration
// Install Crowdin CLI
// npm install -g @crowdin/cli
// crowdin.yml at project root
// project_id: 12345
// api_token_env: CROWDIN_PERSONAL_TOKEN
// base_path: "."
// base_url: "https://api.crowdin.com"
//
// files:
// - source: "/src/locales/en/*.json"
// translation: "/src/locales/%two_letters_code%/%original_file_name%"
// - source: "/src/locales/en/**/*.json"
// translation: "/src/locales/%two_letters_code%/**/%original_file_name%"
OTA SDK setup for React
// npm install @crowdin/ota-client
import OtaClient from "@crowdin/ota-client";
const otaClient = new OtaClient(process.env.CROWDIN_OTA_HASH!, {
disableManifestCache: false,
languageCode: "uk", // Ukrainian
});
// Fetch all translations for a language
async function loadOtaTranslations(lang: string): Promise<Record<string, string>> {
const translations = await otaClient.getStringsByLocale(lang);
return translations as Record<string, string>;
}
// Fetch a specific file
async function loadOtaFile(lang: string, filePath: string): Promise<string> {
const content = await otaClient.getFileTranslations(filePath, lang);
return content;
}
API client setup
// npm install @crowdin/crowdin-api-client
import crowdin, {
Credentials,
SourceFilesModel,
TranslationsModel,
} from "@crowdin/crowdin-api-client";
const credentials: Credentials = {
token: process.env.CROWDIN_PERSONAL_TOKEN!,
};
const {
sourceFilesApi,
translationsApi,
sourceStringsApi,
glossariesApi,
uploadStorageApi,
projectsGroupsApi,
translationStatusApi,
} = new crowdin.default(credentials);
const PROJECT_ID = 12345;
Key Techniques
Uploading source files via API
async function uploadSourceFile(
fileName: string,
content: string
): Promise<number> {
// Step 1: Upload to storage
const storage = await uploadStorageApi.addStorage(fileName, content);
const storageId = storage.data.id;
// Step 2: Check if file exists already
const existingFiles = await sourceFilesApi.listProjectFiles(PROJECT_ID);
const existing = existingFiles.data.find(
(f) => f.data.name === fileName
);
if (existing) {
// Update existing file
await sourceFilesApi.updateOrRestoreFile(PROJECT_ID, existing.data.id, {
storageId,
updateOption:
SourceFilesModel.UpdateOption.KEEP_TRANSLATIONS_AND_APPROVALS,
});
return existing.data.id;
} else {
// Create new file
const created = await sourceFilesApi.createFile(PROJECT_ID, {
storageId,
name: fileName,
type: "json",
});
return created.data.id;
}
}
Downloading translations programmatically
async function downloadTranslations(lang: string): Promise<Buffer> {
// Build project translations
const build = await translationsApi.buildProject(PROJECT_ID, {
targetLanguageIds: [lang],
});
const buildId = build.data.id;
// Poll build status
let status: string;
do {
const progress = await translationsApi.checkBuildStatus(
PROJECT_ID,
buildId
);
status = progress.data.status;
if (status === "inProgress") {
await new Promise((resolve) => setTimeout(resolve, 2000));
}
} while (status === "inProgress");
// Download the built translations
const download = await translationsApi.downloadTranslations(
PROJECT_ID,
buildId
);
const response = await fetch(download.data.url);
return Buffer.from(await response.arrayBuffer());
}
Managing glossaries
async function setupGlossary(): Promise<void> {
// Create a glossary
const glossary = await glossariesApi.addGlossary({
name: "Product Terminology",
languageId: "en",
});
const glossaryId = glossary.data.id;
// Add terms
const terms = [
{ term: "workspace", description: "A shared project environment" },
{ term: "pipeline", description: "An automated build/deploy sequence" },
{ term: "artifact", description: "A build output file or package" },
];
for (const { term, description } of terms) {
await glossariesApi.createGlossaryTerm(glossaryId, {
languageId: "en",
text: term,
description,
});
}
// Assign glossary to project
// Done via project settings in Crowdin UI or API patch
}
async function searchGlossary(
glossaryId: number,
query: string
): Promise<string[]> {
const result = await glossariesApi.searchGlossaryTerms(glossaryId, {
languageId: "en",
query,
});
return result.data.map((entry) => entry.data.text);
}
CLI workflow for CI/CD
// scripts/sync-translations.ts
import { execSync } from "child_process";
function syncTranslations(mode: "upload" | "download" | "both"): void {
if (mode === "upload" || mode === "both") {
console.log("Uploading sources to Crowdin...");
execSync("crowdin upload sources --no-progress", { stdio: "inherit" });
}
if (mode === "download" || mode === "both") {
console.log("Downloading translations from Crowdin...");
execSync("crowdin download --no-progress", { stdio: "inherit" });
}
}
// GitHub Actions integration example:
// - name: Crowdin Sync
// uses: crowdin/github-action@v2
// with:
// upload_sources: true
// download_translations: true
// localization_branch_name: l10n_main
// create_pull_request: true
// pull_request_title: "chore(i18n): sync translations from Crowdin"
// env:
// CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
// CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
Source strings management
async function addSourceString(
fileId: number,
key: string,
text: string,
context?: string
): Promise<number> {
const result = await sourceStringsApi.addSourceString(PROJECT_ID, {
fileId,
identifier: key,
text,
context: context ?? "",
maxLength: 0,
});
return result.data.id;
}
async function getTranslationStatus(
lang: string
): Promise<{ phrases: number; translated: number; approved: number }> {
const progress = await translationStatusApi.getProjectProgress(PROJECT_ID, {
languageIds: lang,
});
const langData = progress.data[0]?.data;
return {
phrases: langData?.phrases?.total ?? 0,
translated: langData?.phrases?.translated ?? 0,
approved: langData?.phrases?.approved ?? 0,
};
}
In-context editing setup
// In-context editing lets translators edit strings directly on your site.
// It works by replacing real translations with pseudo-translations containing metadata.
// crowdin.yml addition:
// in_context:
// pseudo_language: "ach" // Acholi is used as the pseudo locale
// Runtime setup for Next.js
function getCrowdinInContextScript(): string {
return `
var _jipt = [];
_jipt.push(['project', 'your-project-identifier']);
_jipt.push(['escape', function() { window.location.reload(); }]);
`;
}
// Add to layout when in-context mode is active
// <script dangerouslySetInnerHTML={{ __html: getCrowdinInContextScript() }} />
// <script src="https://cdn.crowdin.com/jipt/jipt.js" />
// Detect in-context mode
function isInContextMode(): boolean {
return typeof window !== "undefined" &&
new URLSearchParams(window.location.search).has("crowdin");
}
Best Practices
- Use
crowdin.ymlat the repo root and commit it — it documents your file mapping for the whole team. - Set
updateOption: KEEP_TRANSLATIONS_AND_APPROVALSwhen updating source files to preserve existing translator work. - Use the GitHub integration to auto-create PRs with new translations on every Crowdin build.
- Create glossaries before starting translations to enforce consistent terminology across languages.
- Use OTA content delivery for mobile or SPA apps where you want translation hotfixes without a full redeploy.
- Set
contexton source strings so translators see where each string appears, reducing ambiguity. - Use Crowdin's branching feature to isolate translations for feature branches and merge them when the code merges.
- Run
crowdin upload sourcesin CI on every push to main to keep Crowdin in sync automatically. - Use translation memory across projects to avoid paying for re-translation of identical strings.
- Set
maxLengthon strings that appear in space-constrained UI elements so translators respect length limits.
Anti-Patterns
- Do not commit the
CROWDIN_PERSONAL_TOKENincrowdin.yml— useapi_token_envto reference an environment variable. - Avoid manually editing downloaded translation files — changes will be overwritten on the next sync.
- Do not upload translations as sources — keep a clear separation between your source language and translated outputs.
- Avoid using Crowdin's file-based approach for databases or CMS content; use the string-based API instead.
- Do not skip the build step before downloading; unbundled translations may be stale or incomplete.
- Avoid creating one massive JSON file with all strings; split by feature or page for parallel translator work.
- Do not ignore Crowdin's QA checks (missing placeholders, length violations) — they catch real bugs before release.
- Avoid polling build status without a delay — add a backoff to prevent API rate limiting.
Install this skill directly: skilldb add i18n-services-skills
Related Skills
I18next
"i18next: internationalization framework, React (react-i18next), namespaces, interpolation, plurals, backends, language detection"
Lokalise
"Lokalise: translation management, API, SDKs, OTA updates, branching, screenshots, plural forms, CLI"
Next Intl
"next-intl: Next.js internationalization, message catalogs, routing, middleware, server components, formatters, pluralization"
Phrase
"Phrase (formerly PhraseApp): translation management, API, CLI, OTA, in-context editor, branching, webhooks, glossaries"
Pontoon
"Mozilla Pontoon: open-source translation management, Fluent/FTL support, in-place editing, community localization, VCS sync"
Transifex
"Transifex: translation management, Native SDK, OTA delivery, API v3, CLI, GitHub/GitLab integration, ICU plurals"