Crowdin
Crowdin OÜ v1.8.0
Localize products with Crowdin
Competitor profile
Developer declarations; not independently tested. Listing last checked Nov 4, 2026. Package observed Nov 3, 2026.
Declared limitations
- Ad-hoc translation of pasted text is not a Crowdin project operation.
- Requests about other issue trackers or tools stay out of scope.
- Unrelated everyday questions must not trigger the app.
Not specified in the available declarations: capabilities, pricing & account requirements.
Description
From the marketplace listing
Crowdin connects ChatGPT and Codex to your own Crowdin localization projects. Everything runs inside your account, in projects you already have access to. What you can do: - Check translation progress by language - Find source strings and their translations - Look up glossary terms and translation memory - Review tasks, reports, labels, and comments - Make bounded changes: add labels, upload source files, start a translation build – the agent confirms before writing With the bundled Codex skills, you can also set up localization in a code repository, keep files in sync through the Crowdin CLI and GitHub Action, and write translator context and glossaries. Who it's for: Localization managers, product managers, and developers who already use Crowdin and want answers without switching tabs – how far along a language is, where a source string is used, whether a term is in the glossary, what a report says – or who want Codex to set up localization in a repo and keep it in sync.
Language: English · Automatically detected from descriptions.
Changes
No changes recorded yet. Follow this plugin to keep track of future changes.
First seen Oct 28, 2026. Last seen Nov 4, 2026.
Similar plugins
Find competitors to follow as you develop your plugin.
-
SourcePRD reads your codebase and writes the product story: the doors people come in through, the areas inside, and every capability — wh...
Shared terms: product, repository, inside, code
-
Smartling is a cloud-based translation management platform used by global brands to translate and manage localized content at scale. This...
Shared terms: glossarie, glossary, localization, manager
-
Manage clients, projects, tasks and time in your MonoDesk workspace.
Shared terms: comment, switching, task, add
-
Trussi AI connects your Trussi roofing CRM to ChatGPT so your team can run the business in plain language. Look things up: - Find and op...
Shared terms: comment, repo, upload, inside
-
Build and edit email campaigns in your organization's Better Email design system.
Shared terms: runs, upload, inside, already
Keywords
Files & skills
File archives
Skill instructions
context-extraction5.09 KB
---
name: context-extraction
description: Writes meaningful ai_context values in Crowdin JSONL files for strings that need translator context. Use when extracting context, filling ai_context fields, adding context to ambiguous strings, or working with Crowdin JSONL context files.
---
# Context Extraction
## Information sources (priority order)
For each string, use these JSONL fields in order - stop when you have enough to write a confident `ai_context`:
1. `text` - the source string itself; always the primary signal
2. `key` - often encodes structure (e.g. `button.save`, `modal.title.delete_user`, `error.network.timeout`)
3. `context` - may contain a free-text description, a translator comment, a source file reference, or be empty; treat as supplementary and don't assume any specific format
4. Source files - only if `context` contains a parseable file path + line number and the string is still ambiguous; read ±10–15 lines around the reference to identify UI element type, surrounding component, and props
## Strings that need context
Prioritize these - skip strings that are already clear from `text` + `key` alone:
- Ambiguous short words - single verbs, nouns, or adjectives that change meaning depending on UI placement
- Color / status names - words that name a color, state, or category; could be a label, badge, or filter
- ICU / message format strings - strings with plural forms or named parameters whose meaning depends on what's being counted or substituted
- Strings with inline tags - text containing markup tags where the role of the tagged portion isn't clear from the string alone
- Short phrases with unclear scope - brief imperative or standalone phrases that could belong to multiple UI contexts
## Writing good `ai_context` values
- 1–3 sentences, written for a translator - not a developer
- State the UI element type (button, label, tab, tooltip, modal title, dropdown option, etc.) and where it appears
- For plurals: what entity is counted, what `#` is replaced with
- For inline tags: what the tagged portion renders as (link, code, bold text, etc.)
- For color/status names: whether it's a selectable option, badge, filter label, etc.
- Avoid file names and variable names unless they clarify meaning
### Examples
| String | Key | Good `ai_context` |
|--------|-----|-------------------|
| `"Red"` | `color.red` | `"Color option label in a color picker. Refers to the color red as a selectable choice."` |
| `"Blue"` | *(none)* | `"Color name used as a selectable option or status label. Clarify based on surrounding UI."` |
| `"{count, plural, one {# month} other {# months}}"` | `duration.months` | `"Displays a duration in months. '#' is replaced by the numeric count."` |
| `"Edit <0>src/App.tsx</0> and save to test HMR"` | *(none)* | `"Instructional UI message. The tagged portion is rendered as an inline code element highlighting a filename."` |
| `"New"` | `button.new` | `"Label for a button that creates a new item. The exact entity depends on the current page context."` |
## Orchestrated inputs (optional)
When another workflow invokes this skill with extra context, use it:
- App domain - a one-line description of what the product is. Prefix or fold it into placement descriptions where it disambiguates ("Button label in a coffee-brew logging app…").
- Source checkout - when the JSONL `context` field carries a `#: path:line` reference (PO uploads add these automatically) and the repo is available, read the surrounding code instead of guessing: identify the component type, the props, and what any placeholder is replaced with.
Neither input changes the safety rules below - still edit only `ai_context`.
## JSONL file safety rules
**Only ever edit the `ai_context` field value.** Never touch `id`, `key`, `text`, `file`, or `context`.
### Editing procedure
Single-string (1–5 lines): Use StrReplace scoped to the exact line. Target the `"ai_context":""` substring (or the full current value if already set):
```
old: "ai_context":""
new: "ai_context":"Your context here."
```
Batch (many strings): When dozens or hundreds of lines need context, work in batches instead of one string at a time.
- Read a portion of the file (or the whole file if manageable), identify lines that need `ai_context`, fill context for a batch of those lines (parse line as JSON, set `ai_context`, serialize back to a single line), then write or apply that batch of changes. Repeat for the next batch until done.
- Prefer writing (or applying edits) in batches of lines rather than a single global read-then-write or hundreds of single-line replacements. Batch size is up to the implementer (e.g. tens or a hundred lines per batch).
- When batching, still apply the validity checklist below to a sample of changed lines before saving.
### Validity checklist (verify before saving)
- [ ] The line is still a single valid JSON object
- [ ] `ai_context` value is a quoted string
- [ ] No unescaped double quotes inside the value - use `\"`
- [ ] No literal newlines inside the value - keep it on one line
- [ ] No fields added, removed, or reordered
### Character escaping
- `"` → `\"`
- `\` → `\\`
- Newline → avoid; use a space instead
create-app25.1 KB
---
name: create-app
description: Build a Crowdin app end to end and leave the user with a published app they can click - scaffold it with @crowdin/serverless-apps-cli, write the UI with @crowdin/serverless-apps-sdk, publish it into their Crowdin or Crowdin Enterprise organization, and open it for them. Reach for this whenever someone wants something of their own inside the Crowdin UI - a panel in the translation editor, a tool or report in a project, a page in the organization menu, a dashboard over their projects - and also when they describe a localization chore they are tired of doing by hand. Covers phrasings like "create a Crowdin app", "add a tool to my Crowdin project", a bare "/create-app", and cases where the word "app" is never said. Also use it when a serverless Crowdin app already exists in the working directory and needs another module, a manifest change, or a republish. Not for the separate Crowdin CLI that syncs sources and translations through crowdin.yml.
---
# Create a Crowdin app
## Who is asking
A translator or a localization manager. They arrive with a chore they are sick of, not a specification: "show me how we translated this term before", "I want to see what the proofreader changed after me", "every Thursday I build this spreadsheet by hand".
They do not know what a module, a manifest, a scope, an editor mode or a project identifier is, and they do not know what this platform can and cannot do. They will not pre-check whether their idea is buildable, and they should not have to. **Every question you put to them must be answerable by someone who has never seen a line of code.** A question about module types, scopes, manifests, templates, editor modes, project ids or editions is a defect in this workflow, not a gap in the user.
When their request runs past what the platform allows, that is your problem to solve, not theirs to have avoided. See [when the platform says no](#when-the-platform-says-no).
## What you are actually building
A frontend-only React bundle that Crowdin loads into an iframe inside its own UI. There is no backend and no token in the app: the host executes Crowdin REST calls and keeps the app's persisted records ([Crowdin Storage](#when-the-app-needs-to-remember-something)) for you, under the session of whoever opened the app, limited to the scopes the manifest declares.
## Where the facts come from
The platform gains module types, manifest fields, SDK methods and scopes over time. This skill cannot keep up with that, and neither can your training data - so for **what exists**, fetch the docs. They ship a machine-readable variant of every page: append `.md` to any documentation URL, or take the whole set at once.
| Fetch this | For |
|---|---|
| [building-app/modules.md](https://crowdin.github.io/serverless-apps/building-app/modules.md) | The current module types and their `prepare*` functions |
| [reference/manifest.md](https://crowdin.github.io/serverless-apps/reference/manifest.md) | Manifest fields, module shapes, permissions |
| [reference/cli-commands.md](https://crowdin.github.io/serverless-apps/reference/cli-commands.md) | Commands and flags as released |
| [building-app/crowdin-api.md](https://crowdin.github.io/serverless-apps/building-app/crowdin-api.md) | The tokenless API client |
| [building-app/storage.md](https://crowdin.github.io/serverless-apps/building-app/storage.md) | Crowdin Storage - persisted shared, per-user and module-scoped records |
| [building-app/context.md](https://crowdin.github.io/serverless-apps/building-app/context.md) | Context, theme, events |
| [building-app/host-actions.md](https://crowdin.github.io/serverless-apps/building-app/host-actions.md) | Editor and host actions |
| [building-app/user-interface.md](https://crowdin.github.io/serverless-apps/building-app/user-interface.md) | The UI kit |
| [building-app/i18n.md](https://crowdin.github.io/serverless-apps/building-app/i18n.md) | Localization |
| [development/troubleshooting.md](https://crowdin.github.io/serverless-apps/development/troubleshooting.md) | Known failures |
| [understanding-scopes](https://support.crowdin.com/developer/understanding-scopes/) | The scope catalogue |
| [llms-full.txt](https://crowdin.github.io/serverless-apps/llms-full.txt) | Everything, one fetch |
The division of labour: **the docs are authoritative on what exists, this skill on what order to do it in and what breaks.** When a list here disagrees with the docs, the docs win and the list here is stale - reach for a capability the docs describe even if it is absent from these pages. When something works but breaks in a way the docs do not mention, that is what this skill is for.
## Where you must land
The finish line is not "code written" and not even "app published" - it is **the user looking at their working app**. Treat the run as unfinished until you have handed over a link you have reason to believe renders, or, for the placements that have no link, until they confirm they can see it.
## The one question worth asking first
Ask for a link:
> Could you send me a link to your Crowdin page - the project you work in, or just your home page?
Anyone can copy a URL out of their address bar, and it answers the two things you otherwise cannot derive:
- **Which edition.** Host `crowdin.com` means Crowdin; anything else (`<org>.crowdin.com`) means Crowdin Enterprise. This decides whether the placement they want even exists - six of the eleven openable module types live in exactly one edition.
- **Which project.** `crowdin.com/project/<identifier>` or `<org>.crowdin.com/u/projects/<id>` gives you the value `preview --project` needs, without ever asking for an "identifier".
If they send nothing, the CLI's own state file holds the edition after login: `~/.crowdin/serverless-apps-cli.json`, where a non-empty `domain` means Enterprise and its absence means crowdin.com.
## Settle four things before scaffolding
| Slot | Why it has to be settled early |
|---|---|
| **Name** | `create <name>` names the directory, registers the app, and labels the app and all its modules in the UI. Pass a short latin slug; if the user wants a human label, set `name` in the manifest afterwards and push it. |
| **Placement** | Where it appears in the Crowdin UI. The most expensive to get wrong, and constrained by edition ([modules.md](references/modules.md)). |
| **Logic** | What result they expect to see, which drives the UI, the API calls and therefore the `scopes`. |
| **Who sees it** | `default_permissions`. Its default is `owner` only, which is wrong for most requests and cannot be widened after the fact - see [visibility](#8-close-the-loop-on-visibility). |
## Ask sparingly, and only in their words
**Derive, do not ask:** who the user is, what data the app touches, the `scopes` ([scopes.md](references/scopes.md)), the edition and project (from their link), editor `modes` (declare **every mode the docs list** by default, so the panel is there wherever the user happens to be working - narrow it only if they ask, because a module is simply absent from any mode its manifest omits and the user has no way to know why), and a sensible app name. The template is fixed: always `projects-dashboard-cli`.
**State, do not ask:** put everything you derived into a short brief and let them correct it. Write it in the language they wrote to you in, and in plain words - "a panel on the right in the editor, next to the string", never "editor-right-panel, modes translate/review". A list of claims is easier to object to than a list of questions.
**Ask only when:**
1. **Placement genuinely does not follow from the request.** "in the editor next to the string" is settled. A bare "in the project" leaves a real choice between the Tools section and the Reports section - ask that, described as places, with your recommendation.
2. **Two implementations differ in what the user would see.** Not "how should I build it" but "is progress a chart per language, a table per file, or one number?".
3. **The platform forces a trade-off on them.** Covered below.
One question at a time, each with your recommended answer attached, so "yes" is a complete reply. If they do not know, take your recommendation and move on. **Zero questions is a perfectly good outcome.**
Then show the brief and wait for a go-ahead: `create` registers a real app in their organization and there is no `delete` command.
## When the app needs to remember something
Crowdin Storage is the app's own persisted key-value store, hosted by Crowdin per installation - so stored settings, notes that survive a reload, and state a team shares are all buildable, without a backend. It needs `application.storage` in the manifest `scopes`, and calls run like the REST proxy: as the current viewer, no tokens. The surface lives in [building-app/storage.md](https://crowdin.github.io/serverless-apps/building-app/storage.md); [references/sdk.md](references/sdk.md#keeping-data-crowdin-storage) has the verified snippets.
The design decision is not how to store but **who each record belongs to**, and the key prefix is what enforces the answer:
| The data is | Use | Because |
|---|---|---|
| One user's own - their filters, drafts, notes, a credential they entered | `kv.user.*` (the `user:` prefix) | The platform hides it: another user's records simply do not exist for the caller, in reads and listings alike |
| Owned by one of the app's surfaces - team state, integration settings, a credential of that surface's team | a key under `module:{moduleKey}:` | The platform serves it only where the app runs as that module, to viewers allowed there - a managers-only module's records never reach the app's other surfaces |
| Public to everyone the app reaches, or shared between the app's modules | a plain key | Nothing protects it: everyone who can use the app can read it - in a public project that includes anonymous visitors - and every signed-in viewer can **overwrite** it |
Default to `user:` for anything about a person and `module:` for anything one surface's team owns, because both stay access-controlled by the platform; a plain shared key is for values anyone the app is visible to may legitimately see - and, signed in, change - including data the app's modules must share, since `module:` records never cross surfaces. One person is exempt from module narrowing: the app's installer sees every module's records on any surface - only other users' `user:` records stay hidden from them. The order matters because the failure is silent: a plain key behaves perfectly while one person tests the app, and leaks or gets clobbered only when the second user arrives.
Two facts that shape designs: unauthenticated viewers have no user identity, so `kv.user.*` throws for them while shared keys keep working - check `user.id` in the context before offering per-user features; and uninstalling the app deletes every record immediately, with no retention.
## When the platform says no
Three things a frontend-only app genuinely cannot do:
- **Use a secret without revealing it.** There is no backend, so the code that uses a credential runs in the viewer's browser - a key baked into the bundle is readable by any viewer, and a storage record comes back decrypted to everyone its scope admits, `secret: true` or not. Storing one is fine exactly when everyone who can reach it may know it: a credential each user enters for themselves under `kv.user.*`, or a team credential for an integration under the `module:` prefix of a module only that team can use - `secret: true` in both cases. A key that must work for people who should not see it belongs in Crowdin's own integration settings, not in the app.
- **Be called from outside.** Webhooks, schedules, emails, and the platform extension points Crowdin invokes over HTTP (`custom-mt`, `custom-file-format`, `ai-provider`, `external-qa-check` and friends - not even valid module types here). A closed tab is listening to nothing.
- **Outlive the tab.** A few megabytes of file processing in the browser is fine; hundreds are not.
**A no is never the end of the run.** The user came with a real chore and cannot act on "this needs a backend". Deliver the part that works and name the trade-off in one plain sentence:
| They asked for | Give them |
|---|---|
| A weekly email digest | The page itself, which they open when they need it, plus a pointer to Crowdin's own notification settings. Say the email part is not possible in this kind of app |
| A third-party engine or key | Crowdin's own settings for that integration when the key must work for people who should not see it. When everyone using it may also know it, [Crowdin Storage](#when-the-app-needs-to-remember-something) holds it with `secret: true`: `kv.user.*` for a personal key, a restricted module's `module:` prefix for a team key |
| A huge file processed | The same work on a size that fits, with the limit agreed up front |
Only when nothing useful survives, say so and offer to hand it to a developer. Never end a user's run with npm package names as their next step.
## The build sequence
### 0. Preflight
Node `^22.19 || ^24` and pnpm. `npx` implies Node, so pnpm is the usual gap: `corepack enable pnpm`. If Node itself is missing, say it plainly - this needs one install first - and help rather than turning it into a requirement they must satisfy alone.
Probe auth with a read-only call:
```bash
npx @crowdin/serverless-apps-cli@latest list --json
```
JSON means you are logged in. `Not logged in. Run crowdin-serverless-apps login first.` means run [the auth handshake](#the-auth-handshake) first.
Say where the app will be created, in absolute terms, before creating it. Every later change happens in that directory and the user needs to know where their app lives.
### 1. Scaffold
```bash
npx @crowdin/serverless-apps-cli@latest create <name> --template projects-dashboard-cli --yes
cd <name> && pnpm install
```
Both flags matter: without them the CLI waits on an interactive picker that hangs in a non-interactive shell.
**Always `projects-dashboard-cli`.** The template is not a decision to weigh and not worth a question. With it the CLI owns the whole toolchain - Vite, Tailwind, Lingui, Biome, the base tsconfig - so everything downstream works as described here. `projects-dashboard-standalone` hands the build back to the app and every later problem with it becomes yours; pick it only if the user explicitly asks to own the build.
`create` writes `CROWDIN_APP_ID` into `.env` and registers the app. From here use the local binary inside the app directory - `npx crowdin-serverless-apps <command>` - so the toolchain matches the app's pinned version.
### 2. Placement: the part that silently breaks
The scaffold ships two modules, `organization-menu` + `profile-resources-menu`, and **each of them exists in only one edition**. Whichever edition the user is on, one of the pair is dead: it will never appear, and `preview` refuses to build a link for it. So the first edit is always to keep the one that matches their edition and delete the other from **both** `manifest.json` and `src/index.tsx`.
If the app belongs somewhere else entirely, change it in both places together:
- `manifest.json` - the `modules` map, keyed by module type
- `src/index.tsx` - the matching `prepare*` call, synchronous, at the top level
They have to agree with each other and with what the host sends in `context.app.type`. When they do not, the SDK never calls `prepare()`, the host shows a blank iframe, and nothing in the console explains why.
Editor modules need `modes`; asset panels need `fileNamePattern`; context-menu entries need an `options` triple whose legal targets depend on the location. If the user's projects have no files - string-based projects - set `stringBasedAvailable: true`, because it defaults to false and the app is otherwise absent from every one of their projects with no error anywhere.
[references/modules.md](references/modules.md) has the per-type matrix: placement in plain words, `prepare*`, extra fields, edition lock, and whether `preview` can open it at all.
### 3. Write the UI
Read [references/sdk.md](references/sdk.md) before writing SDK code - verified snippets from a working scaffold, and the traps types do not catch. Two that reach the user directly: call `resize()` after any layout change or the app looks truncated, and design a real empty state, because a first-run organization with no TMs or a project with no glossary renders a blank screen the user reads as broken.
**Every app is localizable, and that is not an optional extra.** The scaffold arrives wired for it - Lingui, a `locales/` folder, catalogs - so the default is to keep it, and stripping it would be the deviation. Put every user-visible string behind a macro (`<Trans>`, `<Plural>`, `` t` ``) as you write it, not afterwards: retrofitting means touching every line of JSX again. Then `npx crowdin-serverless-apps extract` collects them into `locales/*.po`, and `build` re-extracts by default. This app runs inside a localization platform whose own users work in their own languages; hardcoded English is the one thing they will notice immediately.
### 4. Give it a logo
The scaffold ships a placeholder `public/logo.svg`, and leaving it is the clearest sign nobody finished the job - every app in the menu wears the same generic tile. Draw the thing the app is about: layers for translation memories, bars for a report, a tag for a glossary.
Square `viewBox` matching the placeholder's `0 0 64 64`; no text, since it renders at roughly 24px; give the mark its own background, because host themes flip between light and dark and CSS cannot reach inside an `<img>`; self-contained, no remote fonts or images; a couple of KB at most. Write it to `public/logo.svg` - the manifest already points at `/logo.svg` and everything under `public/` ships in the bundle. Afterwards confirm `dist/logo.svg` exists and appears in `dist/bundle.zip`.
### 5. Green gate
```bash
pnpm typecheck && pnpm lint
npx crowdin-serverless-apps manifest validate
```
Do not hand code over while any of the three is red. `typecheck` catches invented SDK APIs; neither it nor `lint` reads `manifest.json`, which is what `manifest validate` is for. Validation is offline - no login, no app link, not even `node_modules` - so run it the moment the manifest changes. It exits 1 on an invalid manifest or malformed JSON, groups problems by section and prints a `fix` line under each; read that hint, it spells out the accepted values.
Passing does not mean Crowdin will accept the manifest, only that the cheap mistakes are gone.
### 6. Sync the manifest
```bash
npx crowdin-serverless-apps manifest status # exit code 1 means local and remote differ
npx crowdin-serverless-apps manifest push
```
Before publishing, not after. `publish` moves only the bundle; `modules`, `scopes` and `name` travel only with a push. `preview` also reads the manifest from Crowdin, not from disk, so an unpushed change means you are previewing the old placement.
### 7. Publish instead of running a dev server
```bash
npx crowdin-serverless-apps publish -y
```
Builds, uploads `dist/bundle.zip`, and switches Crowdin to serving it. Do not start `dev`: it ties the app to your machine and forces the user to keep a terminal open, which defeats handing them something that just works. `dev` is the right tool when the *user* wants a fast local loop, in their own terminal. Say the trade-off out loud - every iteration is now a build plus an upload rather than an instant reload.
### 8. Close the loop on visibility
```bash
npx crowdin-serverless-apps list --json
```
Each app reports its resolved `permissions`, so read them instead of assuming. A fresh install is `user: owner`, meaning the app works for whoever created it and is invisible to everyone else - and because `create` installed the app before you edited anything, setting `default_permissions` in the manifest afterwards does not widen an existing install.
So: derive the audience from the request ("so the translators can see it" is `all`, a vendor page is `guests`, a cost report is `owner`), and if it needs to be wider than what `list --json` reports, walk the user to the Crowdin apps manager in plain words. Skipping this is how a run ends badly with everything green: they share the link, a colleague sees nothing, and the app looks broken.
Remember calls run as the **viewer**. An app built for translators must only need what a translator may do, or it will 403 for exactly the person it was made for.
### 9. Open it, and look
```bash
npx crowdin-serverless-apps preview --module <key> [--project <id|identifier>]
```
Always pass `--module`: with more than one module and no flag the CLI refuses to choose. Project modules and `editor-right-panel` need a project - omitting `--project` falls back to the first project the login can access, which is fine for a smoke test and wrong whenever the content depends on the project, so pass the one from the user's link.
Seven of the eighteen types have no page to open - the three editor types other than `editor-right-panel`, plus `modal`, `chat`, `context-menu` and `navbar-extension` - and for those `preview` **fails with an error rather than degrading**. That is expected, not a bug: the handover is a short navigation instruction instead of a link. Tell the user that before scaffolding such a placement, and offer the linkable alternative.
Then verify, because nothing before this point proves the app renders: `typecheck`, `lint` and `publish` are all blind to a blank iframe, a 403, a clipped panel or an empty screen. If you can drive a browser, open the link, confirm the app draws and the console is clean. If you cannot, say plainly that it is unverified and ask the user to open it and tell you what they see. Never report success on the strength of a printed URL.
Hand the link over as a clickable link in your message - `preview` opens a browser in *your* environment, which may not be the user's.
## The auth handshake
Login is a browser OAuth flow, so it needs the user. Start it **in the background** so you can read its output while it waits:
```bash
npx @crowdin/serverless-apps-cli@latest login
```
In a non-interactive shell it prints the authorize URL as plain text - `open this link (https://accounts.crowdin.com/oauth/authorize?...)`. Pull that URL out and hand it over as a clickable link; the CLI's own attempt to open a browser may do nothing in your environment. It waits five minutes, then gives up - mention that, and restart it if they stepped away.
Never ask for a token in chat. On a machine with no browser at all, `CROWDIN_PERSONAL_TOKEN` in the environment works instead.
## When something goes wrong
| What you see | What it means |
|---|---|
| `Not logged in. Run crowdin-serverless-apps login first.` | The auth handshake. |
| The CLI hangs with no output on `create` | Missing `--template` or `--yes`; it waits on a picker. |
| `This app declares 2 modules. Pass --module <key>` | The untouched scaffold. Delete the module that is dead on this edition, then pass the key. |
| `Module type X only has a page on Crowdin Enterprise` / `only on crowdin.com` | Edition-locked placement. Pick one that exists on their edition. |
| `Module type X has no page of its own` | One of the seven unopenable types. Expected: hand over navigation, not a link. |
| Blank iframe, or the host reports the app did not start | Manifest type, `prepare*` and `context.app.type` disagree, or a registration is not at the top level, or two modules of one type without keys. |
| App published but absent from the UI slot | `manifest push` skipped, or a string-based project without `stringBasedAvailable`. |
| App opens but shows nothing | Empty data, or the first-project fallback landed somewhere with no glossary/TM/strings. Check before blaming the code. |
| 403 or an empty list at runtime, all checks green | A scope. See [scopes.md](references/scopes.md), including the read-looking calls that need the bare scope. |
| Every storage call rejected | `application.storage` missing from the manifest `scopes`, or the manifest change not pushed. |
| `kv.user.*` throws while shared keys work | The viewer is anonymous - no user identity. Check `user.id` in the context before offering per-user features. |
| A write under `module:...:` rejected | The key names a module other than the one the app is currently running as, or an unknown one. On reads the same situation is silent: `get` resolves `undefined`, listings skip the records. |
| `You have no accessible projects to preview this module in.` | Either no project access, or a crowdsource module needing a public project. |
| `Could not list your projects - this login may lack the project scope.` | Log in again to grant it. |
| `GraphQL is not available to serverless apps` | The proxy is REST `/api/v2` only. |
| `FormData bodies are not supported through AP.apiRequest` | Pass a raw `Blob`, `File` or `ArrayBuffer`. |
| `manifest push` rejected by Crowdin | Run `manifest validate`; it names the offending path. |
| `npx` fails on network or permissions | Your environment, not the CLI. Ask the user to allow it. |
## References
- [references/modules.md](references/modules.md) - every module type: placement, `prepare*`, manifest fields, edition lock, what `preview` can open
- [references/sdk.md](references/sdk.md) - verified SDK snippets and the traps types do not catch
- [references/scopes.md](references/scopes.md) - the scope catalogue and the read-needs-write cases
- [references/cli.md](references/cli.md) - command and flag reference, env vars, what each command writes
Referenced files: 4 — file references are metadata, not package contents.
croql4.56 KB
--- name: croql description: Helps write, fix, and optimize Crowdin CroQL expressions for strings, translations, TM segments, and glossary terms. Use whenever the user mentions CroQL, Crowdin filters, advanced editor filtering, "where" conditions for localization data, or API queries with a `croql` parameter, even if they do not explicitly ask for query syntax help. --- # CroQL Skill Use this skill to turn natural-language filtering goals into valid Crowdin CroQL expressions. ## Full documentation - [Crowdin Query Language (CroQL) docs](https://support.crowdin.com/developer/croql/) ## What to produce When helping with CroQL, return: 1. A primary CroQL expression. 2. A short explanation of why it matches the request. 3. One optional stricter or broader variant if the request is ambiguous. ## Quick workflow 1. Identify the data context: - Source strings - Language translations - TM segments - Glossary terms 2. Map user intent to CroQL fields and operators. 3. Build the query in small clauses and combine with `and`/`or`. 4. Validate likely pitfalls (wrong context field, missing language scope, unencoded query). 5. Provide the final query and ready-to-run endpoint sample. ## Core syntax cheatsheet - Comparison: `=`, `!=`, `>`, `>=`, `<`, `<=`, `between ... and ...`, `contains` - Logic: `and`, `or`, `xor`, `not` - Filtering collections: `collection where (predicate)` - Counting: `count of translations`, `count of comments where (...)` - Object member access: `id of file`, `count of votes` - Mentions: - User: `@user:"username"` - Language: `@language:"uk"` ## Context-aware query patterns Pick fields that belong to the correct context. A frequent failure mode is using fields from a different context. ### 1) Source strings context Common fields: - `text`, `identifier`, `context` - `is hidden`, `is duplicate` - `count of translations`, `count of comments`, `count of screenshots` - `count of languages summary where (...)` - `id of file`, `added`, `updated` - Enterprise fields filters: - `count of fields where (name = "Priority" and value = "High") > 0` Examples: - Hidden non-duplicates with translations: - `is hidden and not is duplicate and count of translations > 0` - Source contains `ABC`, but translations do not: - `text contains "ABC" and count of translations where (text contains "ABC") = 0` - At least one approval in any language: - `count of languages summary where (approvalsCount >= 1) > 0` ### 2) Translation context Common fields: - `text`, `plural form`, `is pre translated`, `provider`, `user`, `updated` - Nested: `count of votes where (is up)`, `count of approvals where (...)` Example: - By specific user or highly upvoted: - `user = @user:"crowdin" or count of votes where (is up) >= 100` ### 3) TM segment context Common structure is record-based: - `count of records where (usageCount > 0) > 0` - `count of records where (createdBy = @user:"crowdin") > 0` ### 4) Glossary term context Common fields: - `text`, `description`, `language`, `user`, `partOfSpeech`, `status`, `type`, `gender` - `note`, `lemma`, `url`, `createdAt`, `updatedAt` Examples: - Obsolete or not recommended: - `status = "not_recommended" or status = "obsolete"` - English lemma: - `lemma = "cancel" and language = @language:"en"` ## Advanced editor-style filters These are frequent requests from users trying to mirror UI filters in API calls: - Untranslated: - `count of languages summary = 0` - Translated: - `count of languages summary where (is translated) > 0` - Approved: - `count of languages summary where (is approved) > 0` - Has QA issues: - `count of languages summary where (has qa issues) > 0` - Duplicates only: - `is duplicate` If target language matters, scope it explicitly: - `count of languages summary where (language = @language:"uk" and is translated) > 0` ## Query construction strategy When a user request is underspecified: 1. Start with the strictest interpretation. 2. Offer one broader fallback query. 3. State what assumption changed (language scope, approval threshold, date range, etc.). Example: - Strict: - `count of languages summary where (language = @language:"uk" and is translated and not is approved) > 0` - Broader: - `count of languages summary where (is translated and not is approved) > 0` ## Common mistakes to prevent - Mixing source-string fields with translation-only or glossary-only fields. - Forgetting language scoping when reproducing Editor filters in API. - Using raw (non-encoded) CroQL in request URLs. - Missing parentheses in nested `where` clauses. - Overusing `or` without grouping conditions.
crowdin-api-client5.15 KB
---
name: crowdin-api-client
description: Use when implementing, refactoring, or reviewing `@crowdin/crowdin-api-client` usage in any JavaScript or TypeScript project, especially for endpoint selection, request/response typing, pagination, uploads, retries, timeouts, or `CrowdinError` handling.
---
# Crowdin API Client
## Overview
Use this skill when touching Crowdin API integration code in any project that depends on `@crowdin/crowdin-api-client`.
Core principle: verify local client typings first, then implement with strict model types and correct response envelope handling.
## When to Use
- New or changed calls to `@crowdin/crowdin-api-client`.
- Compile/type errors after API-client upgrades.
- Confusion about method names, request fields, or `ResponseList` vs `ResponseObject`.
- Refactors involving uploads, file translations, screenshots, tasks, comments, strings, or projects.
- Reviews where behavior works at runtime but types/mapping look suspicious.
- Multi-step flows: build and download, pre-translate, upload or update files, label strings and open a task, TM or glossary import, reports. Start from [references/recipes.md](references/recipes.md) instead of composing the calls from memory.
Do not use this skill for unrelated infra-only changes.
## Required Preflight (Before Editing)
1. Check local declarations first: `node_modules/@crowdin/crowdin-api-client/out/<module>/index.d.ts`.
2. Confirm exact API class and method name in use.
3. Confirm request model type and required fields.
4. Confirm response envelope: `ResponseList<T>` or `ResponseObject<T>`.
Do not code from memory when typings and docs disagree.
## Quick Reference
| Scenario | Preferred Pattern |
|---------|-------------------|
| Multi-module flow | `new Client(credentials, options)` and destructure needed APIs |
| Single narrow flow | Direct module API class is acceptable |
| Full collection reads | `.withFetchAll()` |
| Transient failures | `retryConfig` |
| Bounded request duration | `{ httpRequestTimeout: 60_000 }` |
| Uploads | `uploadStorageApi.addStorage` -> `sourceFilesApi.createFile` |
| Long-running operations (builds, pre-translation, imports, exports, reports) | Poll the matching `check...Status` method with a delay; stop on `finished`, `failed` or `canceled` — [recipes](references/recipes.md#helpers-wait-save-chunk) |
| Error branching | `CrowdinValidationError` before `CrowdinError` |
## Core Patterns
### Bootstrap
```ts
import { Client, Credentials } from '@crowdin/crowdin-api-client';
const credentials: Credentials = {
token: process.env.CROWDIN_PERSONAL_TOKEN!,
organization: process.env.CROWDIN_ORGANIZATION, // Crowdin Enterprise only
};
const client = new Client(credentials);
```
### Response Envelope Handling
```ts
const listResponse = await client.tasksApi.withFetchAll().listTasks(projectId, options);
const tasks = listResponse.data.map((entry) => entry.data); // ResponseList<T>
const createResponse = await client.tasksApi.addTask(projectId, request);
const task = createResponse.data; // ResponseObject<T>
```
### Typed Request Models
```ts
const request: TasksModel.CreateTaskRequest = {
title: 'My Task',
languageId: 'de',
type: 0,
fileIds: [12],
};
```
### Error Handling
```ts
import { CrowdinError, CrowdinValidationError } from '@crowdin/crowdin-api-client';
try {
// API call
} catch (error) {
if (error instanceof CrowdinValidationError) throw error;
if (error instanceof CrowdinError) throw error;
throw error;
}
```
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Guessing endpoint methods from memory | Validate in local `index.d.ts` first |
| Treating list response as object response | Check `ResponseList<T>` vs `ResponseObject<T>` and map accordingly |
| Weakening or bypassing model typing | Use client `*Model` types in service signatures and fixtures |
| Manual pagination loops by default | Prefer `.withFetchAll()` unless custom batching is required |
| Uploading file directly to `createFile` | Create storage first, then pass `storageId` |
| Catching all errors as generic | Branch `CrowdinValidationError` and `CrowdinError` explicitly |
| Polling a status endpoint in a tight loop, or only until `finished` | Sleep between checks and treat `failed` and `canceled` as terminal — see [recipes](references/recipes.md) |
| Composing a multi-step flow from memory | Start from the matching recipe in [references/recipes.md](references/recipes.md) |
## Verification Gate
After changing API-client usage:
1. Run lint/type checks in the current project.
2. Run at least one targeted test for the modified API flow.
3. If tests fail on strict typing, fix fixture/model typing instead of loosening production types.
## Review Checklist
- Correct API module and method for the task.
- Request models are explicit and include required fields.
- Response mapping matches envelope shape.
- Type safety is preserved without bypassing model contracts.
- Pagination strategy matches expected volume.
- Retry/timeout/runtime options fit environment.
- Error handling distinguishes validation from generic API errors.
Reference docs: [Crowdin API Client modules](https://crowdin.github.io/crowdin-api-client-js/modules.html) · [API recipes](references/recipes.md)
Referenced files: 1 — file references are metadata, not package contents.
crowdin-cli11.2 KB
---
name: crowdin-cli
description: Guides correct usage of Crowdin CLI - the `crowdin` command that syncs localization files between a local project and Crowdin. Use whenever the user runs, scripts, or debugs `crowdin` commands, authorizes the CLI, creates or edits a crowdin.yml configuration, uploads sources, downloads translations, checks translation status, auto-translates a project, wires localization into CI/CD, or migrates scripts from CLI v4 to v5 - even if they just say "sync translations" or "push strings to Crowdin" without naming the CLI.
---
# Crowdin CLI
Crowdin CLI is the command-line tool for managing and synchronizing localization resources with a Crowdin project. It ships as a single self-contained binary — no Java or other runtime required.
Check what's installed with `crowdin --version`. If it prints `4.x` or a Java error, the user is on the legacy CLI — commands below still mostly apply, but see [references/migrating-from-v4.md](references/migrating-from-v4.md) for the differences.
Full documentation: https://crowdin.github.io/crowdin-cli
## Install
```bash
npm install -g @crowdin/cli
```
Also available via Homebrew (`brew tap crowdin/crowdin && brew install crowdin@5`), WinGet, Chocolatey, Docker (`crowdin/cli`), Linux package repositories, and as a standalone binary — see [references/configuration.md](references/configuration.md#installation) for every option.
## Authenticate
The CLI needs an **API token** and (for most commands) a **project ID**. On a developer's machine the quickest route is:
```bash
crowdin login
```
It authorizes in the browser and saves the token to the identity file `~/.crowdin.yml`, where every later command — run by the user or by an agent — picks it up. No configuration file is generated or required, and Crowdin Enterprise needs nothing extra: the organization comes from the account signed in. `crowdin init` runs the same browser authorization and then generates a `crowdin.yml`; `login` is the choice when the config is written by hand or already exists. The flow finishes through a browser callback on the same machine, so when the agent's shell may be elsewhere, the user runs it in their own terminal. The browser-issued token expires after 30 days — re-run `crowdin login` when a command exits `101`. Details: [references/commands.md](references/commands.md#login).
For CI and anything long-lived, use a **personal access token** — it does not expire:
- crowdin.com: create the token in **Settings → API** (`https://crowdin.com/settings#api-key`). `base_url` is `https://api.crowdin.com` (the default — no need to set it).
- Crowdin Enterprise: create it in **Account Settings → Access Tokens**. `base_url` must be set to `https://{organization}.api.crowdin.com`.
The project ID is numeric — get it with `crowdin project list` once a token is configured (add `-i 1` while no `crowdin.yml` carries a `project_id`: validation demands one even though this command ignores it), or from the project page in Crowdin.
Credentials resolve in this priority order (highest wins):
1. CLI flags: `-T/--token`, `-i/--project-id`, `--base-url`, `--base-path`
2. Identity file: `--identity <path>`, or `~/.crowdin.yml` / `~/.crowdin.yaml` if present
3. `*_env` keys in the config file (`api_token_env: MY_VAR` reads `$MY_VAR`)
4. Literal keys in the config file (`api_token: "..."`)
5. `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID`, `CROWDIN_BASE_PATH`, `CROWDIN_BASE_URL` environment variables
`.env` files in the working directory are loaded automatically, so `CROWDIN_PERSONAL_TOKEN` can live there. **Never write a real token into `crowdin.yml`** if the file is committed — use `api_token_env` or the `CROWDIN_PERSONAL_TOKEN` variable instead. A committed `api_token_env` and a local `crowdin login` coexist: the unset variable contributes nothing, so the identity file supplies the token on the developer's machine and the variable supplies it in CI.
## Configure — crowdin.yml
Commands read `crowdin.yml` (or `crowdin.yaml`) from the current directory; override with `-c/--config <path>`. Minimal real-world example:
```yaml
"project_id": "123456"
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
"base_path": "."
"preserve_hierarchy": true
files:
- source: "/src/locales/en/**/*.json"
translation: "/src/locales/%two_letters_code%/**/%original_file_name%"
```
- `source` is a glob for local source files (relative to `base_path`); `translation` tells Crowdin where exported translations land, using placeholders like `%two_letters_code%`, `%locale%`, `%original_file_name%`, `%original_path%`, `%file_name%`.
- `preserve_hierarchy: true` keeps the local directory structure in Crowdin; without it, the common parent directory is stripped.
- One `files` entry per file group; each entry can add `ignore`, `dest`, `type`, `update_option`, `labels`, `excluded_target_languages`, `languages_mapping`, and more.
The full reference — all placeholders, per-entry options, language mapping, multiple file groups — is in [references/configuration.md](references/configuration.md).
Before the first upload, verify the config instead of debugging a wrong upload after the fact:
```bash
crowdin config lint # validate syntax
crowdin config sources # list local files matched by each source pattern
crowdin config translations # list translation paths that will be produced
crowdin upload sources --dryrun
```
## Core workflow
The everyday cycle is: upload sources → translate → download translations.
```bash
crowdin upload sources # push local source files (alias: crowdin push)
crowdin status # translation & proofreading progress per language
crowdin auto-translate --method tm # optional: pre-fill via TM, MT, or AI
crowdin download translations # pull completed translations (alias: crowdin pull)
```
Useful variants:
```bash
crowdin upload sources --cache # skip unchanged files (checksum cache)
crowdin upload sources --delete-obsolete # remove files from Crowdin that no longer exist locally
crowdin upload translations # import existing local translations
crowdin download translations -l uk -l fr # only some languages
crowdin download translations --skip-untranslated-strings
crowdin download sources # pull sources back from Crowdin
```
Everything is branch-aware: pass `-b <name>` to scope upload/download/status to a Crowdin branch (create it with `crowdin branch add`).
`auto-translate` (v4's `pre-translate`) applies TM (`--method tm`), machine translation (`--method mt --engine-id <id>`), or AI (`--method ai --ai-prompt <id>`), with fine-grained control: `--scope untranslated|translated|all`, `--skip-approved-translations`, `--label`/`--exclude-label`, `--replace-translations-option`, `--translation-modified-before`, and more — see [references/commands.md](references/commands.md#auto-translate).
## Output for scripts and agents
The global `-o/--output` option changes any command's output format:
- `-o json` — machine-readable JSON; pipe into `jq` or parse from a script
- `-o toon` — [Token-Oriented Object Notation](https://github.com/toon-format/toon), same data as JSON at a fraction of the size; **prefer this when the consumer is an LLM**
- `-o plain` — minimal processable text (v4's `--plain` flag)
In `json` and `toon` modes stdout carries nothing but data — no spinners, colors, or decorative messages — so output is safe to parse. When running the CLI as an agent, default to `-o toon` for lists you only need to read and `-o json` when a script must parse the result.
```bash
crowdin file list -o json | jq -r '.[].path'
crowdin status -o toon
```
Exit codes are stable and meaningful — branch on them rather than parsing error text:
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error |
| 2 | Validation error (bad config or arguments) |
| 101 | Authorization error (bad/expired token) |
| 102 | Not found |
| 103 | Forbidden |
| 129 | Rate limit exceeded |
Mutating file commands accept `--dryrun` to preview what would happen. Add `-v/--verbose` when diagnosing; `--no-progress`/`--no-colors` clean up logs in CI (json/toon modes already imply this).
## Command map
23 top-level commands; most have subcommands. Full option-level reference: [references/commands.md](references/commands.md).
| Command | Purpose |
|---------|---------|
| `upload` (`push`) | Upload sources / translations per crowdin.yml |
| `download` (`pull`) | Download translations / sources per crowdin.yml |
| `init` | Generate crowdin.yml interactively |
| `login` | Authorize in the browser and save the token to `~/.crowdin.yml` |
| `status` | Translation & proofreading progress |
| `auto-translate` | Pre-fill translations via TM / MT / AI |
| `file` | Direct file ops without config patterns (list, upload, download, delete) |
| `string` | Manage source strings (list, add, edit, delete; CroQL filters) |
| `branch` | Manage branches (add, list, clone, merge, edit, delete) |
| `task` | Create and list translation/proofreading tasks |
| `tm` | Translation memories (list, upload, download; `list --assigned` scopes to the project) |
| `glossary` | Glossaries (list, upload, download; `list --assigned` scopes to the project) |
| `style-guide` | Style guides (list, download, upload, delete) |
| `bundle` | Export bundles (list, add, download, clone, browse) |
| `distribution` | Content distributions (add, list, edit, release) |
| `screenshot` | Screenshots for context (list, upload, delete) |
| `comment` | String comments and issues (list, add, resolve) |
| `label` | Labels (list, add, delete) |
| `language` | List project/supported languages; `--verbose` adds text direction and plural categories |
| `project` | List, add, browse projects |
| `config` | Validate config, preview matched sources/translations |
| `app` | Install/uninstall Crowdin apps |
| `context` | AI context for strings — covered by the [crowdin-context-cli](../crowdin-context-cli/SKILL.md) skill |
Shell completion: `source <(crowdin complete zsh)` (also `bash`, `fish`, `powershell`).
## CI/CD
- Store the token as a CI secret exposed as `CROWDIN_PERSONAL_TOKEN`; keep `project_id` in the committed `crowdin.yml` (it is not sensitive).
- Typical pipeline: `crowdin upload sources` on merge to the main branch; `crowdin download translations` on a schedule or before release, followed by a commit/PR of the updated files.
- For GitHub Actions prefer the official [crowdin/github-action](https://github.com/crowdin/github-action), which wraps this CLI — covered by the [github-action](../github-action/SKILL.md) skill.
- The CLI never prompts when all required values are provided; a missing value fails with exit code 2 rather than hanging.
- `crowdin status --fail-if-incomplete` exits non-zero when the project isn't fully translated — a ready-made release gate.
## Related skills
- [crowdin-context-cli](../crowdin-context-cli/SKILL.md) — `crowdin context download/upload` for AI context enrichment
- [croql](../croql/SKILL.md) — CroQL expressions for `--croql` filters on string commands
- [crowdin-api-client](../crowdin-api-client/SKILL.md) — the JS/TS API client, when a CLI command doesn't cover the need
- [translate](../translate/SKILL.md) — drafts translations into local resource files with the project's glossary, TM and style guide, and uploads them as suggestions
Referenced files: 3 — file references are metadata, not package contents.
crowdin-context-cli5.11 KB
---
name: crowdin-context-cli
description: Documents the Crowdin CLI context commands (download, upload, status, reset) for AI enrichment. Use when downloading or uploading strings context for Crowdin, checking context coverage, resetting AI-generated context, managing context files, or running crowdin context commands.
---
# Crowdin Context CLI
Use the Crowdin CLI for context download and upload. Config is read from `crowdin.yml` by default; override with `-c, --config=<path>` if needed.
```
crowdin context <command> [options]
```
## context download
Download strings context to a separate file for enrichment by AI Agent.
```
crowdin context download [CONFIG OPTIONS] [OPTIONS]
```
- `--to=<path>` — File path to download the context to. Default: `crowdin-context.jsonl`
- `-f, --file=<glob>` — Filter strings by Crowdin file paths (glob). Multiple paths can be specified
- `--label=<label>` — Filter strings by labels. Multiple labels can be specified
- `-b, --branch=<name>` — Filter by branch name
- `--croql=<expr>` — CroQL expression (cannot be combined with the other filter options)
- `--since=<YYYY-MM-DD>` — Only strings created after this date
- `--status=<value>` — Which kind of context a string already has: `empty` (none of any kind), `manual` (source context, no AI context), `ai` (AI context written). See below — this is not a filter for "still needs context"
Config options (if not using a config file): `-T, --token`, `-i, --project-id`, `--base-url`, `--base-path`. Use `-c, --config=<path>` to override config file (default: `crowdin.yml` or `crowdin.yaml`).
**Which `--status` selects the strings needing AI context depends on the source format, so prefer omitting it.** A format that carries source references into Crowdin — PO `#:` lines, for one — leaves every string holding manual context, so `empty` matches nothing and `manual` matches everything; a format carrying no context at all inverts that. With no `--status` the download covers both cases in one call, and costs nothing extra: `context upload` writes back only the records whose `ai_context` was actually filled.
## context upload
Upload strings context. Only files previously downloaded by `context download` are supported.
```
crowdin context upload [CONFIG OPTIONS] [OPTIONS]
```
- `--from=<path>` — File path to upload the context from. Default: `crowdin-context.jsonl`
- `--overwrite` — Also update strings where `ai_context` is empty (removes their AI section). Default: false
- `--dryrun` — Print command output without execution
## context status
Show context coverage statistics — how many strings have manual, AI, or no context.
```
crowdin context status [CONFIG OPTIONS] [OPTIONS]
```
- `-f, --file=<glob>` — Filter strings by Crowdin file paths (glob). Multiple paths can be specified
- `--label=<label>` — Filter strings by labels. Multiple labels can be specified
- `-b, --branch=<name>` — Filter by branch name
- `--croql=<expr>` — CroQL expression (cannot be combined with the other filter options)
- `--since=<YYYY-MM-DD>` — Only strings created after this date
- `--by-file` — Break statistics down per file
## context reset
Remove AI-generated context from strings; manually written context is preserved.
```
crowdin context reset [CONFIG OPTIONS] [OPTIONS]
```
- `-f, --file=<glob>` — Only reset strings from matching file paths. Multiple paths can be specified
- `--label=<label>` — Only reset strings with matching labels. Multiple labels can be specified
- `-b, --branch=<name>` — Only reset strings from the matching branch
- `--croql=<expr>` — Only reset strings matching the CroQL expression (cannot be combined with the other filter options)
- `--since=<YYYY-MM-DD>` — Only reset strings created after this date
- `--dryrun` — Print command output without execution
- `--all` — Required safety flag when no filter is specified
## JSONL format
One JSON object per line. Fields:
- `id` — String ID in Crowdin
- `key` — String key
- `text` — Source text
- `file` — Crowdin file path
- `context` — Existing source context
- `ai_context` — AI context to set (**edit this field before uploading**)
Example line:
```json
{"file":"/src/locales/en.po","ai_context":"","context":"#: src/App.tsx:54","id":3125833,"text":"Click on the Vite and React logos to learn more","key":"Click on the Vite and React logos to learn more"}
```
## Typical Workflow
1. Check coverage (optional): `crowdin context status` — the split between manual, AI and no context. Read it before picking a `--status` filter for the next step, or skip the filter and take everything.
2. Download: `crowdin context download` (writes to `crowdin-context.jsonl` by default) or `crowdin context download --to=path/to/file.jsonl`
3. Edit the file - fill in `ai_context` for each string (use [context-extraction](../context-extraction/SKILL.md) to help).
4. Upload: `crowdin context upload` (reads from `crowdin-context.jsonl` by default) or `crowdin context upload --from=path/to/file.jsonl`
To discard AI-generated context later (e.g. before re-running enrichment from scratch), use `crowdin context reset` with a filter, or `crowdin context reset --all`.
feedback7.92 KB
---
name: feedback
description: Drafts feedback about Crowdin for the Crowdin team - the skills in this plugin, the Crowdin MCP server, the Crowdin CLI, the Crowdin API or the product - and sends it as a GitHub issue in crowdin/skills only when the user explicitly asks. Use when the user asks to report or file something about Crowdin; after a Crowdin skill instruction, CLI command, MCP tool call or API call failed reproducibly and was just fixed or given up on; when the user is clearly frustrated with a Crowdin component; or when a Crowdin capability the user expected does not exist and blocked the task. Feedback about the agent itself belongs to the agent's own feedback mechanism.
---
# Feedback
Turns a problem with a Crowdin component (a skill in this plugin, the Crowdin MCP server, the Crowdin CLI, the Crowdin API, the product) into a short, factual draft on the user's machine and, only when the user asks, into a GitHub issue the Crowdin team reads.
## When to draft
Draft at high-signal moments, with or without being asked:
- The user asks: "report this", "file feedback", "this should be an issue", "tell Crowdin about this", "the skill was wrong about X".
- A Crowdin skill instruction, CLI command, MCP tool call or API call failed reproducibly and was just fixed or given up on.
- The user expressed clear frustration with a Crowdin component.
- A Crowdin capability the user reasonably expected does not exist and blocked the task.
Type each draft as one of `bug` (something behaves differently from what the documentation, the skill or the user reasonably expected), `idea` (a change that would have made the task easier) or `missing_capability` (something Crowdin cannot do that blocked the task).
One draft per distinct problem per session. Before writing, list `~/.crowdin/feedback/`; if a draft there describes the same problem, update it rather than create a second one. The same failure hit twice in different places is still one problem.
Drafting is silent. Finish the current reply as you would have, then add one line at the very end: `Drafted feedback about <short title> to ~/.crowdin/feedback/<file>. Say "send feedback" to review and send it.` That line is the only trace of the draft in the reply; questions about it wait until the user brings it up. When the user asked for the draft explicitly, show the draft in the reply instead of that line.
## Where drafts live
Write drafts to `~/.crowdin/feedback/<yyyy-mm-dd>-<slug>.md`, where the slug is three to six lowercase words joined by hyphens that name the problem. Create the folder on first use. The folder sits outside any repository, so a draft is never committed with the user's code.
Assume no shell exists until you see one. Where there is no filesystem or shell, as in Claude Cowork, ChatGPT or a skill uploaded to Claude.ai, put the same draft inline in the reply and store nothing; the user copies it from there.
## The draft
The file body is exactly what will become the issue body: no YAML frontmatter, since GitHub renders `---` in an issue as a horizontal rule. Labeled lines in this order, one to three lines each, facts only, no narrative:
```markdown
**Type:** bug
**Component:** api
**Agent:** Claude Code 2.1.269
**Plugin version:**
**What happened:** `POST /api/v2/projects/{projectId}/translations/builds` returned 500 with `{"error":{"message":"Internal Server Error"}}` for a project with one branch and eight target languages. Expected a build identifier, or a 4xx that names what is wrong with the request.
**What the user said:** "third time today, I can't ship without the build"
**Repro:** Call the endpoint with an empty JSON body on a file-based project that has one branch; three attempts one minute apart returned the same response.
**Evidence:** 2026-09-14T10:32:07Z, 10:33:11Z and 10:34:20Z (UTC); the request body was `{}`; the same call on a project without branches succeeded.
```
- `Component` is the skill name for a skill (`crowdin-cli`, `how-to-crowdin` and so on), or `mcp-server`, `cli`, `api` or `product`.
- `Agent` is the agent tool and version when the session shows them, otherwise blank. `Plugin version` is the version from the plugin manifest when the host exposes one, otherwise blank.
- `What happened` states observed versus expected, with the exact error text when it is short.
- `What the user said` quotes the user's own words; the quote carries the weight, so it stays as said. When the user said nothing about it, write `User didn't comment; observed by the model.`
- `Repro` gives the minimal steps, or the shape of input, that reproduces it.
- `Evidence` lists identifiers a reader can chase: timestamps, request identifiers from API error bodies, the Crowdin CLI version, paths relative to the working directory or `~`-prefixed. Run `crowdin --version` for it only when the CLI was involved and a shell exists. Omit the line when there is nothing.
- `Cause` appears only for a root cause verified in this session; otherwise omit the line.
Everything comes from the session or the user. A field the session did not supply stays blank; an empty field beats a plausible one.
## Privacy
Apply these while drafting, so the file on disk is already safe:
- No secrets or credentials of any kind: no Crowdin personal tokens or OAuth secrets, no value of `CROWDIN_PERSONAL_TOKEN`, nothing from `crowdin.yml` that looks like a credential, no API keys of other services.
- No Crowdin organization names, project names or project identifiers. Describe the shape instead: "a file-based project with twelve target languages".
- No source strings, translations, glossary terms or file contents from the user's project. Replace them with a placeholder that keeps the shape: "a string with two ICU plural branches", "a 40 KB JSON file with nested keys".
- People by role, never by name, inside quotes too: "[a translator]", "[the project manager]", "[the user]".
- Paths relative to the working directory or `~`-prefixed, never absolute paths that reveal a user name.
- No customer channel or direct-message identifiers and no customer content.
- If the problem looks like a security vulnerability, describe the class of problem only ("a permission check that can be bypassed on one endpoint") and, when sending, prefer the private route below.
## Sending
Send only when the user explicitly asks, and only the draft they name, or the single queued draft when there is one. Show the draft first unless it is already visible in the reply, so what they approve is what goes out. "Send" after seeing the draft is the consent; never send on an earlier or general instruction.
The title is `[<type>] <component>: <short title>`, for example `[bug] api: translation build returns 500 on a project with one branch`. The type lives in the title because labels need triage permission on the repository, which most submitters do not have.
Then, in this order:
| Situation | Do this |
|---|---|
| A shell exists and `gh auth status` succeeds | `gh issue create --repo crowdin/skills --title "<title>" --body-file <draft path>` |
| `gh` is missing or not authenticated, or there is no shell | Give the user a prefilled link: `https://github.com/crowdin/skills/issues/new?title=<url-encoded title>&body=<url-encoded body>`. They open it and press Submit. |
| The encoded link would exceed 8000 bytes, or the user says the content is sensitive | Hand over the draft text and point to `https://github.com/crowdin/skills/issues/new`, or to Crowdin support at `https://crowdin.com/contacts` for anything they do not want public. Send nothing. |
Encode the title and body fully; the one-to-three-line rule keeps ordinary drafts far below the 8000-byte ceiling.
After a successful `gh` send, prepend `Sent: <issue URL>` as the first line of the draft and move the file to `~/.crowdin/feedback/sent/`. After handing over a link, leave the draft where it is; you cannot know whether the user submitted it.
Issues in `crowdin/skills` are public. Say so in one clause when you hand over a link or before running `gh`, unless the user has already been told in this session.
github-action13.5 KB
---
name: github-action
description: Sets up and debugs the Crowdin GitHub Action (crowdin/github-action), which syncs a repository with a Crowdin project and opens the translation pull request. Use whenever the user wires Crowdin into GitHub Actions, writes or reviews a .github/workflows/crowdin.yml step, tunes its inputs (upload_sources, download_translations, localization_branch_name, create_pull_request, command), or debugs a run that opened no pull request, opened one that gets no CI checks, or hit a 409 while a build was in progress - including bare phrasings like "automate translations in CI" or "my Crowdin workflow is broken".
---
# Crowdin GitHub Action
`crowdin/github-action` runs [Crowdin CLI](https://crowdin.github.io/crowdin-cli) inside a Docker container as a single workflow step: it uploads source files to Crowdin, downloads finished translations, commits them to a localization branch, and opens a pull request.
Because it is a Docker action it only runs on **Linux** runners — `ubuntu-latest`, or a self-hosted Linux runner with Docker available. It will not run on `macos-*` or `windows-*`.
Reference: [README](https://raw.githubusercontent.com/crowdin/github-action/refs/heads/master/README.md) · [examples](https://raw.githubusercontent.com/crowdin/github-action/refs/heads/master/docs/EXAMPLES.md) · [inputs and outputs](references/inputs.md) · [workflow recipes](references/recipes.md)
## The step is a pipeline, not a command
One step performs up to five operations, always in this fixed order, and each one is gated by its own input:
| Order | Input | Runs |
|---|---|---|
| 1 | `upload_sources: true` *(default)* | `crowdin upload sources` |
| 2 | `upload_translations: true` | `crowdin upload translations` |
| 3 | `download_sources: true` | `crowdin download sources`, then the **git half** |
| 4 | `download_translations: true` | `crowdin download`, then the **git half** |
| 5 | `download_bundle: <id>` | `crowdin bundle download <id>`, then the **git half** |
The **git half** is a single unit of work: `git add .`, commit, force-push the localization branch, open the PR. Its parts are individually switchable (`push_translations`, `push_sources`, `create_pull_request`), and only rows 3–5 can reach it.
That order explains nearly every surprise the action produces:
- **`upload_sources` defaults to `true`.** A step you intended as download-only also pushes your sources to Crowdin unless you write `upload_sources: false`. Set every flag you care about explicitly rather than relying on defaults.
- **The git half runs only after a download.** `create_pull_request: true` on an upload-only step is inert — there is nothing to commit.
- **Two downloads in one step run the git half twice** — two commits to the same localization branch, under one PR.
- **The git half commits with `git add .`** — everything dirty in the workspace goes in, including files earlier steps generated. Keep the Crowdin step ahead of any build steps, or give it its own job.
- **Every run with changes force-pushes**, so an open translation PR is rewritten rather than appended to — which is why its checks start over each time.
## Setting it up
Two of the five steps belong to a human: the repository secrets in step 2 and the pull-request setting in step 4 need someone with repository admin access clicking through the GitHub UI. Hand that list over explicitly rather than assuming it is already in place — a workflow that is perfect on disk still fails its first run without them.
### 1. Configure `crowdin.yml`
The action reads the same `crowdin.yml` as the CLI — the [crowdin-cli](../crowdin-cli/SKILL.md) skill covers `files`, placeholders, and language mapping. Three parts matter specifically here:
```yaml
"project_id_env": "CROWDIN_PROJECT_ID"
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
"preserve_hierarchy": true
files:
- source: "/src/locales/en/**/*.json"
translation: "/src/locales/%two_letters_code%/**/%original_file_name%"
```
- `project_id_env` / `api_token_env` point at the environment variables the workflow supplies, so no credential is ever committed.
- `preserve_hierarchy: true` keeps Crowdin's file tree identical to the repository's. Without it Crowdin strips the common parent directory, and downloaded translations land in paths that don't match the repo — the usual cause of a PR full of files in the wrong place. It is also required when moving off the native GitHub (OAuth) integration.
- Keep the file at the repository root, or pass its path via the `config` input (no leading `/`).
Repositories with a single source and translation pattern can skip the file entirely and pass `source` and `translation` as inputs instead — see [no-`crowdin.yml`](references/recipes.md#no-crowdinyml-configuration). Pick one or the other: a `crowdin.yml` in the repository conflicts with those inputs.
### 2. Create the repository secrets
In **Settings → Secrets and variables → Actions**, add:
- `CROWDIN_PROJECT_ID` — the numeric project ID, from **Tools → API** in the Crowdin project.
- `CROWDIN_PERSONAL_TOKEN` — a Crowdin personal access token ([crowdin.com](https://crowdin.com/settings#api-key), or **Account Settings → Access Tokens** in Enterprise).
Scope the token to what the workflow actually does — for a normal sync that is Projects → Read, Translation Status → Read, Source files & strings → Read and Write, Translations → Read and Write.
### 3. Write the workflow
`.github/workflows/crowdin.yml`:
```yaml
name: Crowdin
on:
push:
branches: [ main ]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
concurrency:
group: crowdin
cancel-in-progress: false
jobs:
synchronize:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Synchronize with Crowdin
uses: crowdin/github-action@v3
with:
upload_sources: true
upload_translations: false
download_translations: true
localization_branch_name: l10n_crowdin
create_pull_request: true
pull_request_title: 'New Crowdin translations'
pull_request_base_branch_name: main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
```
Details worth keeping when you adapt it:
- Credentials go in `env:`, not `with:` — the CLI and the action's git code read them from the environment. (`token` and `project_id` inputs exist as an alternative, but a token in `with:` shows up in the step's rendered inputs.)
- `workflow_dispatch` is what lets you test the workflow without waiting for a push.
- `concurrency` with `cancel-in-progress: false` queues runs instead of racing them. A second run that starts while Crowdin is still building the previous export fails with a 409.
- `pull_request_base_branch_name` is worth setting explicitly: unset, the base branch is whatever ref triggered the run, which is correct for a push to `main` but broken for tag and release triggers.
- A single sync step is the simplest shape, but the two directions have different natural rhythms — uploading on every push to `main`, downloading on a schedule. See [split workflows](references/recipes.md#split-upload-and-download).
- `actions/checkout@v7` was current in August 2026; check its releases rather than copying a pin from an older example.
### 4. Grant the token write access
Pushing a branch and opening a PR needs `contents: write` and `pull-requests: write`. With the default `GITHUB_TOKEN` that means both the job-level `permissions:` block above **and** the repository setting **Settings → Actions → General → Allow GitHub Actions to create and approve pull requests**. Without the setting, PR creation fails even though the permissions look right.
The default token has one limitation no permission fixes: **pull requests it opens do not trigger workflows**, so the translation PR arrives with no CI checks. If checks must run on it, authenticate as something other than the Actions bot — a [GitHub App](references/recipes.md#github-app-authentication) (scoped, auto-rotating, the better default) or a classic PAT with the `repo` scope, passed as `GH_TOKEN` or `GITHUB_TOKEN`.
### 5. Prove it runs
Valid YAML is not the finish line. This action fails at runtime — on credentials, file patterns and permissions — so the work is done when a run has printed the stages you expected, not when the file is written:
1. Set `dryrun_action: true` and dispatch the workflow. The CLI prints the files it would manage, so wrong `source` patterns surface here, before anything reaches Crowdin.
2. Drop the flag, dispatch again, and read the log through to the end: `UPLOAD SOURCES`, then `DOWNLOAD TRANSLATIONS`, then either a PR URL or `NOTHING TO COMMIT`.
3. Re-run a misbehaving run with **Enable debug logging** — the action detects `RUNNER_DEBUG`, dumps its environment, and adds `--verbose --debug` to every CLI call.
Dispatching needs the human who holds the secrets; ask for the run's log rather than stopping at the workflow file.
## What breaks, and why
Read the step's log first: it prints `UPLOAD SOURCES`, `DOWNLOAD TRANSLATIONS`, `PUSH TO BRANCH`, `NOTHING TO COMMIT`, and `CREATE PULL REQUEST` as it goes, so the pipeline stage that failed is visible before you start guessing.
| Symptom | Cause | Fix |
|---|---|---|
| Green run, no PR, log says `NOTHING TO COMMIT` | Downloaded translations are byte-identical to what's committed | Working as intended — nothing to do |
| Green run, no PR, no download in the log | `download_translations` left at its `false` default | Set `download_translations: true` |
| `ERROR: Either 'GITHUB_TOKEN' or 'GH_TOKEN' must be set` | Token missing from the step's `env:` | Add `GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}` |
| `FAILED TO AUTHENTICATE WITH GITHUB`, or 403 on push | Job lacks write permissions, or the repo blocks Actions from creating PRs | Add the `permissions:` block **and** enable the repository setting (step 4) |
| PR opens but no CI checks run on it | PRs from the default `GITHUB_TOKEN` don't trigger workflows | Use a GitHub App token or a PAT |
| Checks restart on every sync run | The localization branch is force-pushed each time | Pass a PAT to `actions/checkout` so the pushes come from a user |
| PR creation fails on a tag or release trigger | Base branch defaults to the triggering ref, which is `refs/tags/...` | Set `pull_request_base_branch_name` |
| Later steps hit permission errors on downloaded files | The container runs as root, so the files it writes are root-owned | Add `user: auto` to detect the workspace owner |
| `409` / build already in progress | Parallel runs against one Crowdin project | Serialize with `concurrency`, or `max-parallel: 1` in a matrix |
| Build artifacts in the translation PR | `git add .` commits the whole workspace | Move the Crowdin step before the build, or into its own job |
| Nothing downloads, project is string-based | `download_translations` doesn't apply to string-based projects | Download a bundle: `download_bundle: <id>` |
| `crowdin.yml` not found | The action looks in the repository root | `config: path/to/crowdin.yml`, no leading `/` |
| Enterprise: 404 or auth failure from the CLI | Enterprise needs an organization-specific API host | `base_url: 'https://{organization}.api.crowdin.com'` |
## `command` — running any CLI command
`command` (plus optional `command_args`) turns the step into an arbitrary CLI invocation: `status`, `auto-translate`, `bundle`, `string`, anything the CLI supports.
```yaml
- name: Fail the build unless translations are complete
uses: crowdin/github-action@v3
with:
command: 'status translation'
command_args: '--fail-if-incomplete'
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
```
Two consequences of how it is implemented, neither of them documented upstream:
- **`command` replaces the pipeline.** The step runs the command and exits — no upload, no download, no commit, no PR, whatever the other inputs say. Sequencing a command between an upload and a download takes three separate steps ([AI pre-translation](references/recipes.md#ai-pre-translation) is the canonical example).
- **The action's config inputs don't reach it.** `config`, `token`, `project_id`, `base_url`, `base_path`, `crowdin_branch_name`, and `dryrun_action` are not forwarded to a `command` step — only `command_args` is. Supply credentials through `env:` and put everything else in `command_args` (`--config path/to/crowdin.yml`, `--branch main`, `--dryrun`).
The command's stdout is available to later steps as the `command_output` output.
## Which version to pin
`@v3` is the current stable line and runs Crowdin CLI 5.
`@v2` is the legacy line, bundling the Java-based Crowdin CLI 4. Every input, output, and default is identical between the two, so upgrading a pipeline-style step is a one-line pin change; what changes is the CLI underneath, and therefore the strings you pass to `command` and `*_args`. Most notably v4's `pre-translate` became `auto-translate`, and `--plain` became `-o plain`; the [migration reference](../crowdin-cli/references/migrating-from-v4.md) lists the rest. Stay on `@v2` only while a `command` step still depends on v4-specific CLI syntax.
## Related skills
- [crowdin-cli](../crowdin-cli/SKILL.md) — `crowdin.yml` configuration and the commands and flags behind every input
- [crowdin-api-client](../crowdin-api-client/SKILL.md) — for automation the CLI doesn't cover
- [croql](../croql/SKILL.md) — CroQL filters for `command_args` on string commands
Referenced files: 2 — file references are metadata, not package contents.
glossary-generation8.79 KB
--- name: glossary-generation description: Generates a starting glossary for a Crowdin project from the project's own source strings and uploads it with the Crowdin CLI. Use when someone wants to create a glossary, extract key terms or terminology from an app's strings, mark product and feature names as do-not-translate, or seed a Crowdin project with consistent vocabulary - including bare phrasings like "make a glossary for my project". Terms are drafted for review and never uploaded unseen, and translations are never invented. Not for managing an existing glossary's entries one by one, and not for the full internationalize-and-connect journey (that is i18n-setup, which delegates here). --- # Glossary generation Translators meet a product's vocabulary one string at a time, with no way to tell which words are the product's own. A glossary is where a term gets decided once — translated one way everywhere, or not translated at all — and it is worth most before the first translation is made rather than after fifty of them disagree. The glossary this skill produces is a **starting point, not a finished asset**. Ten to thirty terms a localization manager can accept, edit, or delete beats two hundred that read as noise; the second kind gets deleted wholesale, which also throws away the few good entries in it. Treat forty terms as a hard ceiling, and prefer leaving a doubtful term out — a missing entry costs one inconsistent translation, a wrong one teaches every translator the wrong answer. For the `glossary` command's full flag set, read the `crowdin-cli` skill's [commands reference](../crowdin-cli/references/commands.md#glossary); below are only the flags this skill passes. ## Inputs Three things, resolved before any term is picked: - **The source strings.** Read the **source catalog** — the file `crowdin.yml` uploads, at its resolved `source` pattern — not the source tree. Those are exactly the strings translators will see, already stripped of code. When there is no `crowdin.yml`, ask the user where the source-language strings live rather than scanning code for them. - **A one-line product description.** This is what makes "brew" a domain term in a coffee-logging app and an ordinary verb anywhere else; without it, term selection degenerates into picking capitalized words. Ask for it if nothing in the conversation or repository supplies one. - **A draft location outside version control** — a scratch directory, or a gitignored path the caller names — so a rejected draft never lands in the repository. ## What earns an entry A term qualifies when a translator, seeing it in one string with no other context, could reasonably get it wrong. In practice that is four kinds: - **Product and feature names.** Brand names, plan names, named surfaces — usually the entries that say *do not translate this*. This is the single most valuable category and the one most often missing. - **Domain vocabulary the product leans on.** Words with a specific meaning in this product's world, particularly ones that recur across strings. - **Words that are ambiguous out of context.** English's noun/verb pairs are the classic case: on its own, a `Log` label could be a record or an action, and a translator has to pick a word class before they can pick a word. - **UI objects that must stay consistent with each other.** When a product distinguishes two similar things, translating them to the same word collapses a distinction the interface depends on. Recurrence is the strongest signal: a term in two or more source strings is a candidate on frequency alone. A term appearing once needs one of the other reasons — a brand name earns its entry the first time it is used. **What does not earn an entry:** ordinary UI verbs every translator has already translated a thousand times (`Save`, `Cancel`, `Delete`) unless this product uses one in a non-obvious way; whole sentences or message strings, which are strings to translate rather than terms to define; and anything you would be guessing at, including a term whose meaning you could not determine from the strings and the product description together. Each entry carries a **description**, and it answers a practical question — what this is in this product, and whether it should be translated at all — rather than defining the English word. "The paid tier; a plan name, keep in English" is useful. "A plan is an intention to do something" is not. Never invent translations. The glossary goes up with source-language terms only; translations are the translators' work, and a machine-guessed one in a glossary is a wrong answer wearing the authority of a decision. ## Draft the CSV Write the terms to the draft location: ```csv term_en,description_en BrewLog,The product name. Keep in English everywhere. brew,A single coffee preparation the user records. Not a beverage in general. cup,One serving logged against a brew. Countable; appears in plural forms. Home barista,Free plan name. Keep in English. ``` The header uses the **source locale's Crowdin language id** (`term_en`, `description_en` for an `en` source), which is the language id, not whatever code scheme the project spells its locale directories in — those two are allowed to differ, and the glossary API only ever speaks Crowdin ids. Then show the user the terms and stop. This is a real gate: the file is small, plainly readable, and about to become an asset their translators treat as authoritative. Ask for an explicit go-ahead, and take edits before uploading rather than after. ## Upload Both commands are authenticated: the token comes from the environment (`CROWDIN_PERSONAL_TOKEN`, or whatever `crowdin.yml`'s `api_token_env` names), and its value is never asked for, echoed, or written. A gitignored `.env` at the project root is enough to supply it — the CLI reads `.env` from its working directory natively. **First, check for an existing glossary** — uploading without an id creates a *second* glossary rather than merging: ```bash crowdin glossary list --assigned ``` - **A glossary already exists** — do not create another; import into it: `crowdin glossary upload <file> --id <id>`. Read its term count before saying anything about the merge. **`terms: 0` is an empty glossary Crowdin created with the project**, which happens on every new project and means there is nothing to preserve — import and move on. A **non-zero count is an asset somebody curated**: say plainly that the import merges into it, and let the user decline, which a project with a mature glossary usually should. `--assigned` lists only the glossaries assigned to the project in `crowdin.yml`, which on most projects is exactly one; without the flag the list is account-wide and the project's own glossary has to be picked out by name. - **No glossary exists** — create one: upload without `--id`, passing `--language` the Crowdin language id of the source locale: ```bash crowdin glossary upload glossary.csv \ --language en \ --scheme term_en=0,description_en=1 \ --first-line-contains-header ``` The upload prints the new glossary's id — report it back, and record it wherever the caller keeps decisions: it is what a later run passes to `--id` instead of creating a duplicate. The import is asynchronous. `glossary upload` prints `Imported in #<id>` while `glossary list` can still report `terms: 0`; the count catches up moments later. Confirm the term count by re-reading it, so a race is not mistaken for a failed import. ## Failure and re-runs - **The upload fails** — the terms survive in the draft CSV, so nothing has to be regenerated. Read the error before changing the file: an authentication or permission failure is not a malformed CSV. - **The run repeats on the same project** — `glossary list` is what makes it idempotent. Without it, every re-run leaves another glossary behind, and a project with three glossaries called after the same file is worse off than one with none. - **The user declines the upload** — that is a complete outcome, not a failure. Leave the CSV in place, tell them where it is and what command would upload it, and stop. ## Invoked by another workflow? A calling workflow (the `i18n-setup` journey is one) may pass the inputs directly instead of being asked for them: the source catalog path, the product description, the draft path, and the source locale's Crowdin language id. Honor them as given. The contract back is small: the draft CSV at the path it named, the review gate still real (the caller's user, not the caller, says go), and one of two outcomes reported — the uploaded glossary's id, or "declined, draft kept at `<path>`". Both are complete outcomes. ## Related skills - **`crowdin-cli`** — the authoritative surface for `crowdin glossary` and every other command this skill runs. - **`i18n-setup`** — the full internationalize-and-connect journey; its phase 6 delegates its `generate_glossary` and `upload_glossary` steps here.
graphql4.41 KB
---
name: graphql
description: Write valid, schema-aware Crowdin GraphQL queries and debug invalid ones. Use this whenever the user asks for Crowdin GraphQL queries, filters, pagination, sorting, or when they share GraphQL errors from Crowdin Playground (especially unknown argument/field errors) and need a corrected query that actually matches the schema.
---
# Crowdin GraphQL Query Writing
Use this skill to produce valid Crowdin GraphQL queries that match the live schema, avoid unsupported arguments, and include practical pagination/filtering patterns.
## What to Deliver
Return:
1. A working GraphQL query.
2. A short explanation of how it works.
3. If relevant, a corrected version of the user's broken query plus a brief "what was wrong".
4. Optional: variables object when it improves readability.
## Core Workflow
### 1) Confirm intent and scope
Identify:
- Target resource (projects, strings, translations, etc.).
- Required constraints (language, dates, status, user, approval state).
- Output fields user needs.
If the request is ambiguous, choose a conservative default and say so briefly.
### 2) Build from schema-safe primitives
Use this sequence:
1. Pick the root field that actually exposes the target connection.
2. Add required pagination arguments (`first` or `last`) on every connection.
3. Add nested selections (`edges -> node`) and only requested fields.
4. Add filtering/sorting only where the schema supports it.
Do not invent arguments by analogy with REST parameters.
### 3) Validate against common Crowdin GraphQL constraints
Before finalizing:
- Every queried connection has `first` or `last` (1..10000).
- Estimated total requested nodes stay under 10,000.
- Query includes `pageInfo` when pagination is likely needed.
- Query avoids unsupported arguments on a field.
### 4) Add safe pagination pattern
Prefer cursor pagination:
- Forward: `first` + `after`
- Backward: `last` + `before`
If user asks for "from date X to date Y", use supported `filter` input fields (for resources that expose them), not ad-hoc args.
## High-Risk Mistakes to Prevent
### Unknown argument errors
If the user query contains field arguments that are not in the schema, rewrite the query using supported alternatives.
Example anti-pattern from real failures:
- `translations(..., createdAfter: "...", createdBefore: "...")`
Fix strategy:
1. Remove unsupported args.
2. Keep supported args like `languageId`, `first`, `after`.
3. If date constraints are required, move them to a supported `filter` input on fields that provide one (or explain that this field does not expose date filtering directly).
### Missing required pagination arguments
Crowdin GraphQL connections require `first` or `last`. Never omit them.
### Over-fetching
Do not request large nested fan-outs without reason. Keep node count budgeted.
## Query Patterns
### Pattern A: Minimal safe connection query
```graphql
query GetProjects($first: Int!, $after: String) {
viewer {
projects(first: $first, after: $after) {
edges {
node {
id
name
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
}
```
### Pattern B: Filtering + ordering (where supported)
```graphql
query FilteredProjects {
viewer {
projects(
first: 10
filter: { createdAt: { gt: "2023-01-01T00:00:00Z" } }
order: [{ lastActivityAt: asc }]
) {
edges {
node {
id
name
lastActivityAt
}
}
}
}
}
```
### Pattern C: Rate limit awareness
```graphql
query MeAndCost {
viewer {
username
}
rateLimit {
limit
cost
remaining
resetAt
}
}
```
## Debugging Template
When the user provides an error, follow this output format:
1. **Cause**: one sentence pointing to schema mismatch.
2. **Fixed query**: full corrected query.
3. **Why this works**: 2-4 bullets tied to schema rules.
4. **If date filtering was requested**: explain whether that specific field supports date filtering; if not, propose nearest valid alternative.
## Tone and Safety
- Prefer correctness over being "clever".
- If the schema support is uncertain, explicitly say "verify in Playground Explorer for this field".
- Never pretend an argument exists.
## Reference
For constraints and examples, align with Crowdin docs:
- GraphQL API overview, pagination, filtering/sorting, and limits.
- https://support.crowdin.com/developer/graphql-api/
Referenced files: 1 — file references are metadata, not package contents.
how-to-crowdin21 KB
---
name: how-to-crowdin
description: Answers "how do I do X in Crowdin" by mapping a localization pain to the cheapest thing Crowdin already offers - a built-in feature, a few actions through the Crowdin connector or API, an app from the Crowdin Store, or a custom app - and handing the exact how to the skill that owns it. Use whenever someone describes a Crowdin or localization problem without naming a tool ("translators keep missing our terminology", "find strings changed since the last release", "QA passes but the German is wrong"), asks whether Crowdin can do something, asks which Crowdin app to use, or names a store app and wants to know what it does or whether they need it. Not for a crowdin CLI command or crowdin.yml (crowdin-cli), a CroQL expression already in hand (croql), a GraphQL query or Playground error (graphql), code against the API client (crowdin-api-client), building an app (create-app), or internationalizing a codebase (i18n-setup).
---
# How to Crowdin
Crowdin usually offers several ways to solve the same localization problem: a setting the user never found, a few actions on their strings, an app from the Crowdin Store, or something built for the job. Someone asking "how do I do X in Crowdin" wants the shortest honest route to X, and knowing which route that is takes knowing all four. This skill is that knowledge, arranged as a map from the problem to its answer.
You are a **triage layer**, not an execution layer. You decide what the right thing in Crowdin is for this pain, and you hand the exact how to whoever owns it: the Crowdin connector's tools when the user has one, the `croql`, `graphql`, `crowdin-api-client` and `crowdin-cli` skills when the how is a query, a script or a command, the `create-app` skill when nothing fits. You never improvise API calls or filter syntax that one of those owns.
## Who is asking
Usually a localization manager working through an AI assistant with the Crowdin connector, in Cowork, Claude.ai or ChatGPT. Sometimes a developer in a terminal. Assume no shell exists until you see one. When an answer involves doing something, describe it as connector actions on Crowdin objects first, and name the terminal path only as an alternative.
## The ladder
Every answer sits on one **rung**, and you try them cheapest first:
1. **Native feature** the user did not know about. Labels plus a filter, a QA check, a workflow step, a report, a setting.
2. **Connector actions.** A short sequence you perform on demand through the Crowdin connector or API, described at the level of Crowdin objects: filter strings, add a label, create a task, pre-translate a set, pull a report. The same sequence runs as REST calls in a terminal, so there is never a second version to write.
3. **Store app.** Installed from store.crowdin.com.
4. **Custom app.** Nothing fits; hand off to `create-app`.
The rung 2 test decides most disputes. A job is rung 2 when it is a read, a filter, a bulk edit or a report over Crowdin's own data, done when the user asks. It stops being rung 2 when it needs continuous watching (webhooks, schedules), an external system (a CMS, a repo host, a chat tool, a design tool), a file parser, an MT or AI provider connection, a UI inside the Crowdin editor, or a model pass over thousands of strings that the user needs again next week. Be honest in both directions: overstating rung 2 sends people to do by hand what an app does better, understating it pushes an install where a filter would do.
When rung 3 or 4 wins, say in one line each why the cheaper rungs lost. That line is what tells the user the answer was weighed rather than guessed, and it is what makes an install worth paying for when an install is genuinely the answer.
## The flow
1. **Restate the pain in Crowdin terms.** Users describe symptoms: "the German is wrong", "translators ignore our terms". Name the Crowdin objects underneath before you answer: strings, translations, TM, glossary, workflow step, QA check, task, report, integration. Done when you can say the pain in one sentence that uses those nouns. Ask one clarifying question only when two readings would land on different rungs; otherwise pick the likelier reading, say which you picked, and answer.
2. **Look the pain up in the map** below. A hit gives you the rung and the hand-off. Done when you have a rung and a hand-off, or a confirmed miss.
3. **On a miss, walk the ladder** yourself, cheapest first, using the rung 2 test above.
4. **Consult the live catalogs only for the rung that needs one.** `references/catalogs.md` says how. Rung 1 on a miss: the Crowdin Docs index. Rung 3: the store index, narrowed to the matching category, then one or two item files read in full. A tagline shortlists an app; only the item file recommends it. Done when what you are about to name is confirmed to exist and to do what you will claim.
5. **Hand off the how.** Connector actions stay at object level. Anything that needs a CroQL expression, a GraphQL query, a script or a CLI command goes to the owning skill by name, so it is invoked rather than reinvented here.
6. **Answer in the shape below.**
## Answer shape
Solution first, in one or two sentences. Then what it costs the user: nothing, an install, or a build. Then the steps, or the hand-off. When rung 3 or 4 won, one line per rejected cheaper rung. When the answer depends on the user's edition, plan, or what is already enabled in their project, say so rather than guessing; the catalogs cannot see their account, and neither can you without the connector.
## Guardrails
- **Bulk changes get a count and a yes first.** Adding a label to many strings, approving in bulk, cleaning a TM, deleting anything: name how many objects are affected and wait for confirmation. A filter that matched more than the user pictured is the common way an assistant damages a project.
- **Only name what the catalog confirms.** When the store has nothing for a pain, say so and move to rung 4 or to "not currently possible". An app that does not exist is worse than no answer.
- **Enterprise-only stays labeled.** Workflows, vendors, teams and groups exist only in Crowdin Enterprise. Say so when they are the answer, so a crowdin.com user is not sent looking for a menu they do not have.
## The map
Each row is a problem family: the pain in the user's terms, the cheapest rung that answers it, and what to do. "Connector" means actions through the Crowdin connector or API, described at object level. A row that names a store app has already had its cheaper rungs weighed, so confirm the app in the catalog and say what it adds. A row that says something "is an app" marks where the cheaper rung stops being enough.
### Finding, editing and organizing strings
| Pain | Rung | Do this |
|---|---|---|
| Which strings changed, or are new, since a date or release | 1 | Editor filter or CroQL on string dates; label the result if it must be reused |
| Label many strings at once | 2 | Filter with CroQL, batch-add the label; small sets: multi-select in the editor |
| Source copy is sloppy before it reaches translators | 1 | Enterprise: Source Text Review workflow step; elsewhere Replace in Sources with Keep Translations; a recurring ML review is an app |
| Fix or add source strings without losing their translations | 2 | Batch string edit with the translation-preserving option; one string: edit it in the editor |
| Duplicate strings are not detected | 1 | Project duplicates strategy; finding them across files is connector |
| New files should be translated into only some languages | 2 | Update each file's excluded target languages; one file: file settings |
| Where is this string or term used across all our projects | 2 | Organization-wide search, not per-project listing; TM segments per TM |
| Hand a specific subset of strings to someone as a task | 2 | Filter, label, create a task filtered by that label |
| Stakeholders need a subset as a spreadsheet or bilingual document | 2 | Filter, list translations per language, write the file locally; whole file: download translations |
| Personal data slipped into files sent for translation | 2 | CroQL on source text for email and phone patterns, label hits; blocking at import is an app |
| Sentences segmented wrong on import | 1 | Custom segmentation (SRX) import option; re-import existing files is connector; rule authoring is an app |
| Files do not import or export cleanly | 1 | Parser configuration and import options first; content rewriting in the pipeline is an app |
| Keys differ only by naming convention across platforms | 3 | Store: Converter for key conventions; nothing native normalizes keys |
### Context for translators
| Pain | Rung | Do this |
|---|---|---|
| Translators cannot see the screen a string lives on | 1 | Screenshots with auto-tagging; upload and tag on their behalf is connector; live design embeds are apps |
| Translators of web pages or documents cannot see the rendered result | 1 | In-Context localization and the editor's document preview; video timing is an app |
| Nobody wrote context and translators guess | 2 | List strings with empty context, derive meaning from the codebase, batch-edit context; in a terminal, `crowdin-context-cli` plus `context-extraction` |
| Which strings lack enough context to translate correctly | 2 | Coverage: filter empty context per file and report; sufficiency judged by a model at scale is an app |
| Spreadsheet has context columns Crowdin ignores on import | 2 | After import, batch-edit string context from the original sheet; on every import is an app |
| Reference material lives in other tools | 1 | Project description and file context carry links; rendering a board inside Crowdin is an app |
| ICU plurals, JSON or entities inside strings get in the way | 1 | ICU and tags QA checks catch broken output; per-string editor helpers are apps |
### Quality
| Pain | Rung | Do this |
|---|---|---|
| Translations are sloppy: punctuation, casing, spacing, spelling | 1 | Turn on the native QA checks in project settings; a cleanup of existing translations is connector |
| Numbers, colors, shortcuts, emoji or escape sequences got dropped or altered | 1 | Native Variables, Tags, Special characters checks; other token classes: a one-off audit is connector, live flagging is a store QA check (Enterprise) |
| URLs, emails, dates or units were copied instead of localized | 2 | Audit and bulk-fix a language's translations after review; live enforcement per language is a store QA check |
| Translation does not fit its UI element or caption timing | 1 | Per-string max length and the Length check; a whole-file character budget is connector; characters per second is a QA check |
| QA issues pile up and cannot be triaged | 2 | List QA issues by language, group by type, add false positives to the project dictionary in one batch |
| The same source is translated differently across the project | 2 | Report same-source, different-target groups per language; editor consistency hints exist natively for a limited language set |
| Spelling or grammar errors specific to a locale slip through | 1 | Native spell check, or a spell-check provider under organization settings; locale-convention QA as you type is an app |
| Offensive or off-policy wording in translations | 1 | Custom QA check with a word list (Enterprise); a model judging every new translation is an app |
| I cannot verify a language I do not speak | 2 | Back-translate or explain a bounded sample on demand; every export back-translated is an app |
| Human proofreading is the bottleneck | 1 | Proofread workflow step and tasks; a bounded review batch is connector; reviewing every new translation with AI is an app |
| I cannot see what proofreaders changed | 1 | Translator Accuracy report per translator; a per-string before-and-after is connector; deletions are in project Activity |
| Score translation quality against an LQA model | 3 | Store: Linguistic Quality Assurance (Enterprise); annotation lives in the editor |
| Suspect translations were pasted from an AI tool | 3 | Store AI-detection reports; provenance (who or which engine produced a translation) is connector |
| Check translations against our own rules or tone | 2 | Spot-check a file or language against the rule on demand; project-wide and repeatable is an app |
| Which MT or AI translations are safe to ship without a human | 3 | Store quality-estimation apps; crude triage (task for MT-only translations) is connector |
### Terminology, TM and style
| Pain | Rung | Do this |
|---|---|---|
| Glossary is empty or thin | 2 | `glossary-generation` from a terminal; through the connector, propose terms from source strings and add after review |
| Developers want the strings they just added translated now, consistently with our glossary and TM | 2 | `translate` from a terminal: drafts into the local resource files, uploads as unapproved suggestions; bulk work is pre-translation (`crowdin-cli`) |
| Glossary has source terms but no translations | 2 | Export the glossary, upload it as a source file to be translated, import back |
| Terms need adding or fixing while translation is underway | 2 | Find the concept, add or update the term; translators doing it inline need the editor app |
| No style guide, so tone and formatting drift | 2 | Infer rules from approved translations, write the native Style Guide with AI instructions |
| Translations went stale after a glossary or style guide change | 1 | Known term rename: Replace in Translations, or CroQL plus a label when each one needs review; a style rule or continuous watching is an app |
| MT or AI ignores our glossary and TM | 1 | Native AI prompts already receive glossary, TM and sibling strings; DeepL honors DeepL-side glossaries; syncing terms into DeepL is an app |
| Every project spawns its own TM and glossary | 1 | Assign the shared TM and glossary in project settings; merging strays is connector; automatic on project creation is an app |
| TM is bloated with duplicates | 3 | Store: TM Cleaner (keeps a backup); a small TM can be deduplicated by connector after an export |
| I need a filtered slice of the TM, not the whole export | 2 | CroQL over TM records (dates, usage, author), write out CSV; unfiltered: native TM download |
| Finished translations are not in a TM another project can use | 1 | One TM assigned to several projects; a TM from a subset of files is connector |
| Legacy translated documents or TMX are not in the TM intact | 1 | Native TM upload for TMX, XLSX, CSV; aligning document pairs or preserving prev/next context is an app |
| Translators want external dictionaries in the editor | 1 | Import the termbase as a glossary and it shows natively; live external corpora are apps |
| AI keeps repeating mistakes translators already corrected | 1 | Enterprise: AI Alignment drafts glossary terms from human translations; elsewhere compare AI output with approved replacements by connector; continuous learning from edits is an app |
### Tasks, people and workflow
| Pain | Rung | Do this |
|---|---|---|
| How much did TM, MT or AI save us; how much source changed; who reports issues | 1 | Project Overview reports; Enterprise adds Organization Reports across projects |
| I cannot see progress across all my projects | 2 | List projects, read per-language progress, one table sorted by completion; periodic email is an app |
| Every time developers push new strings I have to click Auto-Translate | 1 | Auto-Translate settings on crowdin.com; TM, MT or AI auto-translation workflow steps on Enterprise |
| Creating translation tasks for new content is repetitive | 1 | Task templates; "create tasks now for everything untranslated" is connector; on file upload is an app |
| Team workload, overdue tasks and turnaround across projects | 2 | List tasks across projects, compute overdue and elapsed time, summarize per person or vendor |
| The same settings, members and placeholders in every project | 2 | Read the reference project, patch each target project; full duplication with translations is an app |
| Translator issues are scattered and get lost | 1 | Editor filter for unresolved issues and issue notifications; cross-project triage list is connector; forwarding is an app |
| Nobody learns when a file, language or directory is done | 1 | Project notifications and native webhooks; directory-level completion and digests are apps |
| Crowdin events should reach Slack, Teams, Jira or a database | 1 | Native Slack integration and custom notification channels for personal alerts, native webhooks for everything else; catching them in another tool is Zapier, Make or n8n |
| Requests in our tracker do not become Crowdin tasks | 2 | Name the ticket, create the task for the right files, languages and assignees; unattended sync is n8n or Zapier |
| Workflow treats every string the same (routing, batching) | 3 | Store workflow-step apps (Enterprise): router, path filter, delay |
| Vendor or LSP works outside Crowdin | 3 | Store integrations (Trados, TextMaster, ProZ); the offline hand-off (export XLIFF, import back) is native |
| Colleagues need translations without becoming Crowdin users | 3 | Store: Translation Portal or Client Portal (Enterprise); an agent cannot be a surface for many people |
| Recognize, reward or pay contributors | 1 | Top Members report by period and language; ranking and CSV is connector; payouts, tokens and certificates are apps |
| How much time do translators spend | 1 | Time Spent report, fed by time logged in task comments; automatic timing in the editor is an app |
| We are new and do not know if the project is set up right | 1 | Project Advisors tab lists what is missing; a checklist against the user's own goals is connector |
| Run Crowdin by asking an assistant instead of clicking | 2 | This is the mechanism: Crowdin MCP Server for an outside assistant, Crowdin Copilot inside Crowdin |
### AI and machine translation
| Pain | Rung | Do this |
|---|---|---|
| Use a specific AI model or provider | 1 | Native AI provider settings, own key or Crowdin-managed; OpenAI-compatible endpoints via the base URL; others are store connectors |
| One AI prompt overloads the model and it ignores instructions | 1 | Trim the prompt, move rules into Style Guide and glossary, pick a stronger model; a multi-pass translate, verify and correct loop is the AI Pipeline app |
| One prompt cannot fit every language and file type | 1 | Several native prompts, chosen per pre-translation run; automatic routing or templating is an app |
| Cannot see what the AI was asked or what it costs | 3 | Store: AI Debug, AI Token Usage; being in the request path is what an agent cannot do |
| Which MT engine or AI prompt should we use | 1 | Auto-translation Accuracy report compares engines and prompts by post-editing; a controlled BLEU-style evaluation is an app |
| Repetitive chores after project events (file uploaded, create tasks) | 2 | On demand: list recent files, create the task; firing on the event without asking is the AI Automator app |
### Delivery and integrations
| Pain | Rung | Do this |
|---|---|---|
| Export only some languages or files | 1 | Build for chosen languages or download one file's translation; chaining several is connector |
| Untranslated strings should fall back to a language other than source | 3 | Store: Better Fallback; native fallback is to source only |
| Website has no localization layer and nobody can touch the code | 3 | Store: Website Translator (proxy, CDN, snippet) |
| Site visitors should be able to suggest better translations | 3 | Store: Website Proofreader |
| Audio or video needs dubbing or voiceover | 3 | Store: Dubbing Studio; only the subtitle text half is native |
| Connect a CMS, repository, design tool, storage, help center, marketing or e-commerce platform | 3 | Store category for that kind; native integrations exist for the major repository hosts and design tools, check the docs first |
| Crowdin does not parse my file format | 1 | Native format list and the generic JSON, XML, CSV and spreadsheet parsers with column mapping; then a store File Formats app; then `create-app` |
| Add a machine translation engine | 1 | Native MT engines under project settings; others are store Machine Translation connectors |
| Notify a chat tool | 1 | Native webhooks; Slack, Teams and Discord apps are in the store's Team Chat category |
| My tool has no Crowdin integration and I do not want to build one | 3 | Zapier, Make or n8n over native webhooks and the API; `create-app` only when those cannot express it |
## Hand-offs
| The how is | Owner |
|---|---|
| a CroQL expression, or a filter the editor cannot express | `croql` |
| a GraphQL query, or a Playground error | `graphql` |
| a script against the API | `crowdin-api-client` |
| syncing files, or anything `crowdin` on the command line | `crowdin-cli` |
| a GitHub workflow that syncs and opens the translation PR | `github-action` |
| writing translator context or a glossary from the source strings | `context-extraction`, `glossary-generation` |
| a Crowdin app, because nothing on rungs 1 to 3 fits | `create-app` |
| the whole journey from hardcoded strings to continuous translation | `i18n-setup` |
## Looking things up
`references/catalogs.md` says how to reach the live store and docs indexes, how to narrow them without reading everything, and what an item's `Type` tells you about its rung.
Referenced files: 1 — file references are metadata, not package contents.
i18n-setup28.5 KB
---
name: i18n-setup
description: Takes a project from hardcoded strings to internationalized, connected to Crowdin, and translating continuously. Use when someone wants to add i18n or multi-language support, internationalize an app, extract hardcoded strings, set up Lingui (or another i18n library), or connect Crowdin to a repository - including connect-only, "my app already uses i18next/next-intl/vue-i18n/Lingui, wire it to Crowdin". Not for individual `crowdin` CLI commands or crowdin.yml syntax (that is `crowdin-cli`), not for editing Crowdin context files or `ai_context` on their own (`context-extraction`, `crowdin-context-cli`), not for generating a glossary alone (`glossary-generation`), and not for building Crowdin apps (`create-app`).
---
# i18n Setup
This skill orchestrates the journey: the phase sequence, the decisions, and the filesystem workspace those decisions live in. Library-specific work runs through the library's own skills, routed by `manifest.json`; this skill does the Crowdin side of each phase and verifies the delegated work on disk. For any `crowdin` command's syntax and semantics, read the `crowdin-cli` skill.
Load references lazily: read `manifest.json` when phase 2 starts, and read a reference only when the step that needs it is the step you are on. A phase you skip should cost nothing.
## The journey
| Phase | Name | Produces | Credentials |
|---|---|---|---|
| 1 | Detect | `.crowdin/detection.json` | none |
| 2 | Decide | `decisions.md`, `manifest-snapshot.json`, `plan.md` | none |
| 3 | Setup | i18n library installed, configured, catalogs scaffolded, `.agents/crowdin-i18n-rules.md` rendered | none |
| 4 | Wrap | every user-visible string wrapped, catalogs populated, build green | none |
| 5 | Connect | `crowdin.yml`, verified config, sources uploaded | **token gate** |
| 6 | Context | `ai_context` written for the new strings, a starting glossary uploaded | token |
| 7 | CI | sync workflow + the secret instruction for the user | token (as a CI secret) |
Two entry points share this spine, and phase 2 decides which: the **full journey** runs all seven phases; **connect-only** — the project is already internationalized — omits phases 3–4 entirely rather than emitting no-op steps. Everything before phase 5 is offline; the token gate opens phase 5 — say so at the end of phase 2, so the user can authorize the CLI while phases 3–4 run.
## Execution model
> The plan is a linear checklist. Execute steps in order on the main thread. If your environment provides a subagent/task tool, you may dispatch the convert phase's file partitions as parallel workers that write `progress/wrap-N.json`; collect them all before the verify step. Never require this: a run with no subagent support must produce an identical end state, just serially.
Two rules are correctness, not architecture, and hold in both modes:
- **Extraction runs once, after all wrapping is done.** Parallel writers pointed at one catalog race and lose messages. Wrap every file first, then extract a single time.
- **Ambiguity is stop-and-ask, never improvisation.** If a reference does not cover the case in front of you — an odd config layout, two i18n libraries present, a string you cannot tell is user-visible — stop and ask. Guessing produces plausible code that breaks a build the user has to debug later.
Package installs always run on the main thread, never inside a worker — including when a delegated skill does the installing — so the lockfile stays consistent.
## The workspace
```
.crowdin/
detection.json # phase 1 output: stack, existing i18n, candidate files, locale signals
decisions.md # frozen user choices: entry point, locales, library, scope, add-ons, mode
plan.md # the executable checklist ("- [ ] step_id" lines)
manifest-snapshot.json # verbatim copy of the matched stacks[] entry + the catalogs[] rows it uses
progress/ # ONLY when parallel wrap workers are dispatched (wrap-N.json)
```
Rules for it:
- **Create and ignore it on first run.** Add `.crowdin/` to the repo's `.gitignore`. If `.gitignore` already has content, show the one-line diff you intend to make before making it. The rules artifact is *outside* the workspace: `.agents/crowdin-i18n-rules.md` is a committed project file.
- **Ticking checkboxes in `plan.md` is the progress protocol.** Check each step off as it completes — `- [ ]` becomes `- [x]`. In the default linear run that is the entire bookkeeping story, and it is enough to resume from. A `progress/` directory exists only when you actually dispatched parallel wrap workers; a serial run never creates one.
- **Resume, don't silently re-plan.** If `plan.md` already exists when you start, report how far the previous run got (ticked versus unticked steps) and ask whether to resume from the first unticked step or restart from detection, which rewrites `detection.json` and `plan.md`. Unguided runs resume.
- **Write atomically.** Every workspace file is written as `<name>.tmp` and then `mv`'d into place, so an interrupted run never leaves a half-written JSON file that the next run parses as truth.
- **Disk beats records.** When a recorded value disagrees with the repository as it is right now — a path in `decisions.md` that no longer exists, a locale directory someone deleted — the disk wins. Re-resolve the value, note the correction in `decisions.md`, and keep going.
## Phase 1 — Detect (read-only)
Phase 1 writes exactly one file, `.crowdin/detection.json`, and touches nothing else in the repository. No installs, no edits, no `git` writes.
Route by language on first match, then follow that language's rules file:
| Signal at the repository root | `language` | Detection rules |
|---|---|---|
| `package.json` | `js-ts` | `references/detect/js-ts.md` |
| *no row matches* | — | Stop: "This project's language isn't supported yet." Name what is (JavaScript/TypeScript today), and offer connect-only if translation catalogs already exist that Crowdin can take as-is. |
Detection *rules* are in the per-language reference (adding a language family: `references/extending.md`).
Emit this schema. The field names and the value sets are load-bearing: phase 2 matches `stacks[]` entries against these exact fields, and the collapse ladder reads these exact values. Renaming one silently breaks routing rather than failing loudly.
```json
{
"language": "js-ts",
"framework": "vite" | "next" | "cra" | "unknown",
"compiler": "swc" | "babel" | null,
"react": true, "typescript": true,
"packageManager": "npm" | "yarn" | "pnpm" | "bun",
"sourceDir": "src",
"git": { "isRepo": true, "branch": "main", "remote": "..." },
"existing": {
"library": "lingui" | "next-intl" | "vue-i18n" | "i18next" | "none",
"configured": false, "stringsWrapped": "yes" | "partial" | "no",
"crowdinYml": false, "workflow": false
},
"candidateFiles": [{ "path": "src/App.tsx", "matchCount": 7 }],
"localeSignals": { "existingLocaleDirs": [], "readmeHints": [] }
}
```
The `|` unions above are the allowed value sets, not values: write literal JSON, one concrete value per field, never the union text itself. Every key is required — `null`, `"none"`, `[]` for the absences. Report uncertainty in the prose summary, never as an invented value or an extra key.
Then summarize what you found for the user in a few lines — stack, existing i18n state, how many files carry candidate strings — before asking anything.
## Phase 2 — Decide
Everything in this phase is a decision, not an action. Nothing is installed and no project file is modified until the plan is approved.
**1. Resolve the entry point.** If `existing.library != "none" && existing.stringsWrapped != "no"`, the project is already internationalized: offer **connect-only as the default** and the full journey as the alternative (which would mean migrating libraries — a real ask, never a default). Otherwise the full journey is the default. An existing foreign i18n library is never a reason to refuse; it is the reason connect-only exists.
**2. Check the hard stops.** First match wins, and this check happens *after* `detection.json` is on disk and *before* `plan.md` is created.
| Condition | Verdict |
|---|---|
| A build toolchain that cannot be extended without ejecting from it — today that means `framework: "cra"` (`react-scripts` in dependencies) | **Stop.** Wrapping needs a build the setup delegation can add a transform to; a toolchain that forecloses that has no path in, and stopping before anything is installed beats a half-configured project. Use the message below. |
| No `stacks[]` entry matches the detected stack **and** `existing.library` is `"none"` | **Stop.** There is no conversion pipeline for this stack and no catalogs to connect. List the stacks that are supported and the catalog formats connect-only accepts, so the user knows which of the two gaps to close. |
| Native/hybrid shell signals (`react-native`, `expo`, `@capacitor/core`) next to a web entry point | **Not a stop.** Route the web path and say plainly that the native shell's own strings are out of scope for this run. |
| `existing.library != "none"` | **Not a stop.** This is connect-only. |
A hard stop still writes `detection.json` — that file is the evidence for the refusal — and writes nothing else: no `plan.md`, no `crowdin.yml`, no installs, no change to `package.json`. Print the message, then stop. Do not offer to proceed anyway.
Say it in full rather than in initials, so the user can search for the toolchain by name. For the first row:
> This project is built with Create React App (`react-scripts`), which this skill can't internationalize. Wrapping strings needs a build the i18n library can add its transform to, and CRA's is closed to that without ejecting — on top of which the toolchain has been unmaintained since 2023. Move to a build that accepts plugins, such as Vite or Next.js, and run this again — the rest of the journey works unchanged afterwards. If the app already has translation catalogs, I can skip the conversion and connect them to Crowdin instead.
**3. Collect the locale set, the code scheme, and the mapping.** Ask for the source locale and the targets, then freeze in `decisions.md` not just which locales but how they're spelled and which `languages_mapping` rows that spelling needs — phases 3 and 5 both read all three. The `translation` pattern's language placeholder is always **`%locale%`**. **Greenfield default spelling: each target's Crowdin locale code** (`uk-UA`, `pt-BR`, `es-ES`) — exactly what `%locale%` resolves to, so the default needs no `languages_mapping` at all. Read the code per language from `crowdin language list --all --code locale -o toon`: the full supported-language list, needing no token and no `crowdin.yml`, which also validates unfamiliar codes before credentials exist. A machine without the CLI yet runs phase 5's CLI preflight early — that install is global, not a project change.
**Record two codes per language, not one.** `%locale%` resolves to the **locale code**; the CLI's `-l/--language` options take the **language id**, and so do `languages_mapping` keys and the glossary's source language in phase 6. The id is its own field, not a prefix of the locale — many ids carry the region too (`es-ES`, `pt-BR`, `zh-CN`). `crowdin language list --all` returns ids by default and locale codes with `--code locale`; run it both ways and join the two lists on the language name.
**Two cases override the default spelling:** the user asks for specific codes (URL segments like `/uk/`, an in-house convention), or the project's locale directories already spell them (`detection.json`'s `localeSignals.existingLocaleDirs`). Keep that spelling and record one `languages_mapping` row per target whose code differs from its Crowdin locale code — never rename a project's directories to dodge a row. `references/connect.md` and the `crowdin-cli` skill's configuration reference own the mapping mechanics; this file doesn't repeat them.
**4. Ask which Crowdin edition.** crowdin.com or Crowdin Enterprise. Enterprise means one extra recorded value — the organization — which becomes `base_url: https://{org}.api.crowdin.com` in `crowdin.yml` and, when a personal access token is used, where it is created. Nothing else in the journey differs.
**5. Confirm the remaining choices**, then freeze all of them in `decisions.md`: the app's one-line product description (phase 6 passes it to the context skill), the scope of files to wrap, whether to work on a new git branch, mode (**guided** — confirm at each phase boundary; **unguided** — run the approved plan to completion, stopping only on ambiguity or a hard stop), and add-ons. Add-ons are off by default, including `crowdin auto-translate`: never machine-translate a fresh project unasked, and when the user does opt in, `--method tm` only. A second add-on, **draft translations**, is never pitched here: it is recorded only when the user asks for translations during the journey, and it adds a `draft_translations` step to phase 6 that delegates to the `translate` skill.
One add-on has to be *asked* rather than left off, because a delegated step installs it either way: the **unwrapped-string lint rule** that phase 4's recall delegate leaves behind as a permanent dev dependency. Its own skill recommends the install and treats the no-install path as the lesser one, so a plan that never raises it forces this orchestrator to answer a dependency question on the user's behalf, silently. Put it to them at Decide — the rule catches the next unwrapped string, the audit alone catches only today's — and record either answer, so phase 4 hands the delegate a decision instead of inheriting one.
**6. Match the manifest.** Compare the structural keys of each `stacks[]` entry's `match` object against the equally-named fields of `detection.json`. An entry matches on `language` and `library`, nothing more; `framework`, `compiler`, `react` and the rest are context to hand the delegation, never keys to route on. The `library` key is the choice being offered, so it is matched against the user's answer, not against existing state. Then write `manifest-snapshot.json`: the matched entry verbatim, including its `variant`, `catalog`, `skills` block, and `references` paths, plus the `catalogs[]` row named by `catalog` (with its `model`, `source`, `translation`, `type`, and `docs`). From here on, read paths, recipes, and delegation targets from the snapshot, not from the live manifest — a run planned against one manifest must execute against that same manifest.
While the snapshot is open, probe whether the skills its `skills` block names are loadable here, and record the answer in `decisions.md` with the install command the user would need. This is a note, not a gate: **planning never blocks on skill availability** — the gate is `verify_skill_dependencies` at the top of phase 3; recording it now lets the plan warn the user in advance instead of stopping them later. In `plan.md` the warning rides the step's own line — `- [ ] context_download — crowdin-cli not loadable here; npx skills add crowdin/skills --skill crowdin-cli` — the step id and checkbox stay canonical, never replaced by a pause marker.
**7. Emit `plan.md` as a strict checklist.** Every step is one line that *begins* with its id:
```
- [ ] step_id — short human-readable detail
```
The id starts the line, immediately after the checkbox. Anything a human or a later step needs goes after the id on the same line or in indented sub-bullets — never before it, and never instead of it. Group the lines under `## Phase N — Name` headings.
The step vocabulary is keyed by the matched catalog's `model` from the snapshot.
Shared by every stack:
| Phase | Steps |
|---|---|
| 3 Setup | `verify_skill_dependencies` first, then `install_packages`, `create_config`, `provider_wiring`, `scaffold_catalogs`, `generate_coding_rules`, `install_coding_rules`, `build_verification`, `commit_setup` |
| 4 Wrap | one `wrap_<path>` step per file (`wrap_src_App_tsx`) — non-component modules (data and util modules, API routes: files that render nothing) come last, behind a single `module_i18n_approach` step that decides once how an i18n instance reaches non-render code, because per-file improvisation here is how threading goes subtly wrong — then `build_check`, `recall_self_check`, `comment_review`, `commit_wrap` |
| 5 Connect | `write_crowdin_yml`, `config_lint`, `config_sources`, `config_translations`, `upload_dryrun`, `upload_sources`, `commit_connect`, plus optional `create_project` |
| 6 Context | `context_download`, `fill_ai_context`, `context_upload`, `context_status`, `generate_glossary`, `upload_glossary`, plus `draft_translations` only when the add-on is recorded |
| 7 CI | `write_workflow`, `commit_ci`, `set_secret_instruction`, `prove_round_trip` |
Added by `model`:
| `model` | Adds |
|---|---|
| `compile-time-extraction` | `extract_smoke` in phase 3 (prove extraction works before wrapping anything), then in phase 4, after every file is wrapped: `extract_clean`, then `catalogs_current` — the catalogs the app loads agree with the source catalog extraction just rewrote. `catalogs_current` is an outcome, not a command: some pipelines reach it through a compile step, others compile at import and reach it by doing nothing, and which one this project has is the setup skill's business, not a field in this manifest |
| `runtime-catalog` | `verify_catalogs` in phase 4 instead of all three — nothing extracts, so nothing keeps the hand-authored catalog in step with the code. The check is that every key the code references resolves in the source catalog; `references/verify-catalogs.md` says what to compare, what a missing key costs once phase 5 has uploaded, and why the count of keys it could not resolve statically is part of the result |
`verify_skill_dependencies` leads phase 3 because phases 3–4 are delegated to the ecosystem skills the snapshot names, and a plan that starts installing before confirming its dependencies are loadable can strand a half-configured project. Connect-only plans have no phases 3–4 and therefore never emit it — they delegate nothing.
`create_project` runs first within phase 5 when it runs at all, because `write_crowdin_yml` needs the numeric project id; when the project already exists the user supplies that id instead. The other six phase-5 steps keep the order shown — that sequence is the safety gate, not a suggestion.
Every phase that writes repository files ends with its commit step — `commit_setup`, `commit_wrap`, `commit_connect`, `commit_ci` — committing that phase's files on the branch `decisions.md` names. The approved plan is the consent for these commits, so none of them asks again, and a run that stops mid-journey loses at most one phase of work. Phase 6 writes only to Crowdin and the gitignored workspace, so it has no commit step, except that `draft_translations`, when recorded, commits the target catalogs it drafted into, so phase 7's round trip starts from clean targets; a phase whose files turn out untouched (a collapse-ladder omission) skips its commit the same way.
**Before writing `plan.md`, read `references/plan-format.md`** — the format contract as one full worked `compile-time-extraction` plan — and emit yours in exactly that shape.
**8. Get an explicit go.** Show the plan and state what it will change — and when the snapshot's `supportLevel` is anything but `stable`, say so plainly: the user is approving a less-proven path. Then wait for approval. No step executes before that word. In an unguided run the go covers the whole plan; in a guided run it covers the next phase.
## Phases 3–4 — Setup and Wrap (delegated)
Phases 3–4 **delegate**: installing an i18n library, wiring its compiler plugin, and wrapping strings in its idiom are the library ecosystem's job, maintained against every release. This orchestrator supplies the Crowdin-side constraints the library's skills cannot know, then checks the result on disk.
Read `references/delegate.md` when phase 3 starts — it is the whole procedure: the `verify_skill_dependencies` gate, the adapter handoff (`references.adapter` in the snapshot: locale codes, catalog layout, format, and the post-condition checklist), the constraints stated in each invocation, the per-role invocations, the post-condition verification on disk, and the `generate_coding_rules` render. One of its rules binds even before it is read: **never substitute your own library guidance for a missing skill** — not a summary of it, not "the general approach". A missing dependency makes the correct outcome a paused run, never an improvised one.
## Phase 5 — Connect
`references/connect.md` covers the CLI preflight, `crowdin.yml` authoring, the token gate, project creation or verification, and every gate's expected output. The order of the gates is fixed and does not vary by stack:
```
crowdin config lint → crowdin config sources → crowdin config translations → crowdin upload sources --dryrun → real upload
```
Only `config lint` answers without credentials; the other three all reach the project, which the gate order absorbs — the token and project id exist before any of them runs. `crowdin config translations` is the kill switch: it resolves the `translation` pattern's `%locale%` placeholder — through the `languages_mapping` rows, if any were decided — against the project's actual languages, printing the per-language paths the pattern resolves to so you can compare them against the layout phase 3 created. Only after all four pass does `upload_sources` run for real.
**The agent never sees the token value.** Not in a prompt, not in a variable it echoes, not in a file it writes. `crowdin.yml` always carries `api_token_env: "CROWDIN_PERSONAL_TOKEN"` and never a literal token; `project_id` is committed, because it is not a secret. `connect.md`'s token gate covers how a token is detected, how the user obtains one — `crowdin login` in their own terminal by default — and how it is verified without ever being read.
## Phase 6 — Context
Context is the part of this journey a human would skip and a translator would miss most. It is a handoff between two existing skills, run after the first successful upload:
1. `crowdin context download` — every string, with no `--status` filter. Command surface: **`crowdin-context-cli`**. Filtering here is a trap in both directions, because `--status` describes *which kind* of context a string already has, not whether it needs AI context: `empty` means no context of any kind, so it matches nothing on a format that carries source references into Crowdin — a PO upload puts every entry's `#:` reference in the string's context, which is exactly what the adapter's reference-comment constraint guarantees — while `manual` matches nothing on a format that carries none. Downloading everything costs one call and avoids having to know which case this project is in. Nothing is over-written as a result: `context upload` writes only the records whose `ai_context` the next step actually filled.
2. Invoke **`context-extraction`** to fill `ai_context` in the downloaded JSONL, passing what this skill uniquely knows: the app's one-line product description from `decisions.md`, and the `#:` source references that came along with the strings (PO uploads carry them into the JSONL `context` field, so the extraction skill can read the actual code around each string instead of guessing).
3. `crowdin context upload` — push the enriched file back.
4. `crowdin context status` — report coverage to the user.
Then the glossary, in the same phase because it needs the same two things: the project to exist, and the catalog already uploaded. `generate_glossary` and `upload_glossary` delegate to the **`glossary-generation`** skill, which drafts the terms, holds the review gate, and runs the idempotent upload. `references/glossary.md` is the handoff: what to pass it, and what to record when it returns.
When `decisions.md` records the draft-translations add-on, `draft_translations` closes the phase, after the glossary exists to be respected. It delegates to the **`translate`** skill, which syncs, drafts into the local target files, verifies, and uploads the drafts as unapproved suggestions. `references/translate.md` is the handoff.
## Phase 7 — Continuous sync (CI)
Delegated, like phases 3–4. The **`github-action`** skill writes the workflow — its inputs, the version to pin, and everything that only fails once a run is on a runner. `references/continuous-sync.md` covers what comes before and after: the decision (a workflow, or Crowdin's native GitHub integration), the gate that runs before anything is written (`git ls-remote origin 'l10n*'` — an existing `l10n*` branch means an automation already syncs this repository, and a hit is a stop-and-ask), the handoff, and the post-conditions to read back.
`write_workflow` then delegates, and `set_secret_instruction` asks the user to set `CROWDIN_PERSONAL_TOKEN` as a repository secret themselves — same rule as phase 5, the value never passes through the conversation. The secret is a personal access token; `continuous-sync.md` says when the user has to create one first.
`prove_round_trip` closes the journey, and it is the last step for the same reason `extract_smoke` is an early one: **every step before it can pass while the app still shows one language.** Translate a single string in the Crowdin project — one word is enough, by hand in the editor — let the sync bring it back, and see it render. That exercises the whole chain end to end: the catalog the workflow downloads, the path `%locale%` resolved to, the macro the setup phase wired, and the locale the app negotiates. Nothing else does; a green build, a full catalog, four passed gates and a written workflow are all compatible with a project that will never display a translation.
It needs the user twice — they hold the token, and they own the translation — so it is a step to *ask for and wait on*, not one to report as done. When they would rather stop, say plainly what is unproven and leave the step unticked: an honest 12-of-13 beats a complete-looking plan over an untested pipeline.
## Collapse ladder
Re-running this skill on a repository it already touched must be safe and quiet. Each rung reads its condition from `detection.json`:
| Already true | Do this instead |
|---|---|
| `existing.stringsWrapped: "yes"` | Skip phase 4. There is nothing to convert. |
| `existing.configured: true` | Verify the config against the adapter's post-conditions and repair only what is wrong. Do not recreate it, and do not re-run the setup delegation over a working config. |
| `existing.crowdinYml: true` | Run `config lint` plus the sources/translations gates against the existing file and fix mismatches. Do not regenerate it — and **the plan omits `write_crowdin_yml` entirely**, keeping the gate steps. A checklist line named for writing a file that nothing will write is a lie the executor of that plan may act on. |
| `existing.workflow: true` | Leave the workflow alone; report its trigger set. **The plan omits `write_workflow`** for the same reason. |
| All of the above | Still write `plan.md`, and still run `config_lint` (plus `config_sources` and `config_translations` where credentials are available) against the existing config as the verification that nothing has drifted, before reporting `crowdin status` and offering only the genuinely missing add-ons. "Change no files" means the *project's* files — planning and read-only verification are not project changes. |
## Extension contract
A stack is a **language family plus a library** (`js-ts-lingui`), never finer — frameworks are the delegated ecosystem's routing, not manifest keys. What a new stack adds, and which shared files it never edits, is `references/extending.md`; nothing there runs during a setup journey.
## Related skills
Phases 5–7 name their Crowdin-side delegates in place: `crowdin-cli` (the authoritative surface for every `crowdin` command and for `crowdin.yml` syntax, placeholders, and language mapping), `crowdin-context-cli` and `context-extraction` (phase 6's context work), `glossary-generation` (the starting glossary), `translate` (phase 6's optional drafts), and `github-action` (**required** for phase 7's default path). Library-side, the `lingui` plugin from [lingui/skills](https://github.com/lingui/skills) is **required** for phases 3–4 of a Lingui stack; which of its skills covers which role — setup, convert, recall — is the snapshot's `skills` block, and the install commands live in the manifest and `references/delegate.md`. One boundary note: a project already on i18next whose owner wants Lingui is a library migration (upstream's `migrate-i18next-to-lingui`), out of scope for this orchestrator — its i18next path is connect-only.
Referenced files: 15 — file references are metadata, not package contents.
translate16.4 KB
---
name: translate
description: Drafts translations into the project's own resource files from the terminal and uploads them to Crowdin as suggestions for review. Use when a developer asks to translate their app's strings, fill the missing translations for a locale, or translate the catalog into a newly added language. Not for prose documents, not for Crowdin's server-side pre-translation (`crowdin auto-translate`, in crowdin-cli), not for the full internationalize-and-connect journey (i18n-setup, which delegates here).
---
# Translate
A developer adds twelve strings and wants to see them in German today, not after the next translation cycle. This skill is the translator seat for that moment: it drafts translations into the project's own resource files, keeps them consistent with what the project's Crowdin resources already say, checks them mechanically, and uploads them to Crowdin as **unapproved suggestions** for reviewers to decide on.
Its one real advantage over Crowdin's server-side pre-translation is that the agent has the code. A source reference or a translator comment leads to the component a string renders in, and that is where `Order` turns out to be a button rather than a noun. For bulk work that advantage costs more than it earns, and the skill says so (see "Large runs").
Every `crowdin` command below belongs to the `crowdin-cli` skill — syntax, credentials, exit codes, `crowdin.yml`. This file names only the flags it passes.
## Preconditions
- **`crowdin.yml` exists** in the working directory. Without it, stop: say that the skill would be drafting with no glossary, no TM and no review in Crowdin, and offer the `i18n-setup` skill's connect-only path instead.
- **The CLI is installed and authorized.** Exit code `101` means the user runs `crowdin login` in their own terminal; the token value stays out of the conversation.
- **The CLI is current.** `--assigned`, `language list --verbose` and `style-guide` are what this skill leans on; an unknown-option or unknown-command error means an older install; upgrade it the way it was installed (the `crowdin-cli` skill has the line for each method).
## The run
Six steps, in order. Steps 1–3 and 5–6 run on the main thread; step 4 may fan out (see "Large runs").
### 1. Resolve
- **Files.** `crowdin config sources` and `crowdin config translations` print the resolved local source and translation paths per language, with any `languages_mapping` from `crowdin.yml` already applied. The path is the only source of a language's locale.
- **Languages.** The ones the user named, otherwise every project language. Run `crowdin language list --verbose -o json` once and keep two fields per language: `code`, which under the default `--code id` is the Crowdin language id that `-l` takes, and `pluralCategoryNames`, which feeds the plural check in step 5. `code` is the only source of the id; a mapping changes path placeholders, and the id stays what the CLI printed.
- **Gaps.** One rule for every format: an entry present in the source file with no non-empty counterpart in the target file, in that format's own notion of an entry, plus any entry the format marks as needing work (a fuzzy PO entry). A target file that does not exist yet is all gaps. [references/formats.md](references/formats.md) shows the rule in each common format family, the fallback for one it does not list, and the library checks that count gaps for you where they exist (Lingui's `lingui check missing --locale <locale> --mode catalog`).
Done when every language in scope has a target path, an id, its plural categories and a gap count.
### 2. Sync
Two commands, in this order, for the languages in scope:
```bash
crowdin upload sources --cache
crowdin download translations -l <language id> --skip-untranslated-strings
```
Before the upload, a library that can prove its source catalog is current gets asked, about the source catalog only: `lingui check sync --locale <source locale>` for Lingui 6.8 or later, and a failure means `lingui extract` first, so the strings that reach Crowdin are the ones in the code. [references/formats.md](references/formats.md#library-checks) has why the check is asked about the source locale only, and the fallback for an older Lingui. Upload first, so every string about to be translated exists in Crowdin — translations uploaded for a string Crowdin has never seen are ignored. Download second, so the local files reflect what reviewers have already decided. `--skip-untranslated-strings` makes untranslated entries come back absent instead of filled with source text; without it a key-value gap becomes invisible. When the project syncs a Crowdin branch, both commands take the same `-b`; the user or the CI workflow says whether it does, and when neither does, ask.
**Gate:** no target file holds unsaved translation work before the download, because the download replaces the file whole. Work means a non-empty translation value that differs from git, or one git has and the file no longer does: run `git diff -- <target files>` and look for a removed or changed value (`-msgstr "…"` with text in it, a changed JSON or XML value). Whitespace and blank-line differences, header and metadata stamps (`PO-Revision-Date`, `X-Generator`, `X-Crowdin-*`) and new entries with empty values are not work, so the rewrite `lingui extract` gives every locale's catalog passes this gate on its own; say in one line that it was seen and carry on. Real work is a stop-and-ask. For a [multilingual file](references/formats.md#multilingual-files) the target is the source file itself, so the gate applies to it.
Then recount the gaps and show a table per language. Strings a human translated since the last pull are no longer gaps. Server-side TM matching (`crowdin auto-translate --method tm`) is a different tool, owned by the crowdin-cli skill.
### 3. Fetch resources
Only what the run needs, into `.crowdin/resources/` (gitignored — add `.crowdin/` to `.gitignore` if it is not there, showing the one-line diff first). Files already present are reused; the user asks for a refresh by saying so. [references/resources.md](references/resources.md) has the commands and the lookup pattern.
| Resource | When | Why |
|---|---|---|
| The target catalog's own translations | Always | Same product, same language, same voice — the strongest consistency signal, and free. |
| Glossary | Always | Small; a term decided once, everywhere. |
| TM language pair | When the target catalog is thin (fewer than roughly fifty existing translations), or the user wants consistency with legacy content | Grepped for a phrase, a few rows at a time. |
| Style guide | Always, when one is assigned to the project and covers the language | Tone, formality, formatting conventions. |
### 4. Draft
Per string, the sources of truth apply in this order, and an earlier one wins:
1. **Glossary term.** A term in the string binds its translation; a do-not-translate term stays in the source language verbatim. A term with no translation in the target language falls under the glossary rule below.
2. **Existing translation** of the same source string elsewhere in the target catalog, or of a near-identical one. Match it.
3. **TM hit.** An exact source match is reused as is; a partial match is a style reference.
4. **Context.** The translator comment, the `ai_context` if the project wrote one, and the source reference — which the agent follows into the code when the string is ambiguous.
5. **Style guide**, for tone, formality and formatting conventions.
Before the first draft in a language, settle what those sources leave open, once, and apply it everywhere: the register (`du` or `Sie`), one rendering for each recurring term the glossary does not cover, and number, date and unit formatting. A literal amount, date or number in a string is written the way the target locale writes it (`€48,000 – €60,000` is `48.000 € – 60.000 €` for Spain); only an identifier stays as it is (a postal code, a version, a unit tag such as `1 TB`). A run with a placeholder style guide, an empty TM and a glossary without a column for the language still has to render "workspace" somehow; choosing one rendering and holding to it is translating a word consistently, which the glossary rule allows. The choices go into the report as decisions for the reviewers, and into the conventions sheet when the run fans out.
Every draft is recorded first as one line in the language's ledger, `.crowdin/translate/drafts-<language id>.jsonl` ([format](references/formats.md#the-drafts-ledger)), then written into the target file in place, mirroring the source file's structure: same entry order, quoting, indentation, headers and comments. A new target file mirrors the source file's structure entirely. The source catalog stays untouched (in a multilingual file, its source-language entries). A ledger already present for the language from a run that stopped before upload is the resume point: verify and upload from it, or start over on the user's word.
If the project's own scripts run a catalog compile (a `compile` script in `package.json`, a command the rules file `i18n-setup` rendered names), run it after the write so the running app shows the drafts; the library's own skill names the command. A project whose bundler plugin compiles the catalogs at build time has no such step, and there the compile happens once, as the validator in step 5.
Done when every gap in the count has exactly one ledger line, a draft or a skip with a reason, and the target file holds every draft.
### 5. Verify
Three checks, all mechanical, before anything is uploaded:
1. **Placeholders and plurals**, over the ledger, with the `scripts/check-drafts.sh` that ships beside this file:
```bash
scripts/check-drafts.sh .crowdin/translate/drafts-uk.jsonl --categories one,few,many,other
```
`--categories` is the language's `pluralCategoryNames` from step 1. Exit `0` is clean; `1` prints one line per mismatch; `2` means `jq` is missing or the ledger is not valid JSONL — then review the placeholders by hand from the ledger and report the check as not run.
2. **Format validity** of every touched target file, with the validator the family's row in [references/formats.md](references/formats.md#the-families) names: the library's own compile first (`lingui compile` for Lingui, whether or not the project runs it as a build step; delete its output files afterwards when the project does not), else the format tool. A validator that dies on a large file (`msgfmt -c` killed in a sandbox) counts as unavailable: fall through to the next. When none is available, read the file back and report the check as not run. Where the library also counts gaps (`lingui check missing --locale <locale> --mode catalog` for Lingui), its exit `0` after the write is the proof that none was left behind.
3. **PO plural header.** For PO, the target file's `Plural-Forms` header must agree with the language's categories; a disagreement is reported, and the header stays as it is.
Then a table per language — drafted, skipped (with the reason), flagged — and flagged entries are fixed or reverted to gaps. They never ship.
### 6. Upload
```bash
crowdin upload translations -l <language id>
```
Once per touched language, with the same `-b` as the sync. Never `--auto-approve-imported` (drafts are suggestions). `--import-eq-suggestions` only when the language's ledger holds drafts deliberately identical to their source (a unit such as `1 TB`, an address, a product name in a template): without it those never reach Crowdin, Crowdin keeps showing them as untranslated, and the next run drafts them again. The flag is safe here because the download skipped untranslated strings, so nothing else in the file equals its source. The report lists them as kept in the source language. Show the per-language counts and get a go first; in an `i18n-setup` run the approved plan is the go. Close with `crowdin status` for the languages touched.
Say two things plainly in the report: the suggestions appear under the account that owns the token, because the CLI has no way to mark a suggestion as machine-drafted; and reviewers will find them as ordinary unapproved suggestions. Offer, once, a proofread task per language — `crowdin task add <title> --type proofread --language <id> --file <path>` — as the explicit handoff to a human. It is an offer, not a default.
## The rules
These are hard, because they are where agent translation actually breaks production.
- **Placeholders are byte-identical.** `{count}`, `{0}`, `%s`, `%1$d`, `{{name}}`, `<0>…</0>`, `#` in a plural form: same spelling, same count, same case. Reordering to fit the target grammar is fine; renaming is not. In a plural, a target form carries exactly the placeholders of the source form with the same name, so a `one` form keeps its number (`# Element`, not `Ein Element`) and a `one` form whose source spells the number out (`One other device is signed in.`) is not held to a `#` it never had; a form the source does not have (Ukrainian's `few` and `many`) carries every placeholder all source forms share and nothing the source never uses, and takes its `#` from the source `other` form, whose numbers it carves out. That is what the check enforces, and it accepts Lingui's numbering of one expression differently per branch (`{1}` in `one`, `{2}` in `other`) when the target mirrors it. The one flag that is expected and resolved by hand: printf reordering (`%s of %s` → `%2$s von %1$s`), the gettext-sanctioned way to reorder, which the check reports as a rename.
- **Plurals use the target language's categories** — the ones `language list --verbose` printed, never the source language's. `one`/`other` becomes four forms in Ukrainian and collapses to `other` in Japanese.
- **Tags, entities, escape sequences and edge whitespace survive.** A trailing space or newline in the source is in the target too.
- **`max_length` is respected** when the format or the string carries one.
- **Do-not-translate terms stay in the source language:** brand and product names per the glossary, URLs, identifiers, and anything the project's rules file lists as non-translatable.
- **Never write the source catalog. Never approve. Never overwrite an existing non-fuzzy translation** unless the user names the string.
- **A glossary term with no target translation is rendered consistently, and recording it is the translators' work.** Render the word the same way everywhere, in line with the catalog and the TM, and list the rendering as a decision in the report; the glossary itself gains a translation only from a translator.
- **No fuzzy marking of drafts.** Review state lives in Crowdin; a fuzzy PO entry reads as untranslated to the app, which defeats drafting locally.
- **Ambiguity is a skip with a reason, never a guess.** A reviewer can answer a question; nobody can spot a confident wrong translation among two hundred right ones. A skip whose cause is in the code (one msgid for two meanings, a string with no reference) names the fix in the report, such as a `msgctxt` or context for the ambiguous string, so the developer sees it without opening the ledger.
## Large runs
The count table always appears. When the total number of translations exceeds 200, add two sentences before waiting for the go: cost scales with the count, and later strings get less care than earlier ones in a single pass; `crowdin auto-translate --method ai` is the cheaper route for bulk work, and the crowdin-cli skill owns it. Then proceed on a yes. Never refuse on size.
Languages are independent, and so are chunks of one language. In an environment with a subagent tool, step 4 may fan out after step 3: one worker per target language, and for a language with more than about 150 gaps, one worker per chunk of 100 to 150 strings. [references/large-runs.md](references/large-runs.md) has the chunking, the conventions sheet and the merge. Without subagent support the same work runs serially with an identical end state.
## Invoked by another workflow?
The `i18n-setup` journey delegates its `draft_translations` step here. A caller passes the target languages, the resolved catalog paths, and the go from its approved plan; this skill asks nothing twice, runs steps 1–6, and reports the per-language table back. The rules above hold unchanged — the caller's plan never waives the counts shown before upload or the "never approve" rule.
## Related skills
- **`crowdin-cli`** — every command this skill runs.
- **`glossary-generation`** — produces the glossary this skill consumes.
- **`i18n-setup`** — the full journey; its phase 6 delegates here when the user asked for translations.
Referenced files: 4 — file references are metadata, not package contents.
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package author
- Crowdin OÜ
- Keywords
- See keywords
- Commerce declaration
- Does not support commerceThis does not establish whether access is free or paid.
- Review scenarios
- 5 positive · 3 negativeScenario counts, not test results.
Package observed Nov 3, 2026.
Technical details
- First seen
- Oct 28, 2026 · 12:00 UTC
- Last seen
- Nov 4, 2026 · 06:00 UTC
- Collection status
- Collected
plugin_asdk_app_6ac7b1a0689c81919b1b49bfa1eb5a9d
Download plugin data (JSON)