Mass approval on Crowdin 

After importing existing translations into Crowdin, they must be explicitly approved before becoming available for export.

This page provides a PHP implementation for programmatically approving translations via the Crowdin API.

Crowdin API workflow 

The mass approval process follows these steps:

  1. Retrieve project metadata to get available languages.
  2. List all files in the Crowdin project.
  3. For each file, retrieve all source strings (once per file).
  4. For each string, iterate through target languages.
  5. Approve unapproved translations using the Approvals API.

API endpoints 

GET  /api/v2/projects/{projectId}
GET  /api/v2/projects/{projectId}/files
GET  /api/v2/projects/{projectId}/strings?fileId={fileId}
GET  /api/v2/projects/{projectId}/translations?stringId={stringId}&languageId={languageId}
POST /api/v2/projects/{projectId}/approvals
Copied!

Authentication 

All API requests require a Personal Access Token:

Authorization: Bearer YOUR_API_TOKEN
Copied!

Generate tokens at: https://crowdin.com/settings#api-key

PHP implementation 

New in version 13.0

The following script requires PHP 8.4+ and uses modern features: readonly classes, constructor property promotion, arrow functions, and enums.

The following script demonstrates mass approval using the Crowdin API v2:

crowdin_mass_approve.php
#!/usr/bin/env php
<?php

declare(strict_types=1);

/**
 * Crowdin Mass Approval Script
 *
 * Approves all unapproved translations in a Crowdin project.
 * Requires PHP 8.4+
 */
const API_BASE = 'https://api.crowdin.com/api/v2';
const ENDPOINTS = [
    'project'      => '/projects/%d',
    'files'        => '/projects/%d/files?limit=500',
    'strings'      => '/projects/%d/strings?fileId=%d&limit=500',
    'translations' => '/projects/%d/translations?stringId=%d&languageId=%s&limit=500',
    'approvals'    => '/projects/%d/approvals',
];

class CrowdinClient
{
    public function __construct(
        private string $token,
        private int $projectId,
    ) {}

    private function request(
        string $endpoint,
        string $method = 'GET',
        ?array $data = null,
    ): array {
        $ch = curl_init(API_BASE . $endpoint);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                "Authorization: Bearer {$this->token}",
                'Content-Type: application/json',
            ],
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_POSTFIELDS => $data ? json_encode($data) : null,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($response === false || $httpCode >= 400) {
            if ($httpCode >= 400) {
                fprintf(STDERR, "ERROR: HTTP %d\n%s\n", $httpCode, $response);
            }
            return [];
        }

        return json_decode($response, true) ?? [];
    }

    private function endpoint(string $key, mixed ...$args): string
    {
        return sprintf(ENDPOINTS[$key], $this->projectId, ...$args);
    }

    public function getLanguages(): array
    {
        $project = $this->request($this->endpoint('project'));
        return array_column($project['data']['targetLanguages'] ?? [], 'id');
    }

    public function getFiles(): array
    {
        $response = $this->request($this->endpoint('files'));
        return array_column(
            array_column($response['data'] ?? [], 'data'),
            'id',
        );
    }

    public function getStrings(int $fileId): array
    {
        $response = $this->request($this->endpoint('strings', $fileId));
        return array_column(
            array_column($response['data'] ?? [], 'data'),
            'id',
        );
    }

    public function getTranslations(int $stringId, string $lang): array
    {
        $response = $this->request($this->endpoint('translations', $stringId, $lang));
        return array_map(
            fn($item) => [
                'id' => $item['data']['id'],
                'approved' => !empty($item['data']['approvals']),
            ],
            $response['data'] ?? [],
        );
    }

    public function approve(int $translationId): bool
    {
        $response = $this->request(
            $this->endpoint('approvals'),
            'POST',
            ['translationId' => $translationId],
        );
        return !empty($response);
    }
}

// --- Main ---

$token = getenv('CROWDIN_TOKEN')
    ?: exit("ERROR: CROWDIN_TOKEN environment variable not set\n");

$projectId = (int)($argv[1]
    ?? exit("Usage: php crowdin_mass_approve.php <project-id> [language-id]\n"));

