Skip to main content
Technology & EngineeringI18n Services319 lines

Crowdin

"Crowdin: translation management, OTA content delivery, API, CLI, GitHub integration, in-context editing, glossaries"

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Crowdin

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.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.

Anti-Patterns

  • 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.
  • 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

Get CLI access →