This guide uploads an i18next project whose translation files live at public/locales/<locale>/<namespace>.json, polls the sync job, downloads the artifact, and applies changed files back to the repository.
Use this alongside the Custom Pipeline API reference for endpoint details, permissions, response fields, and error codes.
The examples use curl, jq, unzip, and rsync. These are commonly available on macOS and many Linux CI images; install them in slim containers, or replace jq and rsync with equivalent JSON parsing and file-copy steps in your own pipeline.
Set API Details
Create a custom project and scoped API token in String Catalog, then expose these variables to your shell or CI job:
export STRINGCATALOG_BASE_URL="https://stringcatalog.com/api/v1"
export STRINGCATALOG_TOKEN="your_api_token"
export STRINGCATALOG_PROJECT_ID="prj_01hzy3n9v6q8k2m5r7t9x0c4ab"
# GitHub Actions: use GITHUB_SHA instead of CI_COMMIT_SHA.
export STRINGCATALOG_IDEMPOTENCY_KEY="i18next-${CI_COMMIT_SHA:-$(git rev-parse --short HEAD)}"
Build The Manifest
Build the manifest and file upload arguments from the same sorted file list. The order matters because manifest.files and files[] are matched by array index.
MANIFEST="$(
find public/locales -type f -name '*.json' | sort \
| jq -R '{path: ., format: "i18next_json"}' \
| jq -s '{files: .}'
)"
file_args=()
while IFS= read -r locale_file; do
file_args+=(-F "files[]=@${locale_file};type=application/json")
done < <(find public/locales -type f -name '*.json' | sort)
The loop uses locale_file instead of path because path is a special variable in zsh.
Bootstrap From Dashboard Languages
If you added target languages in the dashboard and need their files in your repository, create an export once, poll it, download the ZIP, and copy files/ over your checkout. Do not run exports on every CI build; use the sync job below for the normal per-commit loop.
curl -sS --fail-with-body \
-X POST "$STRINGCATALOG_BASE_URL/projects/$STRINGCATALOG_PROJECT_ID/exports" \
-H "Authorization: Bearer $STRINGCATALOG_TOKEN" \
-H "Accept: application/json" \
-H "Idempotency-Key: bootstrap-${CI_COMMIT_SHA:-$(git rev-parse --short HEAD)}" \
| tee /tmp/stringcatalog-export-create.json | jq .
Then poll the export until it finishes. Exports omit artifact_safe_to_apply; download when status is succeeded and download_url is present.
STRINGCATALOG_EXPORT_ID="$(jq -r '.id' /tmp/stringcatalog-export-create.json)"
while true; do
curl -sS --fail-with-body \
"$STRINGCATALOG_BASE_URL/exports/$STRINGCATALOG_EXPORT_ID" \
-H "Authorization: Bearer $STRINGCATALOG_TOKEN" \
-H "Accept: application/json" \
| tee /tmp/stringcatalog-export.json | jq .
export_status="$(jq -r '.status' /tmp/stringcatalog-export.json)"
if [[ "$export_status" != "queued" && "$export_status" != "running" ]]; then
break
fi
sleep 5
done
if jq -e '.status == "succeeded" and .download_url != null' /tmp/stringcatalog-export.json >/dev/null; then
STRINGCATALOG_EXPORT_DOWNLOAD_URL="$(jq -r '.download_url' /tmp/stringcatalog-export.json)"
curl -sS --fail-with-body \
"$STRINGCATALOG_EXPORT_DOWNLOAD_URL" \
-H "Authorization: Bearer $STRINGCATALOG_TOKEN" \
-H "Accept: application/zip" \
--output /tmp/stringcatalog-export.zip
rm -rf /tmp/stringcatalog-export
mkdir -p /tmp/stringcatalog-export
unzip -q /tmp/stringcatalog-export.zip -d /tmp/stringcatalog-export
rsync -av /tmp/stringcatalog-export/files/ ./
fi
See the export endpoint reference for response fields, errors, and ZIP contents.
Create The Sync Job
curl -sS --fail-with-body -D /tmp/stringcatalog-create-headers.txt \
-X POST "$STRINGCATALOG_BASE_URL/projects/$STRINGCATALOG_PROJECT_ID/sync-jobs" \
-H "Authorization: Bearer $STRINGCATALOG_TOKEN" \
-H "Accept: application/json" \
-H "Idempotency-Key: $STRINGCATALOG_IDEMPOTENCY_KEY" \
-F "manifest=$MANIFEST" \
"${file_args[@]}" \
| tee /tmp/stringcatalog-create.json | jq .
If you need to retry this same upload after a timeout or lost response, reuse the same STRINGCATALOG_IDEMPOTENCY_KEY. The replayed response will include Idempotency-Replayed: true.
grep -i '^Idempotency-Replayed:' /tmp/stringcatalog-create-headers.txt
Poll The Job
Poll until the job reaches succeeded, failed, or expired:
STRINGCATALOG_JOB_ID="$(jq -r '.id' /tmp/stringcatalog-create.json)"
while true; do
curl -sS --fail-with-body \
"$STRINGCATALOG_BASE_URL/sync-jobs/$STRINGCATALOG_JOB_ID" \
-H "Authorization: Bearer $STRINGCATALOG_TOKEN" \
-H "Accept: application/json" \
| tee /tmp/stringcatalog-job.json | jq .
status="$(jq -r '.status' /tmp/stringcatalog-job.json)"
if [[ "$status" != "queued" && "$status" != "running" ]]; then
break
fi
sleep 5
done
If polling returns 429 Too Many Requests, wait for the Retry-After response header before trying again. For longer jobs, increase the sleep interval to reduce rate-limit pressure.
Download And Apply
Download and apply the artifact only when artifact_safe_to_apply is true:
if jq -e '.artifact_safe_to_apply == true and .download_url != null' /tmp/stringcatalog-job.json >/dev/null; then
STRINGCATALOG_DOWNLOAD_URL="$(jq -r '.download_url' /tmp/stringcatalog-job.json)"
curl -sS --fail-with-body \
"$STRINGCATALOG_DOWNLOAD_URL" \
-H "Authorization: Bearer $STRINGCATALOG_TOKEN" \
-H "Accept: application/zip" \
--output /tmp/stringcatalog-artifact.zip
rm -rf /tmp/stringcatalog-artifact
mkdir -p /tmp/stringcatalog-artifact
unzip -q /tmp/stringcatalog-artifact.zip -d /tmp/stringcatalog-artifact
rsync -av /tmp/stringcatalog-artifact/files/ ./
fi
The artifact stores changed files under files/ using the same relative paths you uploaded. For example, public/locales/es/common.json is written to files/public/locales/es/common.json, so copying files/ over your repository root applies the changes. If there are no changed files, the job can still succeed with an empty files/ directory.
The ZIP also includes:
| File | Description |
|---|---|
report.json |
Summary counts, changed paths, unchanged paths, skipped paths, and failed paths. |
warnings.json |
Non-fatal warnings encountered while processing the job. |
manifest.json |
The accepted manifest, including each uploaded file's sha256 and byte size. |
Handle Failures And Warnings
Before applying an artifact in CI, check the terminal status and file lists:
jq '{status, error, warnings, skipped_files, failed_files}' /tmp/stringcatalog-job.json
If status is failed, read error.message and failed_files first. Common fixes are correcting invalid JSON, unsupported file paths, missing source strings, or a token that does not have access to the project.
If error.message does not explain the failure, capture the X-Request-Id response header from the polling request and include it when contacting support.
If status is expired, the artifact has been pruned after the retention window. Create a new sync job with a fresh idempotency key.
If warnings is not empty, print them in CI so the team can see non-fatal issues:
jq -r '.warnings[]? | "- \(.file // "job"): \(.message)"' /tmp/stringcatalog-job.json
If skipped_files or failed_files is not empty, do not apply the artifact automatically. Inspect report.json and fix the listed files. Failed and expired jobs are terminal; to try again, create a new sync job with a fresh idempotency key.