$filterLang = $argv[2] ?? null;

$client = new CrowdinClient($token, $projectId);

echo "\n=== Crowdin Mass Approval ===\n";
echo "Project ID: {$projectId}\n";
if ($filterLang) {
    echo "Language filter: {$filterLang}\n";
}

// Step 1: Get languages
echo "\nStep 1: Getting languages...\n";
$languages = $filterLang ? [$filterLang] : $client->getLanguages();
if (empty($languages)) {
    exit("ERROR: No languages found\n");
}
printf("Found %d language(s): %s\n", count($languages), implode(', ', $languages));

// Step 2: Get files
echo "\nStep 2: Getting files...\n";
$files = $client->getFiles();
if (empty($files)) {
    exit("ERROR: No files found\n");
}
printf("Found %d file(s)\n", count($files));

// Step 3: Process translations
echo "\nStep 3: Processing translations...\n";
echo "Legend: F=file, S=string, L=translation\n\n";

$stats = ['approved' => 0, 'skipped' => 0, 'errors' => 0];
$errors = [];

foreach ($files as $fileId) {
    echo 'F';
    foreach ($client->getStrings($fileId) as $stringId) {
        echo 'S';
        foreach ($languages as $lang) {
            foreach ($client->getTranslations($stringId, $lang) as $translation) {
                echo 'L';
                if ($translation['approved']) {
                    $stats['skipped']++;
                    continue;
                }

                if ($client->approve($translation['id'])) {
                    $stats['approved']++;
                } else {
                    $stats['errors']++;
                    $errors[] = [
                        'file' => $fileId,
                        'string' => $stringId,
                        'lang' => $lang,
                        'translation' => $translation['id'],
                    ];
                }
            }
        }
    }
}

// Summary
printf(
    "\n\n=== Summary ===\nApproved: %d | Skipped: %d | Errors: %d\n",
    $stats['approved'],
    $stats['skipped'],
    $stats['errors'],
);

if ($errors) {
    echo "\n=== Failed Approvals ===\n";
    foreach ($errors as $e) {
        printf(
            "  File: %d, String: %d, Lang: %s, Translation: %d\n",
            $e['file'],
            $e['string'],
            $e['lang'],
            $e['translation'],
        );
    }
}
Copied!

Usage 

# Set API token (leading space prevents saving to shell history)
 export CROWDIN_TOKEN="your_api_token_here"

# Approve all translations in the project
php crowdin_mass_approve.php 12345

# Approve translations for a specific language only
php crowdin_mass_approve.php 12345 de
Copied!

Progress indicators 

During execution, the script outputs progress indicators:

  • F - Processing a new file
  • S - Processing a new string within the file
  • L - Processing a translation (language) for the string

Example output:

Processing: de, fr, es
FSLLLSLLLFSLLSLLLFSLLLL...

=== Summary ===
Approved: 234 | Skipped: 56 | Errors: 0
Copied!

Best practices 

Error handling 

The script includes robust error handling:

  • Errors are written to STDERR (not STDOUT).
  • Failed approvals are logged with full context (file, string, language, translation IDs).
  • Empty API responses are handled gracefully (returns an empty array).
  • Network failures are caught and reported.

Validation 

Before mass approval:

  1. Verify that translations are complete and accurate.
  2. Check for placeholder consistency (e.g., %s, {variable}).
  3. Validate that translations do not contain English source text.
  4. Review automatic translations from machine translation services.

Security considerations 

  • API token security: Store API tokens securely - never commit them to version control. Use environment variables.
  • Access control: Use tokens with the minimal required permissions.
  • Audit logging: The script logs all failed approval operations with full context for accountability.
  • Quality assurance: Implement pre-approval validation to prevent approval of invalid translations.

Alternatives 

Crowdin web interface 

For smaller projects, manual approval via Crowdin's web interface may be more appropriate:

  1. Navigate to the project in Crowdin.
  2. Select a language.
  3. Open the editor in side-by-side view.
  4. Use the bulk actions menu to approve all strings for the current language.
  5. Alternatively, use batch operations to selectively approve multiple translations.

See also