# SiteVerdict — Translation QA & AI Translator (JSON, PO, XLIFF) (`siteverdict/siteverdict-translation-qa`) Actor

QA your i18n files before release: missing keys, broken placeholders, ICU plurals, HTML tags, length limits, glossary, tone. JSON, PO, XLIFF, CSV. No key needed for checks; translate gaps with your own DeepL, OpenAI or Anthropic key. HTML report and CI gate.

- **URL**: https://apify.com/siteverdict/siteverdict-translation-qa.md
- **Developed by:** [Aman Thakran](https://apify.com/siteverdict) (community)
- **Categories:** Developer tools, AI
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Translation QA & AI Translator: catch broken translations before your users do

**Point it at your localisation files and get every problem that would break or embarrass a release**: missing keys, broken `{placeholders}`, invalid ICU plurals, lost HTML tags, strings too long for the button, glossary and brand-term violations, and the wrong *du/Sie* or *tu/vous*. Supports **JSON (nested, flat, ARB), gettext PO, XLIFF 1.2/2.0 and CSV**.

- **No API key needed for QA.** All checks are deterministic.
- **Optional, on your own key:** fill missing strings with **DeepL, OpenAI or Anthropic** (placeholders are protected and verified), and get an **AI review** (OpenAI, Anthropic, Google Gemini or Mistral) that flags mistranslations and tone problems with suggested fixes. Translation and AI review need **your own provider API key**; without a key only the deterministic checks run.

### Who it is for

- **Developers / CI:** run before every release and set `failOnErrors` to block a deploy that would ship `Hallo {nom}`.
- **Localisation managers:** one HTML report across all languages and files, with a quality score per locale.
- **Agencies and freelancers:** QA vendor deliveries against your glossary and style guide in minutes.

### Before and after (from the default example input)

| Severity | Locale | Key | Check | Problem |
|---|---|---|---|---|
| error | de | greeting | placeholder_mismatch | Missing: {name}. Unexpected: {nom}. |
| error | de | cart.items | icu_plural_other | Plural "{count}" has no "other" branch. |
| error | de | cart.checkout | max_length | Translation is 28 characters; limit is 20. |
| error | de | cart.title | forbidden_term | Forbidden term "Einkaufswagen" used (use "Warenkorb"). |
| warning | de | cart.title | tone_formality | Informal address found; the de style guide asks for formal. |
| warning | de | help | url_mismatch | https://example.com/help changed or missing. |
| error | de | save | missing_translation | No translation for this key. |
| warning | de | old | extra_key | Key exists in the translation but not in the source. |

Output: one dataset row per issue, plus `REPORT.html` (summary per file and locale with scores, then all issues), `ISSUES.csv` and an `OUTPUT` JSON summary. In translate modes you also get the updated files (`translated-<locale>-<file>`) in their original format.

### Checks

| Check | What it catches |
|---|---|
| `missing_translation`, `empty_translation`, `extra_key` | Gaps and obsolete keys between source and target |
| `untranslated` | Target identical to source (with an allowlist for brand names) |
| `placeholder_mismatch` | `{name}`, `{{name}}`, `%s`, `%1$d`, `%(name)s`, `${name}` lost, added or renamed |
| `icu_syntax`, `icu_plural_other`, `icu_plural_categories` | Broken ICU braces; plural branches missing for the target language (CLDR rules, e.g. Polish needs one/few/many/other) |
| `markup_mismatch` | HTML/XML tags lost or added |
| `max_length`, `length_ratio` | Hard limits (rules by key pattern, XLIFF `maxwidth`) and "too long for UI" warnings |
| `whitespace`, `newlines`, `end_punctuation`, `double_space` | Formatting drift |
| `number_mismatch`, `url_mismatch` | Changed numbers, links or e-mail addresses |
| `glossary_term`, `forbidden_term`, `do_not_translate` | Terminology and brand rules per locale |
| `tone_formality` | du/Sie, tu/vous, tú/usted, tu/Lei, jij/u, tu/você (heuristic) |
| `inconsistent` | The same source translated differently in one locale |
| `needs_review` | PO `fuzzy` and XLIFF needs-review states |
| `ai_review` (optional) | Mistranslation, omission, tone and terminology, with a suggestion |

### Pricing

Pay per event:

- **`string-checked`: $0.0002** per string per locale checked (deterministic QA)
- **`string-translated`: $0.001** per string translated with your key (successful translations only)
- **`string-ai-reviewed`: $0.0005** per string reviewed by your LLM

Examples:

- 2,000 strings × 5 languages QA = **$2.00**
- filling 300 missing strings = $0.30
- AI review of 1,000 strings = $0.50

Your DeepL, OpenAI, Anthropic, Google or Mistral usage is billed by them on your key.

**Maximum total charge:** the Actor reserves money for every string before processing it (check + translation + AI review, as needed). Strings that would not fit in your run's maximum total charge are **not processed and not charged**: the dataset gets a `not_processed` row per file/locale and the run's status message ends with "N strings not processed (max total charge reached)". A budget that is used up exactly is not reported as stopped.

### FAQ

**How do I give it my files?** Use public or pre-signed URLs, paste the content, or upload to an Apify key-value store and reference `store/key`. For JSON, mark the source file with `"role": "source"` or name it after the source locale (`en.json`).

**Will machine translation break my placeholders?** We swap placeholders and tags for protected tokens before translating, restore them afterwards, and **reject** any translation that lost one. Rejected strings keep their old value and are reported as `translation_failed`.

**How reliable is the tone check?** It is a pronoun-based heuristic, good for spotting the wrong form of address. Use `aiReview` for nuanced style feedback.

### Limits

- Up to 100 files and 100,000 strings per run, 20 MB per file.
- XLIFF inline markup is kept verbatim. Exotic XLIFF extensions are passed through but not interpreted.
- Tone checks cover de, fr, es, it, nl and pt only.

### Sources & compliance

- Works only on files you supply. No web scraping. Files from URLs are fetched with an SSRF guard (private and internal addresses blocked) and a size cap.
- Plural rules come from your runtime's CLDR data (Intl.PluralRules).
- **Providers:** DeepL ([terms](https://www.deepl.com/pro-license)), OpenAI ([terms](https://openai.com/policies/terms-of-use)), Anthropic ([terms](https://www.anthropic.com/legal/commercial-terms)), Google Gemini ([terms](https://ai.google.dev/gemini-api/terms)) and Mistral ([terms](https://mistral.ai/terms)) process strings under *your* account and key. Terms links checked 11 Oct 2026 (Google/Mistral links added 3 Nov 2026, not re-reviewed). DeepL Free keys (`…:fx`) use the free endpoint automatically.
- **Hosts called:** only the provider you choose — `api.deepl.com` / `api-free.deepl.com` (DeepL), `api.openai.com`, `api.anthropic.com`, `generativelanguage.googleapis.com` (Gemini, review only) and `api.mistral.ai` (review only) — plus the file URLs you supply. With no translation mode and `aiReview` off (no key), no provider is called.
- **Secrets:** API keys are secret inputs, sent only to the chosen provider and scrubbed from logs, errors and output.
- **Personal data:** UI strings rarely contain personal data. Don't put customer data into translation files you send to third-party MT providers. Not legal advice.

Built by SiteVerdict (BharatFare Travel and Technologies Ltd, UK). Contact: hello@webrevenuescan.com.

# Changelog

This Actor's version history is a separate document: https://apify.com/siteverdict/siteverdict-translation-qa/changelog.md

# Actor input Schema

## `files` (type: `array`):

Localisation files as objects: {"url": "https://…/de.json"} or {"name": "de.json", "content": "…"} or {"keyValueStoreRecord": "store/key"}. Optional "locale" and "role" ("source" or "target"). Formats: JSON (nested/flat, ARB), gettext PO, XLIFF 1.2/2.0, CSV (key + locale columns or key,source,target). For JSON give one source file plus target files.

## `sourceLocale` (type: `string`):

Locale of the source strings (e.g. en, en-GB).

## `mode` (type: `string`):

qa = checks only (no key needed). translate-missing = fill missing/empty translations with your MT key, then QA. translate-all = re-translate every string.

## `targetLocales` (type: `array`):

New locales to create from the source (JSON/CSV), e.g. fr, es, pt-BR.

## `translationProvider` (type: `string`):

Your own DeepL, OpenAI or Anthropic key. Placeholders and markup are protected and verified after translation.

## `translationApiKey` (type: `string`):

Your provider key (DeepL Free keys end with :fx). Never logged or output.

## `translationModel` (type: `string`):

For OpenAI/Anthropic, e.g. gpt-4.1-mini or claude-sonnet-5-5.

## `aiReview` (type: `boolean`):

Ask an LLM (your key) to flag mistranslations, omissions, wrong tone and terminology, with suggested fixes.

## `reviewProvider` (type: `string`):

Provider for AI review.

## `reviewApiKey` (type: `string`):

Your LLM key for AI review (reuses the translation key when the provider is the same).

## `reviewModel` (type: `string`):

Model for AI review.

## `glossary` (type: `array`):

Terminology rules: {"source": "cart", "target": "Warenkorb", "locale": "de"}, {"source": "SiteVerdict", "doNotTranslate": true}, {"source": "Einkaufswagen", "forbidden": true, "locale": "de"}.

## `formality` (type: `object`):

e.g. {"de": "formal", "fr": "formal", "es": "informal"}. Checked heuristically for de/fr/es/it/nl/pt and passed to DeepL/LLMs when translating.

## `styleNotes` (type: `string`):

Brand voice and rules for LLM translation/review (e.g. 'friendly, concise, UK spelling').

## `maxLengthRatio` (type: `number`):

Warn when a translation is this many times longer than the source.

## `maxLengthRules` (type: `array`):

e.g. \[{"keyPattern": "button.\*", "max": 20}]. XLIFF maxwidth is applied automatically.

## `disabledChecks` (type: `array`):

Check ids to skip, e.g. end_punctuation, double_space, inconsistent, length_ratio.

## `untranslatedAllowlist` (type: `array`):

Strings that may stay identical to the source (brand names, 'OK', 'Email').

## `failOnErrors` (type: `boolean`):

Mark the run as FAILED if any error-level issue is found: handy as a CI gate before release.

## `maxStrings` (type: `integer`):

Safety cap on strings processed per run (all files and locales).

## Actor input object example

```json
{
  "files": [
    {
      "name": "en.json",
      "role": "source",
      "content": "{\"cart\":{\"title\":\"Your cart\",\"items\":\"You have {count, plural, one {# item} other {# items}}\",\"checkout\":\"Checkout\"},\"greeting\":\"Hello {name}!\",\"help\":\"See https://example.com/help\",\"save\":\"Save\"}"
    },
    {
      "name": "de.json",
      "content": "{\"cart\":{\"title\":\"Dein Einkaufswagen\",\"items\":\"Sie haben {count, plural, one {# Artikel}}\",\"checkout\":\"Zur Kasse gehen und bezahlen\"},\"greeting\":\"Hallo {nom}!\",\"help\":\"Siehe https://example.com/hilfe\",\"old\":\"Alt\"}"
    }
  ],
  "sourceLocale": "en",
  "mode": "qa",
  "translationProvider": "deepl",
  "aiReview": false,
  "reviewProvider": "openai",
  "glossary": [
    {
      "source": "cart",
      "target": "Warenkorb",
      "locale": "de"
    },
    {
      "source": "Einkaufswagen",
      "forbidden": true,
      "target": "Warenkorb",
      "locale": "de"
    }
  ],
  "formality": {
    "de": "formal"
  },
  "maxLengthRatio": 2,
  "failOnErrors": false,
  "maxStrings": 100000
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `output` (type: `string`):

No description

## `files` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "files": [
        {
            "name": "en.json",
            "role": "source",
            "content": "{\"cart\":{\"title\":\"Your cart\",\"items\":\"You have {count, plural, one {# item} other {# items}}\",\"checkout\":\"Checkout\"},\"greeting\":\"Hello {name}!\",\"help\":\"See https://example.com/help\",\"save\":\"Save\"}"
        },
        {
            "name": "de.json",
            "content": "{\"cart\":{\"title\":\"Dein Einkaufswagen\",\"items\":\"Sie haben {count, plural, one {# Artikel}}\",\"checkout\":\"Zur Kasse gehen und bezahlen\"},\"greeting\":\"Hallo {nom}!\",\"help\":\"Siehe https://example.com/hilfe\",\"old\":\"Alt\"}"
        }
    ],
    "glossary": [
        {
            "source": "cart",
            "target": "Warenkorb",
            "locale": "de"
        },
        {
            "source": "Einkaufswagen",
            "forbidden": true,
            "target": "Warenkorb",
            "locale": "de"
        }
    ],
    "formality": {
        "de": "formal"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("siteverdict/siteverdict-translation-qa").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "files": [
        {
            "name": "en.json",
            "role": "source",
            "content": "{\"cart\":{\"title\":\"Your cart\",\"items\":\"You have {count, plural, one {# item} other {# items}}\",\"checkout\":\"Checkout\"},\"greeting\":\"Hello {name}!\",\"help\":\"See https://example.com/help\",\"save\":\"Save\"}",
        },
        {
            "name": "de.json",
            "content": "{\"cart\":{\"title\":\"Dein Einkaufswagen\",\"items\":\"Sie haben {count, plural, one {# Artikel}}\",\"checkout\":\"Zur Kasse gehen und bezahlen\"},\"greeting\":\"Hallo {nom}!\",\"help\":\"Siehe https://example.com/hilfe\",\"old\":\"Alt\"}",
        },
    ],
    "glossary": [
        {
            "source": "cart",
            "target": "Warenkorb",
            "locale": "de",
        },
        {
            "source": "Einkaufswagen",
            "forbidden": True,
            "target": "Warenkorb",
            "locale": "de",
        },
    ],
    "formality": { "de": "formal" },
}

# Run the Actor and wait for it to finish
run = client.actor("siteverdict/siteverdict-translation-qa").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "files": [
    {
      "name": "en.json",
      "role": "source",
      "content": "{\\"cart\\":{\\"title\\":\\"Your cart\\",\\"items\\":\\"You have {count, plural, one {# item} other {# items}}\\",\\"checkout\\":\\"Checkout\\"},\\"greeting\\":\\"Hello {name}!\\",\\"help\\":\\"See https://example.com/help\\",\\"save\\":\\"Save\\"}"
    },
    {
      "name": "de.json",
      "content": "{\\"cart\\":{\\"title\\":\\"Dein Einkaufswagen\\",\\"items\\":\\"Sie haben {count, plural, one {# Artikel}}\\",\\"checkout\\":\\"Zur Kasse gehen und bezahlen\\"},\\"greeting\\":\\"Hallo {nom}!\\",\\"help\\":\\"Siehe https://example.com/hilfe\\",\\"old\\":\\"Alt\\"}"
    }
  ],
  "glossary": [
    {
      "source": "cart",
      "target": "Warenkorb",
      "locale": "de"
    },
    {
      "source": "Einkaufswagen",
      "forbidden": true,
      "target": "Warenkorb",
      "locale": "de"
    }
  ],
  "formality": {
    "de": "formal"
  }
}' |
apify call siteverdict/siteverdict-translation-qa --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,siteverdict/siteverdict-translation-qa"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/pAv7XpKMHhN7ZEk04/builds/Bnrd1RFsG4fsMkQX5/openapi.json
