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:
- Retrieve project metadata to get available languages.
- List all files in the Crowdin project.
- For each file, retrieve all source strings (once per file).
- For each string, iterate through target languages.
- 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
Authentication
All API requests require a Personal Access Token:
Authorization: Bearer YOUR_API_TOKEN
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:
#!/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'],
);
}
}
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
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
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:
- Verify that translations are complete and accurate.
- Check for placeholder consistency (e.g.,
%s,{variable}). - Validate that translations do not contain English source text.
- Review automatic translations from machine translation services.
Security considerations
Warning
Mass approval bypasses the normal translation review workflow. Use with caution and ensure translations are validated before approval.
- 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:
- Navigate to the project in Crowdin.
- Select a language.
- Open the editor in side-by-side view.
- Use the bulk actions menu to approve all strings for the current language.
- Alternatively, use batch operations to selectively approve multiple translations.
See also
- Integrate Crowdin in your extension — Integrate extensions with Crowdin
- Workflow: From new Crowdin translations to the TYPO3 installation — Complete Crowdin translation workflow
- Crowdin API Documentation