01Quick start
Get from zero to a connected repo in three steps. The whole config is usually under ten lines.
Create the file
Add strings.yaml to the root of your repository.
Map your files
List each source file and the pattern that names its translations.
Connect the repo
Strings reads the file, pulls your locales, and opens a working branch.
Here's a complete, working example for a JSON project with English as the source:
# The source of truth stays in your repo. files: - source: /src/locales/en.json translation: /src/locales/%two_letters_code%.json
That's it. This says: the base language is en.json, and every other language is a sibling file named by its two-letter code — fr.json, es.json, de.json, and so on.
Where everything lives
Here's how that config maps onto a real repository. The strings.yaml sits at the root; the files it points to live wherever you keep your locales:
your-repo/ ├─ strings.yaml # the config — what Strings tracks ├─ package.json └─ src/ └─ locales/ ├─ en.json # source · your base language ├─ fr.json # translation · %two_letters_code% ├─ es.json # translation └─ de.json # translation
What the files contain
Every file holds the same keys — only the values change. Your source file is the reference; each translation mirrors its structure with translated strings:
{
"app": {
"greeting": "Welcome",
"cta": "Get started"
}
}
{
"app": {
"greeting": "Bienvenue",
"cta": "Commencer"
}
}
{
"app": {
"greeting": "ようこそ",
"cta": "始める"
}
}
Keys can be nested as deeply as you like. Strings preserves the whole structure and only ever changes the string values — nested objects come along untouched:
{
"app": {
"nav": {
"home": "Home",
"settings": {
"title": "Settings",
"theme": "Theme"
}
}
}
}
Mind the code. With %two_letters_code%, the file is named by the language's ISO 639-1 code — Japanese is ja (so ja.json), not jp. Name it jp.json and Strings won't match it to Japanese.
Already have a crowdin.yml? You don't need to write anything new — Strings reads the exact same format. Jump to migration →
02The files list
Everything lives under a single top-level key: files. It's a list, so you can track as many source files as you like. Each entry has exactly two fields.
| Field | Required | What it means |
|---|---|---|
source | Yes | Path to your base language file — the one you edit directly and treat as the source of truth. |
translation | Yes | A path pattern with one locale placeholder. Strings expands it into one file per language. |
How a pattern expands
The translation pattern is a template. Strings swaps the placeholder for each locale to find (or create) that language's file:
# pattern /src/locales/%two_letters_code%.json # becomes, per locale ↓ /src/locales/fr.json # French /src/locales/es.json # Spanish /src/locales/de.json # German
Path rules
- Put the files wherever you like. The
sourceandtranslationpaths are independent — the base file and its translations don't have to share a folder. Keeping them as siblings in onelocales/folder is common, but not required. - Paths are repo-relative. A leading slash is optional —
/src/locales/en.jsonandsrc/locales/en.jsonare treated the same. - The placeholder must resolve to a single path segment — the locale can't contain a
/. Use it in a filename or one folder name, e.g.locales/%locale%/messages.json. - Use exactly one locale placeholder per
translationpattern.
Tracking more than one file
Add another entry for every source file — say, app strings and marketing strings kept apart:
files: - source: /src/locales/en.json translation: /src/locales/%two_letters_code%.json - source: /marketing/i18n/en.json translation: /marketing/i18n/%two_letters_code%.json
03Locale placeholders
A placeholder is where the language code goes in the translation path. Pick the one that matches how your files are already named — Strings supports these:
| Placeholder | Example | Description |
|---|---|---|
| %two_letters_code% | fr | ISO 639-1 two-letter language code. The most common choice. |
| %language_code% | fr | The short language code — interchangeable with the two-letter code for most projects. |
| %locale% | fr-FR | Language and region, hyphen-separated. Use when you ship regional variants. |
| %locale_with_underscore% | fr_FR | Same as above with an underscore — common in Android and gettext layouts. |
| %language% | French | The full language name. Handy for human-readable folder names. |
Not sure which to use? Look at your existing translation files. If they're fr.json, use %two_letters_code%. If they're fr-FR.json, use %locale%. Match what you already have.
A quick word on the codes. %two_letters_code% is the two-letter language code (ISO 639-1: en, fr, ja) — not a country code. Need more than two letters? That's a regional locale — switch to %locale% (fr-FR) or %locale_with_underscore% (fr_FR). Three-letter codes (fra, jpn) aren't supported, and only the five placeholders above are recognized — a made-up one like %three_letters_code% won't work.
Placeholder in a folder
The placeholder doesn't have to be in the filename. This layout keeps one folder per locale:
files: - source: /locales/en/messages.json translation: /locales/%locale%/messages.json
Here %locale% fills the folder name — /locales/fr-FR/messages.json, /locales/pt-BR/messages.json, and so on.
04Real-world examples
Copy the one closest to your setup and adjust the paths.
Flat JSON, two-letter codes
files: - source: /public/locales/en.json translation: /public/locales/%two_letters_code%.json
Next.js / i18next, folder per locale
files: - source: /public/locales/en/common.json translation: /public/locales/%two_letters_code%/common.json
Source and translations in separate folders
Nothing says they have to sit together. Keep your base file in src/ and route translations to their own translations/ folder — source and translation are independent paths:
files: - source: /src/en.json translation: /translations/%two_letters_code%.json
your-repo/ ├─ strings.yaml ├─ src/ │ └─ en.json # source · base language └─ translations/ ├─ fr.json # translation └─ ja.json # translation
Regional variants with underscores
files: - source: /app/i18n/en_US.json translation: /app/i18n/%locale_with_underscore%.json
05Multiple config files
One config is enough for most projects. But you can keep several in the same repo — Strings automatically finds every config at your repo root and connects each one as its own integration.
When you'd want this
The usual case is a monorepo where separate apps ship separate locale sets — say a web app in every language and a mobile app in just a few. Give each its own config so they stay independent:
your-repo/ ├─ strings-web.yaml # points at apps/web locales ├─ strings-mobile.yaml # points at apps/mobile locales └─ apps/ ├─ web/locales/en.json └─ mobile/locales/en.json
Naming matters for auto-discovery. A config is picked up only if it lives at the repo root and its name starts with strings, crowdin, or i18n and ends in .yaml / .yml — e.g. strings-web.yaml, crowdin.yml, i18n-marketing.yaml. A file named translations.yaml won't be found.
Two files or two configs?
These solve different problems — don't reach for multiple configs when a longer files list will do:
- Many files, one config — add more entries to the
fileslist. They're tracked together as one integration. See the files list → - Many configs — separate, independently-connected integrations, each with its own source and language set. Strings groups branches under the config they belong to.
06Migrating from Crowdin
Strings reads the same files format that Crowdin does, so migration is a non-event. You have two options — both work with zero changes to your files.
- Keep
crowdin.yml. Strings auto-detects it at connect and nothing else changes — run both tools side by side if you like. - Or rename to
strings.yaml. Same contents, clearer intent. This is the recommended long-term name.
Strings uses the files list only. Crowdin-specific keys like project_id, api_token, or preserve_hierarchy are simply ignored — they won't cause errors, but they also don't do anything in Strings.
07Validation & troubleshooting
When you connect a repo, Strings validates the config and tells you exactly what's wrong. The three things it checks:
| If you see… | It means | Fix |
|---|---|---|
is not valid YAML | The file couldn't be parsed. | Check indentation (spaces, not tabs) and that lists use - . |
missing a valid files list | There's no top-level files, or it's empty. | Add at least one entry with source and translation. |
| A language isn't picked up | The file name doesn't match the pattern. | Confirm the placeholder matches your real filenames (e.g. fr.json ↔ %two_letters_code%). |
Rule of thumb: if you can point at your English file and describe every other language's path by swapping one code, your config is correct.
Make your repo multilingual
Drop in a strings.yaml, connect your repo, and translate on a branch that ships as a pull request.