docs / configuration

Configure Strings
with one strings.yaml

Strings needs to know two things about your repo: where your source strings live, and where each language's translations go. You describe both in a single file at your repo root — and if you already use Crowdin, it just works.

01Quick start

Get from zero to a connected repo in three steps. The whole config is usually under ten lines.

1

Create the file

Add strings.yaml to the root of your repository.

2

Map your files

List each source file and the pattern that names its translations.

3

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:

strings.yamlrepo root
# 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 codefr.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 repository
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:

en.jsonsource
{
  "app": {
    "greeting": "Welcome",
    "cta": "Get started"
  }
}
fr.jsonFrench
{
  "app": {
    "greeting": "Bienvenue",
    "cta": "Commencer"
  }
}
ja.jsonJapanese
{
  "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:

en.jsondeeply nested
{
  "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.

FieldRequiredWhat it means
sourceYesPath to your base language file — the one you edit directly and treat as the source of truth.
translationYesA 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:

how %two_letters_code% expands
# 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 source and translation paths are independent — the base file and its translations don't have to share a folder. Keeping them as siblings in one locales/ folder is common, but not required.
  • Paths are repo-relative. A leading slash is optional — /src/locales/en.json and src/locales/en.json are 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 translation pattern.

Tracking more than one file

Add another entry for every source file — say, app strings and marketing strings kept apart:

strings.yaml
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:

PlaceholderExampleDescription
%two_letters_code%frISO 639-1 two-letter language code. The most common choice.
%language_code%frThe short language code — interchangeable with the two-letter code for most projects.
%locale%fr-FRLanguage and region, hyphen-separated. Use when you ship regional variants.
%locale_with_underscore%fr_FRSame as above with an underscore — common in Android and gettext layouts.
%language%FrenchThe 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:

strings.yaml
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

strings.yaml
files:
  - source: /public/locales/en.json
    translation: /public/locales/%two_letters_code%.json

Next.js / i18next, folder per locale

strings.yaml
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:

strings.yaml
files:
  - source: /src/en.json
    translation: /translations/%two_letters_code%.json
your repository
your-repo/
├─ strings.yaml
├─ src/
│  └─ en.json              # source · base language
└─ translations/
   ├─ fr.json              # translation
   └─ ja.json              # translation

Regional variants with underscores

strings.yaml
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 repository
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 files list. 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.

crowdin.ymlKeep it exactly as-is
strings.yamlRename when you're ready
  • 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 meansFix
is not valid YAMLThe file couldn't be parsed.Check indentation (spaces, not tabs) and that lists use - .
missing a valid files listThere's no top-level files, or it's empty.Add at least one entry with source and translation.
A language isn't picked upThe 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.