From 9d2a6867e5926364d62ed773c5d09f09fa511be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Behrendt?= Date: Wed, 29 Apr 2026 11:43:46 +0200 Subject: [PATCH 1/2] chore(CLI): add cli config skill --- clients/cli/skills/phrase-config/SKILL.md | 131 +++++++++ .../skills/phrase-config/references/cli.md | 62 ++++ .../phrase-config/references/examples.md | 194 +++++++++++++ .../phrase-config/references/formats.md | 266 ++++++++++++++++++ .../skills/phrase-config/references/schema.md | 146 ++++++++++ .../references/troubleshooting.md | 111 ++++++++ 6 files changed, 910 insertions(+) create mode 100644 clients/cli/skills/phrase-config/SKILL.md create mode 100644 clients/cli/skills/phrase-config/references/cli.md create mode 100644 clients/cli/skills/phrase-config/references/examples.md create mode 100644 clients/cli/skills/phrase-config/references/formats.md create mode 100644 clients/cli/skills/phrase-config/references/schema.md create mode 100644 clients/cli/skills/phrase-config/references/troubleshooting.md diff --git a/clients/cli/skills/phrase-config/SKILL.md b/clients/cli/skills/phrase-config/SKILL.md new file mode 100644 index 00000000..699b7718 --- /dev/null +++ b/clients/cli/skills/phrase-config/SKILL.md @@ -0,0 +1,131 @@ +--- +name: phrase-config +description: Generate a .phrase.yml config file for the phrase-cli and Strings Repo Sync. Detects the project's i18n format and locale file paths and writes a working push/pull config. TRIGGER when the user wants to create, extend, or troubleshoot a .phrase.yml config, or has questions about push/pull behavior, CLI flags, locale file setup, or Repo Sync configuration. DO NOT TRIGGER when the user wants to run push/pull and isn't asking about the config itself. +--- + +# phrase-config + +Generates `.phrase.yml` for any project that uses the Phrase Strings CLI or Strings Repo Sync. The CLI reads this file from the project root to drive `phrase push` and `phrase pull`; Repo Sync uses the same file to sync locale files between a git repository and Phrase. + +**Behavior model:** detect → propose → write. Scan the project, infer format and pattern, show one combined confirmation, write the file. Don't ask questions you can defer to a placeholder or a chat-side note. + +## References (load on demand) + +- [`references/schema.md`](./references/schema.md) — every `.phrase.yml` key, placeholder rules, and the validation rules the generated file must satisfy. +- [`references/formats.md`](./references/formats.md) — detection rules, all 51 format identifiers + default file patterns, and per-format `format_options`. +- [`references/examples.md`](./references/examples.md) — config examples for common project layouts. +- [`references/troubleshooting.md`](./references/troubleshooting.md) — keyed by error message: locale not found, wildcard rejected, plural splitting, values/ vs values-en/, etc. +- [`references/cli.md`](./references/cli.md) — install instructions, `phrase push` / `phrase pull` flags, and `PHRASE_*` environment variables. + +Official Phrase docs: +- Config file overview: +- Push/pull configuration: +- JSON Schema: + +If `phrase` isn't on PATH, see install options in [`references/cli.md`](./references/cli.md) (Homebrew, asdf, GitHub releases). + +## Workflow + +If `.phrase.yml` already exists at repo root, skip to the **[Augment path](#augment-path)** below. + +### Greenfield path + +#### Step 1 — Detect format and path pattern + +Use the detection rules in [`references/formats.md`](./references/formats.md). First match wins; ignore `node_modules/`, `vendor/`, `.git/`, `dist/`, `build/`, `target/`, `Pods/`, `.dart_tool/`. + +When the format is ambiguous, **always read a sample file** before deciding: + +- Generic JSON: open one file. Flat `{"key": "value"}` → `simple_json`. Nested `{"a": {"b": "value"}}` → `nested_json`. +- XLIFF: check the root element's `version=` attribute. `1.2` → `xlf`; `2.0` → `xliff_2`. +- Vue i18n: check whether the locale files are JSON or YAML. + +**Detect monorepos upfront.** If you find multiple format roots (e.g. both `ios/` and `android/`, or both `*.arb` and `locales/*.json`), emit multiple `sources` and `targets` — one per platform — instead of silently picking one. Surface the ambiguity to the user. + +Then infer the path pattern from the discovered files: + +- Filename matches a known locale code (`en`, `de`, `pt-BR`, `zh-CN`) → ``. +- Filename or directory matches a display name (`English`, `German`) → ``. +- Android `values-de/strings.xml` → `res/values-/strings.xml`. `values/` (no suffix) is the default locale — flag it. +- iOS `de.lproj/Localizable.strings` → `.lproj/Localizable.strings`. +- Flutter `lib/l10n/app_en.arb` → `lib/l10n/app_.arb`. + +If detection fails, fall back to the format's default pattern from `formats.md`. + +**Source locale:** infer from the existing locale files (`en` is the most common). Don't ask — write the inferred value into `locale_id:` and tell the user in chat to change it if their Phrase project uses a different name. + +#### Step 2 — One combined confirmation + +Show the user a single proposal that includes everything you inferred. Only ask follow-up questions if something is genuinely ambiguous (multiple format roots, no clear source locale, unrecognized layout). + +Format: + +> Detected: **i18next** at `src/locales//translation.json` (source locale `en`, EU datacenter). Project ID will be left as a placeholder — replace it with your real ID from Project Settings → API. Write `.phrase.yml`? + +If the user is on the US datacenter, they'll say so — only then add `host:`. Don't ask about it preemptively. + +#### Step 3 — Generate `.phrase.yml` + +Wrap config in the top-level `phrase:` key. + +**Default shape:** push uploads only the source locale (one fixed file, no locale placeholder). Pull downloads every locale into placeholdered paths. + +```yaml +phrase: + project_id: "" + file_format: + + push: + sources: + - file: + params: + file_format: + locale_id: "" + update_translations: true + + pull: + targets: + - file: > + params: + file_format: +``` + +The push `file:` is a fixed path to the source-locale file (e.g. `config/locales/en.yml`, `lib/l10n/app_en.arb`). `params.locale_id` tells Phrase which locale that file represents. + +The pull `file:` keeps the `` placeholder so every locale lands in the right place. + +If the user explicitly asks to push *all* locales (they edit translations locally), switch the push source to use the same placeholder pattern as pull and drop `locale_id` from `params`. + +US datacenter: add `host: https://api.us.app.phrase.com/v2` directly under `phrase:`. + +Multi-platform: emit one source/target per platform, each with its own `file_format` and (optionally) `tags:` to keep keys segmented. + +Before writing, verify: +- No wildcards (`*`, `**`) in pull `file:` paths — use placeholders instead. +- Each placeholder (``, ``, ``) appears at most once per `file:`. +- Never include `access_token:` in the file. +- The file must be wrapped in a top-level `phrase:` key. + +Don't add YAML comments unless they encode a non-obvious constraint. + +#### Step 4 — Post-generation + +Show the generated file in chat, then end with this copy-pasteable block: + +```sh +export PHRASE_ACCESS_TOKEN= # from app.phrase.com → Profile → OAuth Access Tokens +phrase pull +``` + +Set the token via env var only — never in `.phrase.yml`. Suggest persisting it in a shell rc file or `.envrc` (with direnv). + +For ongoing reference, point the user at the `references/` files relevant to their workflow (branches, tags, cleanup, format-specific options, error keys). + +### Augment path + +When `.phrase.yml` already exists at repo root: + +1. Read it. Show the user what's there in 1–2 lines (formats, source/target counts). +2. Ask what to add (e.g. "a new platform target", "a tag-segmented pull", "iOS Strings Catalog source"). +3. Run only Step 1 detection for the *new* piece, then propose just the added block — don't rewrite the rest of the file. +4. Use `Edit`, not `Write`, to splice the new entry in. diff --git a/clients/cli/skills/phrase-config/references/cli.md b/clients/cli/skills/phrase-config/references/cli.md new file mode 100644 index 00000000..bfab5756 --- /dev/null +++ b/clients/cli/skills/phrase-config/references/cli.md @@ -0,0 +1,62 @@ +# CLI flags & environment variables + +These don't go in `.phrase.yml` — they're passed on the command line or set in your shell. CLI flags override config file values. + +## Installing the CLI + +If `phrase` isn't on PATH, point the user at one of these: + +**Homebrew** (macOS / Linux): + +```sh +brew install phrase-cli +``` + +**asdf** (version-managed, useful for repos that pin a CLI version): + +```sh +asdf plugin add phrase https://github.com/phrase/asdf-phrase +asdf install phrase latest +asdf set phrase latest # or: asdf set -u phrase to write a .tool-versions +``` + +**GitHub releases** (any OS — pick the matching binary): + + +Verify with `phrase --version`. Full install guide: . + +## `phrase push` + +| Flag | Short | What it does | +|---|---|---| +| `--wait` | `-w` | Block until uploads are processed. | +| `--cleanup` | `-c` | Same as `delete_unmentioned_keys: true`. Deletes keys not in any uploaded file. | +| `--branch ` | `-b` | Push to a specific Phrase branch. | +| `--use-local-branch-name` | | Use the current git/hg branch name as the Phrase branch. | +| `--tag ` | | Apply a tag to every key in this upload. | +| `--token ` | `-t` | Override `access_token`. | +| `--host ` | `-h` | Override `host`. | + +## `phrase pull` + +| Flag | Short | What it does | +|---|---|---| +| `--branch ` | `-b` | Pull from a specific Phrase branch. | +| `--use-local-branch-name` | | Use the current git/hg branch name. | +| `--async` | `-a` | Asynchronous downloads. Useful for projects with many locales. | +| `--cache` | | Conditional downloads via ETags (sync mode only). | +| `--parallel` | `-p` | Download up to 4 locales in parallel (sync mode only). | +| `--token ` | `-t` | Override `access_token`. | +| `--host ` | `-h` | Override `host`. | + +`--cache` and `--parallel` cannot be combined with `--async`. + +## Environment variables + +| Variable | What it does | +|---|---| +| `PHRASE_ACCESS_TOKEN` | Provides the access token. The recommended way — never put the token in `.phrase.yml`. | +| `PHRASE_PROJECT_ID` | Override `project_id`. | +| `PHRASE_HOST` | Override `host`. | + +Precedence (highest first): CLI flag → environment variable → config file → built-in default. diff --git a/clients/cli/skills/phrase-config/references/examples.md b/clients/cli/skills/phrase-config/references/examples.md new file mode 100644 index 00000000..942becab --- /dev/null +++ b/clients/cli/skills/phrase-config/references/examples.md @@ -0,0 +1,194 @@ +# Config examples + +Each example shows what the skill should produce for a typical project. + +For first-time gotchas and error symptoms (locale not found, wildcard rejected, plurals splitting), see [`troubleshooting.md`](./troubleshooting.md). + +**Default shape:** push uploads only the source locale (one fixed path, no placeholder). Pull downloads every locale using a placeholder. + +## Rails + +Detected: `config/locales/en.yml`, `config/locales/de.yml` + `Gemfile` mentions `rails`. Source locale: `en` ("English" in Phrase). + +```yaml +phrase: + project_id: "abc123..." + file_format: yml + push: + sources: + - file: config/locales/en.yml + params: + file_format: yml + locale_id: "English" + update_translations: true + pull: + targets: + - file: config/locales/.yml + params: + file_format: yml +``` + +## React + i18next + +Detected: `package.json` deps include `react-i18next`; `src/locales/en/translation.json`. Source locale: `en`. + +```yaml +phrase: + project_id: "abc123..." + file_format: i18next + push: + sources: + - file: src/locales/en/translation.json + params: + file_format: i18next + locale_id: "English" + update_translations: true + pull: + targets: + - file: src/locales//translation.json + params: + file_format: i18next +``` + +## Flutter + +Detected: `pubspec.yaml`; `lib/l10n/app_en.arb`, `lib/l10n/app_de.arb`. Source locale: `en`. + +```yaml +phrase: + project_id: "abc123..." + file_format: arb + push: + sources: + - file: lib/l10n/app_en.arb + params: + file_format: arb + locale_id: "English" + update_translations: true + pull: + targets: + - file: lib/l10n/app_.arb + params: + file_format: arb +``` + +## iOS + Android monorepo (multi-platform) + +Source locale: `en` for both platforms. + +```yaml +phrase: + project_id: "abc123..." + push: + sources: + - file: ios/en.lproj/Localizable.strings + params: + file_format: strings + locale_id: "English" + tags: ios + update_translations: true + - file: android/app/src/main/res/values/strings.xml + params: + file_format: xml + locale_id: "English" + tags: android + update_translations: true + pull: + targets: + - file: ios/.lproj/Localizable.strings + params: + file_format: strings + tags: ios + - file: android/app/src/main/res/values-/strings.xml + params: + file_format: xml + tags: android +``` + +Note Android's quirk: `values/` (no suffix) holds the source locale, while translated locales go into `values-/`. The push source matches `values/` exactly; the pull target uses the placeholdered path. + +## Android `r`-prefixed regional locales (locale_mapping) + +Android writes regional locales as `values--r/` (e.g. `values-zh-rCN/`, `values-pt-rBR/`), but Phrase stores those locales as `zh-CN`, `pt-BR`. The `` placeholder expands to the Phrase code, so use `locale_mapping` to translate Phrase codes to the Android-flavored folder name on disk. + +```yaml +phrase: + project_id: "abc123..." + file_format: xml + locale_mapping: + zh-CN: zh-rCN + zh-TW: zh-rTW + pt-BR: pt-rBR + en-GB: en-rGB + push: + sources: + - file: app/src/main/res/values/strings.xml + params: + file_format: xml + locale_id: "English" + update_translations: true + pull: + targets: + - file: app/src/main/res/values-/strings.xml + params: + file_format: xml +``` + +`` will now expand to `zh-rCN` for the `zh-CN` Phrase locale, so the file lands in `values-zh-rCN/strings.xml`. Pure-language locales like `de` or `fr` need no mapping — they go to `values-de/`, `values-fr/` as-is. + +## Splitting files per tag + +When keys are tagged in Phrase (e.g. `checkout`, `dashboard`, `marketing`), use the `` placeholder to push and pull each tag into its own file. Both `` and `` appear in the path; `tags:` in `params:` lists which tags to expand. + +```yaml +phrase: + project_id: "abc123..." + file_format: i18next + push: + sources: + - file: src/locales//.json + params: + file_format: i18next + tags: checkout,dashboard,marketing + update_translations: true + pull: + targets: + - file: src/locales//.json + params: + file_format: i18next + tags: checkout,dashboard,marketing +``` + +On pull, one file is written per (locale, tag) pair. On push, the CLI uploads each matching file and tags its keys accordingly — keys in `checkout.json` get the `checkout` tag, and so on. + +## US datacenter + +Same shape as any of the above, with `host:` directly under `phrase:`: + +```yaml +phrase: + host: https://api.us.app.phrase.com/v2 + project_id: "abc123..." + ... +``` + +## Pushing all locales (override) + +If you edit translations locally and want to upload every locale, mirror the pull pattern in the push source and drop `locale_id`: + +```yaml +phrase: + project_id: "abc123..." + file_format: yml + push: + sources: + - file: config/locales/.yml + params: + file_format: yml + update_translations: true + pull: + targets: + - file: config/locales/.yml + params: + file_format: yml +``` diff --git a/clients/cli/skills/phrase-config/references/formats.md b/clients/cli/skills/phrase-config/references/formats.md new file mode 100644 index 00000000..84601dbb --- /dev/null +++ b/clients/cli/skills/phrase-config/references/formats.md @@ -0,0 +1,266 @@ +# Formats: detection, identifiers, and per-format options + +Three things in one file: how to recognize a project's format, the full list of identifiers + default file patterns, and per-format `format_options`. + +## Detection rules + +Walk the project tree from repo root. Ignore `node_modules/`, `vendor/`, `.git/`, `dist/`, `build/`, `target/`, `Pods/`, `.dart_tool/`. Apply rules in order; first match wins. For monorepos, multiple matches are allowed. + +| Signal | `file_format` | Typical project | +|---|---|---| +| `pubspec.yaml` + `*.arb` files (often under `lib/l10n/`) | `arb` | Flutter | +| `*.xcstrings` files | `strings_catalog` | Apple String Catalog (Xcode 15+) | +| `*.lproj/Localizable.strings` | `strings` | iOS/macOS (legacy) | +| `*.lproj/Localizable.stringsdict` (alongside `.strings`) | `stringsdict` | iOS plurals (legacy) | +| `*.plist` files in localized form | `plist` | Objective-C/Cocoa property lists | +| `res/values*/strings.xml` | `xml` | Android | +| `*.po` files | `gettext` | Python/PHP/Django/C | +| `*.pot` template files | `gettext_template` | gettext templates | +| `config/locales/*.yml` + `Gemfile` mentioning `rails` | `yml` | Rails | +| `app/Resources/translations/*.yml` (Symfony layout) | `yml_symfony2` | Symfony | +| `app/Resources/translations/messages.*.xlf` (Symfony) | `symfony_xliff` | Symfony XLIFF | +| `resources/lang//messages.php` | `laravel` | Laravel | +| `*.php` array translation files (non-Laravel) | `php_array` | Generic PHP | +| `package.json` deps include `i18next` | `i18next` (or `i18next_4` if v4+) | i18next (React/Vue/Node) | +| `package.json` deps include `react-intl` — flat files | `react_simple_json` | React Intl flat | +| `package.json` deps include `react-intl` — nested files | `react_nested_json` | React Intl nested | +| `package.json` deps include `vue-i18n` (inspect file) | `simple_json` or `yml` | Vue i18n | +| `package.json` deps include `next-intl` or `next-translate` | `nested_json` | Next.js | +| `package.json` deps include `angular-translate` | `angular_translate` | AngularJS | +| `app/locales/*/translations.js` | `ember_js` | Ember.js | +| `*.go` with `go-i18n` and `*.all.json` | `go_i18n` | Go i18n | +| Chrome extension `_locales//messages.json` | `json` | Chrome i18n | +| `conf/messages.` | `play_properties` | Play Framework | +| `*.properties` under `src/main/resources/` | `properties` | Java | +| Mozilla addon `*.properties` | `mozilla_properties` | Firefox/XUL | +| `*.resx` files | `resx` | .NET / WinForms | +| `*.resw` files | `windows8_resource` | Windows Store apps | +| `Resources/AppResources..resx` | `resx_windowsphone` | Windows Phone | +| `*.xlf` / `*.xliff` (XLIFF 1.2) | `xlf` | Angular i18n, .NET MAUI, generic | +| `*.xlf` / `*.xliff` declared as XLIFF 2.0 (`version="2.0"`) | `xliff_2` | Modern XLIFF | +| `*.ts` (Qt translation source) | `ts` | Qt Linguist | +| `*.tmx` | `tmx` | Translation memory | +| `*.xlsx` | `xlsx` | Excel workflow | +| `*.csv` | `csv` | Spreadsheet workflow | +| `*.html` / `*.htm` | `html` or `html_5` | Marketing/CMS pages | +| `*.docx` | `docx` | Documentation | +| `locales/*.json` (flat key/value) | `simple_json` | Generic | +| `locales/*.json` (nested objects) | `nested_json` | Generic | + +**Always read a sample file when the format is ambiguous:** + +- Generic JSON: open one locale file. Flat `{"key": "value"}` → `simple_json`. Nested `{"a": {"b": "value"}}` → `nested_json`. +- XLIFF: check the root element's `version=` attribute. `1.2` → `xlf`; `2.0` → `xliff_2`. +- Vue i18n: deps + a `.json` locale file → `simple_json` (or `nested_json`); deps + a `.yml` locale file → `yml`. + +If detection finds **multiple** locale-file roots (e.g. both `*.arb` and `locales/*.json`, or both `ios/` and `android/`), surface that to the user and ask which to wire up — or whether to emit one source/target per platform. + +If nothing matches, ask the user to pick from the identifiers below. + +## Identifiers and default file patterns + +The CLI accepts any identifier returned by `phrase formats list`. The 51 public formats: + +| Identifier | Display name | Extension | Default file pattern | +|---|---|---|---| +| `arb` | ARB | `.arb` | `./.arb` | +| `yml` | Ruby/Rails YAML | `.yml` `.yaml` | `./config/locales/.yml` | +| `gettext` | Gettext | `.po` | `./.po` | +| `gettext_template` | Gettext template | `.pot` | `./.pot` | +| `gettext_mo` | Gettext compiled | `.mo` | `./.mo` | +| `xml` | Android Strings | `.xml` | `./values-/strings.xml` | +| `strings` | iOS Localizable Strings | `.strings` | `./.lproj/Localizable.strings` | +| `stringsdict` | iOS Localizable Stringsdict | `.stringsdict` | `./.lproj/Localizable.stringsdict` | +| `strings_catalog` | Apple Strings Catalog | `.xcstrings` | `./.xcstrings` | +| `xlf` | XLIFF 1.2 | `.xlf` `.xliff` | `./.xlf` | +| `xliff_2` | XLIFF 2.0 | `.xlf` `.xliff` | `./.xlf` | +| `symfony_xliff` | Symfony XLIFF | `.xlf` `.xliff` | `./messages..xlf` | +| `qph` | Qt Phrase Book | `.qph` | `./.qph` | +| `ts` | Qt Translation Source | `.ts` | `./.ts` | +| `json` | Chrome JSON i18n | `.json` | `./.json` | +| `simple_json` | Simple JSON (flat) | `.json` | `./.json` | +| `nested_json` | Nested JSON | `.json` | `./.json` | +| `react_simple_json` | React-Intl Simple JSON | `.json` | `./.json` | +| `react_nested_json` | React-Intl Nested JSON | `.json` | `./.json` | +| `i18next` | i18next | `.json` | `./locales//translations.json` | +| `i18next_4` | i18next 4 | `.json` | `./locales//translations.json` | +| `go_i18n` | Go i18n JSON | `.json` | `./.all.json` | +| `node_json` | i18n-node-2 JSON | `.js` | `./locales/.js` | +| `angular_translate` | Angular Translate | `.json` | `./i18n/.json` | +| `ember_js` | Ember.js | `.js` | `./app/locales//translations.js` | +| `genesys_json` | Genesys JSON | `.json` | `./.json` | +| `strings_json` | Strings JSON | `.json` | `./.json` | +| `resx` | .NET ResX | `.resx` | `./.resx` | +| `resx_windowsphone` | Windows Phone ResX | `.resx` | `./Resources/AppResources..resx` | +| `windows8_resource` | Windows 8 Resource | `.resw` | `./Resources/AppResources..resw` | +| `properties` | Java Properties | `.properties` | `./MessagesBundle_.properties` | +| `mozilla_properties` | Mozilla Properties | `.properties` | `./.properties` | +| `properties_xml` | Java Properties XML | `.xml` | `./MessagesBundle_.xml` | +| `play_properties` | Play Framework Properties | `.locale` | `./conf/messages.` | +| `ini` | INI | `.ini` | `./.ini` | +| `plist` | Objective-C/Cocoa Property List | `.plist` | `./.plist` | +| `tmx` | TMX Translation Memory | `.tmx` | `./.tmx` | +| `xlsx` | Excel XLSX | `.xlsx` | `./.xlsx` | +| `csv` | CSV | `.csv` | `./.csv` | +| `txt` | Tab-separated TXT | `.txt` | `./.txt` | +| `zendesk_csv` | Zendesk CSV | `.csv` | `./.csv` | +| `php_array` | PHP Array | `.php` | `./locale_.php` | +| `laravel` | Laravel/F3/Kohana Array | `.php` | `./resources/lang//messages.php` | +| `yml_symfony` | Symfony YAML | `.yml` `.yaml` | `./app/Resources/translations/.yml` | +| `yml_symfony2` | Symfony2 YAML | `.yml` `.yaml` | `./app/Resources/translations/.yml` | +| `episerver` | Episerver XML | `.xml` | `./.xml` | +| `linguist_xml` | Linguist XML | `.xml` | `./linguist.xml` | +| `linguist_xml_2` | Linguist XML 2 | `.xml` | `./linguist.xml` | +| `html` | HTML | `.html` `.htm` | `./.html` | +| `html_5` | HTML 5 | `.html` `.htm` | `./.html` | +| `docx` | Word DOCX | `.docx` | `./.docx` | + +The default file pattern is what `phrase init` would suggest for that format — a good fallback when you can't infer the pattern from existing files. + +Full format guide with import/export quirks: . + +--- + +## Per-format options (`params.format_options`) + +Per-format options go under `params.format_options:` in `.phrase.yml`. They are not interchangeable across formats — only set options that the chosen format declares. + +### `xml` (Android Strings) + +| Option | What it does | Example | +|---|---|---| +| `convert_placeholder` | Convert `%s`-style placeholders to and from Android's `%1$s` style. | `convert_placeholder: true` | +| `escape_linebreaks` | Escape line breaks as `\n` when downloading. | `escape_linebreaks: true` | +| `unescape_linebreaks` | Turn `\n` back into real newlines on upload. | `unescape_linebreaks: true` | +| `enclose_in_cdata` | Wrap each value in ``. | `enclose_in_cdata: true` | +| `preserve_cdata` | Keep existing CDATA wrappers when re-uploading. | `preserve_cdata: true` | +| `escape_tags` / `unescape_tags` | Escape or unescape HTML/XML tags inside values. | `escape_tags: true` | +| `escape_android_chars` / `unescape_android_chars` | Escape or unescape Android-special characters (`'`, `"`, `@`, `?`, `\`). | `escape_android_chars: true` | +| `indent_size` | How many characters to indent. | `indent_size: 4` | +| `indent_style` | Indent with spaces or tabs. | `indent_style: "space"` | +| `include_tools_ignore` | Add `tools:ignore` attributes to keys that have them in Phrase. | `include_tools_ignore: true` | +| `include_tools_locale_definition` | Add `tools:locale` to the `` element. | `include_tools_locale_definition: true` | + +### `strings` (iOS) + +| Option | What it does | Example | +|---|---|---| +| `convert_placeholder` | Normalize placeholder syntax (`%s` ↔ `%@`). | `convert_placeholder: true` | +| `include_pluralized_keys` | Keep keys with plural forms in `.strings` (instead of moving them to `.stringsdict`). | `include_pluralized_keys: true` | +| `multiline_descriptions` | Wrap long key descriptions across multiple comment lines. | `multiline_descriptions: true` | + +### `stringsdict` + +| Option | What it does | Example | +|---|---|---| +| `convert_placeholder` | Normalize placeholder syntax. | `convert_placeholder: true` | + +### `strings_catalog` (Apple `.xcstrings`) + +| Option | What it does | Example | +|---|---|---| +| `convert_placeholder` | Normalize placeholder syntax. | `convert_placeholder: true` | +| `ignore_translation_state` | Don't read or write the translation state field. | `ignore_translation_state: true` | +| `default_extraction_state` | The `extractionState` value for new entries. | `default_extraction_state: "manual"` | + +### `xlf` (XLIFF 1.2) + +| Option | What it does | Example | +|---|---|---| +| `enclose_in_cdata` | Wrap target text in CDATA. | `enclose_in_cdata: true` | +| `content_as_literal` | Treat content as literal text — don't decode XML entities. | `content_as_literal: true` | +| `include_translation_state` | Add the `state` attribute to translation units. | `include_translation_state: true` | +| `replace_target_translations_with_empty_string` | When a translation is missing, write an empty target instead of falling back to the source. | `replace_target_translations_with_empty_string: true` | +| `keep_plural_skeletons` | Preserve the plural-form skeleton on round-trip. | `keep_plural_skeletons: true` | +| `ignore_source_translations` | Skip the source-language text on import. | `ignore_source_translations: true` | +| `ignore_target_translations` | Skip the target-language text on import. | `ignore_target_translations: true` | +| `export_key_id_as_resname` | Put the Phrase key id in the `resname` attribute. | `export_key_id_as_resname: true` | +| `export_key_name_hash_as_extradata` | Put a hash of the key name in the `extradata` attribute. | `export_key_name_hash_as_extradata: true` | +| `delimit_placeholders` | Wrap placeholders with delimiters during export. | `delimit_placeholders: true` | +| `strip_placeholder_delimiters` | Remove placeholder delimiters during import. | `strip_placeholder_delimiters: true` | +| `override_file_language` | Force the file's language attribute to match the downloaded locale. | `override_file_language: true` | +| `key_name_attribute` | Which XML attribute holds the key name. Defaults to `id`. | `key_name_attribute: "resname"` | +| `custom_metadata_columns` | Map your custom metadata fields to XLIFF attributes. |
custom_metadata_columns:
team: extradata
| + +### `xliff_2` (XLIFF 2.0) + +Most of the XLIFF 1.2 options work here too: `enclose_in_cdata`, `include_translation_state`, `replace_target_translations_with_empty_string`, `content_as_literal`, `keep_plural_skeletons`, `ignore_source_translations`, `ignore_target_translations`, `override_file_language`. + +### `symfony_xliff` + +| Option | What it does | Example | +|---|---|---| +| `enclose_in_cdata` | Wrap target text in CDATA. | `enclose_in_cdata: true` | + +### `simple_json` / `nested_json` / `react_nested_json` + +| Option | What it does | Example | +|---|---|---| +| `enable_pluralization` | Treat keys with plural suffixes (`_one`, `_other`, etc.) as plural variants of one key. | `enable_pluralization: true` | + +### `i18next` / `i18next_4` + +| Option | What it does | Example | +|---|---|---| +| `nesting` | Output nested objects (the default). Turn off for flat keys. | `nesting: false` | + +### `gettext` / `gettext_template` + +| Option | What it does | Example | +|---|---|---| +| `msgid_as_default` | When `msgstr` is empty, use `msgid` as the fallback. | `msgid_as_default: true` | +| `is_bilingual_file` | (`gettext` only) Treat the file as bilingual — both source and target. | `is_bilingual_file: true` | + +### `properties` (Java) + +| Option | What it does | Example | +|---|---|---| +| `escape_single_quotes` | Double single-quotes (`'` → `''`) for Java `MessageFormat`. | `escape_single_quotes: true` | +| `omit_separator_space` | Don't put spaces around `=`. | `omit_separator_space: true` | +| `crlf_line_terminators` | Use Windows line endings (`\r\n`). | `crlf_line_terminators: true` | +| `escape_meta_chars` | Escape backslashes, tabs, and other control characters. | `escape_meta_chars: true` | + +### `play_properties` + +| Option | What it does | Example | +|---|---|---| +| `escape_single_quotes` | Double single-quotes for Play's MessageFormat. | `escape_single_quotes: true` | + +### `csv` / `txt` + +| Option | What it does | Example | +|---|---|---| +| `column_separator` | Field separator. Defaults to `,` for CSV, tab for TXT. | `column_separator: ";"` | +| `quote_char` | Quote character. Defaults to `"`. | `quote_char: "'"` | +| `first_content_row` | Row number where actual translation rows start (1-based). | `first_content_row: 2` | +| `key_index` / `key_name_column` | Which column holds the key. Use either an index or a header name. | `key_name_column: "Key"` | +| `key_id_column` | Which column holds the Phrase key id. | `key_id_column: 1` | +| `translation_index` / `translation_indexes` / `translation_columns` | Which column(s) hold translations. Map locale → column. |
translation_columns:
en: 2
de: 3
| +| `comment_index` / `comment_column` | Which column holds key descriptions. | `comment_column: "Notes"` | +| `tag_column` | Which column holds tags. | `tag_column: 5` | +| `max_characters_allowed_column` | Which column holds the per-key character limit. | `max_characters_allowed_column: "Limit"` | +| `custom_metadata_columns` | Map custom metadata fields to columns. |
custom_metadata_columns:
team: 6
| +| `enable_pluralization` | Treat plural-suffixed keys as plurals. | `enable_pluralization: true` | +| `include_headers` | Write a header row when downloading. | `include_headers: true` | +| `export_tags` / `export_system_tags` | Include tag columns when downloading. | `export_tags: true` | +| `export_key_id` | Include the Phrase key id column when downloading. | `export_key_id: true` | +| `export_max_characters_allowed` | Include the character-limit column when downloading. | `export_max_characters_allowed: true` | +| `group_by_key_name` | Group rows by key name in the output. | `group_by_key_name: true` | + +### `xlsx` (Excel) + +These CSV options also work for Excel files: `first_content_row`, `key_name_column`, `translation_column`, `translation_columns`, `custom_metadata_columns`, `comment_column`, `tag_column`, `max_characters_allowed_column`, `enable_pluralization`, `export_tags`, `export_system_tags`, `export_max_characters_allowed`. + +### `strings_json` + +| Option | What it does | Example | +|---|---|---| +| `custom_metadata_columns` | Map custom metadata fields to JSON properties. |
custom_metadata_columns:
team: team_field
| +| `export_description` | Include key descriptions in the output. | `export_description: true` | +| `export_tags` / `export_system_tags` | Include tag fields in the output. | `export_tags: true` | +| `export_max_characters_allowed` | Include the per-key character limit. | `export_max_characters_allowed: true` | +| `include_translation_state` | Include the translation state field. | `include_translation_state: true` | +| `ignore_translation_state` | Skip importing translation state. | `ignore_translation_state: true` | +| `export_use_ordinal_rules` | Use ordinal plural rules (1st, 2nd, 3rd) instead of cardinal. | `export_use_ordinal_rules: true` | + +Formats not listed above (`arb`, `yml`, `json`, `react_simple_json`, `qph`, `ts`, `go_i18n`, `node_json`, `resx`, `resx_windowsphone`, `windows8_resource`, `mozilla_properties`, `properties_xml`, `plist`, `ini`, `tmx`, `zendesk_csv`, `php_array`, `laravel`, `yml_symfony`, `yml_symfony2`, `episerver`, `linguist_xml`, `linguist_xml_2`, `angular_translate`, `ember_js`, `genesys_json`, `html`, `html_5`, `docx`, `gettext_mo`) currently expose **no per-format options**. diff --git a/clients/cli/skills/phrase-config/references/schema.md b/clients/cli/skills/phrase-config/references/schema.md new file mode 100644 index 00000000..ab4d4fa1 --- /dev/null +++ b/clients/cli/skills/phrase-config/references/schema.md @@ -0,0 +1,146 @@ +# `.phrase.yml` schema reference + +Every key the CLI understands. Tables use plain language and example values. + +## Top-level keys (under `phrase:`) + +| Key | What it does | Example | +|---|---|---| +| `project_id` | **Required.** Identifies your Phrase project. Find it in Project Settings → API. | `project_id: "abcd1234ef56..."` | +| `host` | Tells the CLI to talk to the US datacenter. Leave it out for EU. | `host: https://api.us.app.phrase.com/v2` | +| `access_token` | Don't use this — set the `PHRASE_ACCESS_TOKEN` environment variable instead so your token never lands in git. | (omit) | +| `file_format` | Default format if you don't repeat it on every source/target. | `file_format: yml` | +| `per_page` | How many items to fetch per page. Defaults to 100, you usually don't need this. | `per_page: 50` | +| `locale_mapping` | Use different locale names on disk than in Phrase. Keys are the names in Phrase, values are the names you want in your files. |
locale_mapping:
English: eng
German: ger
| +| `push` | The upload section. Contains `sources:`. | (see below) | +| `pull` | The download section. Contains `targets:`. | (see below) | + +## Push (`phrase.push`) + +```yaml +push: + sources: + - file: + project_id: # optional + branch: # optional, push to a Phrase branch + file_format: # optional + params: + file_format: + locale_id: + # ...further upload params (see table below)... +``` + +### Per-source keys (sibling to `params:`) + +| Key | What it does | Example | +|---|---|---| +| `file` | **Required.** Where your locale files live. Supports `*`, `**`, and placeholders. | `file: config/locales/.yml` | +| `project_id` | Use a different project for just this source. | `project_id: "other-project-id"` | +| `branch` | Upload into a Phrase branch instead of the main project. | `branch: my-feature` | +| `file_format` | The format for this source if it differs from the top-level. | `file_format: i18next` | +| `params` | Per-source upload settings (see below). | (see below) | + +### `params:` keys for push + +| Key | What it does | Example | +|---|---|---| +| `file_format` | The format for this upload. | `file_format: yml` | +| `locale_id` | Force this upload into a specific locale instead of guessing from the file path. | `locale_id: "English"` | +| `tags` | Tag every new key from this upload. Comma-separated for multiple tags. | `tags: "frontend,v2"` | +| `update_translations` | Overwrite existing translations with what's in the file. By default only brand-new keys are created. | `update_translations: true` | +| `update_translations_on_source_match` | For bilingual files, only update if the source text in the file matches the source on Phrase. | `update_translations_on_source_match: true` | +| `update_translation_keys` | Set to `false` to upload translations without creating any new keys. | `update_translation_keys: false` | +| `update_descriptions` | Replace existing key descriptions with whatever's in the file (an empty description wipes the existing one). | `update_descriptions: true` | +| `update_custom_metadata` | Update custom metadata fields from the file. An empty value deletes the field. | `update_custom_metadata: true` | +| `source_locale_id` | The source locale of a bilingual file. | `source_locale_id: "English"` | +| `skip_upload_tags` | Don't tag this upload with the auto-generated upload tag. | `skip_upload_tags: true` | +| `skip_unverification` | Don't mark touched translations as unverified after this upload. | `skip_unverification: true` | +| `file_encoding` | Force a file encoding. Allowed values: `UTF-8`, `UTF-16`, `ISO-8859-1`. | `file_encoding: UTF-16` | +| `autotranslate` | Auto-translate the uploaded locale after the upload finishes. | `autotranslate: true` | +| `verify_mentioned_translations` | Mark every translation in the file as verified. | `verify_mentioned_translations: true` | +| `mark_reviewed` | Mark every translation in the file as reviewed (only available if review workflow is on). | `mark_reviewed: true` | +| `tag_only_affected_keys` | Only apply `tags` to keys whose translations actually changed. | `tag_only_affected_keys: true` | +| `translation_key_prefix` | Add a prefix to every key from this upload. The placeholder `` works here too. | `translation_key_prefix: "web."` | +| `locale_mapping` | For CSV/XLSX uploads: which column holds which locale. |
locale_mapping:
en: 2
de: 3
| +| `format_options` | Format-specific settings — see [format-options.md](./format-options.md). |
format_options:
enable_pluralization: true
| + +### Push-only top-level setting (under `phrase.push`, not under `sources:`) + +| Key | What it does | Example | +|---|---|---| +| `delete_unmentioned_keys` | Delete any key that doesn't show up in any of the uploaded files. Same as the `--cleanup` / `-c` flag. Use carefully. | `delete_unmentioned_keys: true` | + +## Pull (`phrase.pull`) + +```yaml +pull: + targets: + - file: + project_id: # optional + file_format: # optional + params: + file_format: + locale_id: + # ...further download params (see table below)... +``` + +### Per-target keys (sibling to `params:`) + +| Key | What it does | Example | +|---|---|---| +| `file` | **Required.** Where downloaded files should land. Placeholders only — no `*` or `**`. | `file: src/locales//translation.json` | +| `project_id` | Pull from a different project for this target. | `project_id: "other-project-id"` | +| `file_format` | The format for this target if it differs from the top-level. | `file_format: arb` | +| `params` | Per-target download settings (see below). | (see below) | + +### `params:` keys for pull + +| Key | What it does | Example | +|---|---|---| +| `file_format` | The format to download. | `file_format: yml` | +| `locale_id` | Download just one specific locale. Use either this *or* a placeholder in `file:`, not both. | `locale_id: "English"` | +| `branch` | Download from a Phrase branch instead of the main project. | `branch: my-feature` | +| `tags` | Only download keys with these tags. Comma-separated for multiple. | `tags: "frontend,v2"` | +| `include_empty_translations` | Include keys that have no translation yet. | `include_empty_translations: true` | +| `exclude_empty_zero_forms` | For plurals, drop the "zero" form when it's empty. | `exclude_empty_zero_forms: true` | +| `include_translated_keys` | Combined with `include_empty_translations: true`, lets you flip to download only the *untranslated* keys. | `include_translated_keys: false` | +| `keep_notranslate_tags` | Keep `[NOTRANSLATE]` markers in the output. | `keep_notranslate_tags: true` | +| `format_options` | Format-specific download settings — see [format-options.md](./format-options.md). |
format_options:
enclose_in_cdata: true
| +| `encoding` | Force a file encoding. Allowed values: `UTF-8`, `UTF-16`, `ISO-8859-1`. | `encoding: UTF-8` | +| `include_unverified_translations` | Set `false` to skip unverified translations. | `include_unverified_translations: false` | +| `use_last_reviewed_version` | Download the last reviewed version of each translation (review workflow only). | `use_last_reviewed_version: true` | +| `fallback_locale_id` | If a translation is missing, fall back to this locale. Requires `include_empty_translations: true`. Don't combine with `use_locale_fallback`. | `fallback_locale_id: "English"` | +| `use_locale_fallback` | Use the fallback chain configured in your Phrase project. Don't combine with `fallback_locale_id`. | `use_locale_fallback: true` | +| `source_locale_id` | When downloading job-scoped files, the source locale to use. | `source_locale_id: "English"` | +| `translation_key_prefix` | Remove this prefix from key names in the downloaded file. | `translation_key_prefix: "web."` | +| `filter_by_prefix` | Only download keys starting with `translation_key_prefix`, and strip the prefix on the way out. | `filter_by_prefix: true` | +| `custom_metadata_filters` | Only download keys whose custom metadata matches. |
custom_metadata_filters:
team: mobile
| +| `locale_ids` | Limit the download to a list of specific locales. | `locale_ids: ["English", "German"]` | +| `updated_since` | Only download keys/translations changed since this date (ISO 8601). | `updated_since: "2026-01-01T00:00:00Z"` | + +## Placeholder rules + +| Token | Push | Pull | Replaced with | +|---|---|---|---| +| `` | yes | yes | ISO code (e.g. `en`, `de-AT`) | +| `` | yes | yes | Display name (e.g. `English`) | +| `` | yes | yes (requires `tags:` in `params:`) | Tag name | +| `*` | yes (at most once) | **NO** | Single-segment glob | +| `**` | yes (at most once) | **NO** | Recursive glob | + +Each placeholder appears at most once per `file:`. Pull `file:` must contain exactly one locale placeholder OR the target's `params.locale_id` must be set — not both. + +## Validation rules the generated file must satisfy + +- `pull.targets[].file` MUST NOT contain `*` or `**`. The CLI rejects wildcards in pull targets — only placeholders are allowed. If a push pattern uses a wildcard, generate the equivalent placeholder pattern for pull. +- Each placeholder (``, ``, ``) appears at most once per `file:` value. +- Pull targets must have **either** a locale placeholder in `file:` OR `locale_id:` in `params:`, not both. +- If `` is used in a pull `file:`, `tags:` must be set in `params:`. +- If `locale_mapping` is used, no two remote locales may map to the same on-disk name — the mapping must be reversible. +- Never write `access_token:` into the file. Use `PHRASE_ACCESS_TOKEN` env var. + +## JSON Schema + +Phrase has a public JSON Schema on SchemaStore: . Most YAML editors (VS Code with the YAML extension, JetBrains, anything that uses `yaml-language-server`) will auto-detect it from the filename `.phrase.yml` and provide autocomplete plus inline validation — no directive needed in the file itself. + +The schema covers the common subset (push/pull, basic params, format_options, locale_mapping). It does NOT cover every CLI-supported parameter — fields like `update_translation_keys`, `update_custom_metadata`, `source_locale_id`, `verify_mentioned_translations`, `delete_unmentioned_keys`, and `per_page` are missing. The tables above are the authoritative reference; the schema is a convenience. diff --git a/clients/cli/skills/phrase-config/references/troubleshooting.md b/clients/cli/skills/phrase-config/references/troubleshooting.md new file mode 100644 index 00000000..3ea4fd4e --- /dev/null +++ b/clients/cli/skills/phrase-config/references/troubleshooting.md @@ -0,0 +1,111 @@ +# Troubleshooting `.phrase.yml` + +Keyed by error message or symptom. Most failures come from a misconfigured `file:` pattern, a missing `tags:` parameter, or wildcards in the wrong place. + +## Common gotchas + +Things that catch people on first use. + +- **Android `values/` vs `values-en/`.** `values/` (no suffix) is the default locale. The `` placeholder will *not* match it. Push the source with an explicit fixed path + `locale_id:`, and only use the placeholdered pattern for translated locales. +- **Strings Catalog migration.** A project with both `*.lproj/Localizable.strings` and `*.xcstrings` is mid-migration. Pick one; don't push both, or the same keys will collide. +- **Source locale doesn't exist in Phrase yet.** The CLI won't auto-create it on first push. Add the locale in the Phrase web UI before pushing, or push will fail with "locale not found". +- **CSV with a BOM.** Excel-saved CSVs often have a UTF-8 BOM. Set `file_encoding: UTF-8` and verify the first column header parses cleanly — a stray BOM prepended to the key column header won't match `key_name_column: "Key"`. +- **Wildcards in pull.** `*` and `**` only work in push sources. Pull rejects them; use placeholders. +- **Filename guesses on push.** If the source path has a fixed name (e.g. `en.yml`) and no placeholder, set `locale_id:` explicitly. The CLI's filename guess can pick the wrong locale (`en` vs `en-US` vs `English`). +- **`update_translations: true` overwrites.** By default push only creates new keys; with this flag, existing translations get replaced by what's in the file. Standard for source-locale uploads, dangerous for translated ones. +- **`delete_unmentioned_keys: true` is project-wide.** It deletes any Phrase key not in the uploaded files — including keys for locales you didn't push. Only enable when push covers everything. +- **`` ≠ Android folder name.** Phrase emits `zh-CN`, Android expects `zh-rCN`. Use `locale_mapping:` to bridge — see `examples.md` → "Android `r`-prefixed regional locales". +- **Plurals in JSON need `enable_pluralization`.** Without it, `count_one` and `count_other` come in as two separate keys instead of one plural key. + +## "Locale not found" / "could not determine locale" + +The CLI couldn't map your file (push) or your `locale_id` (pull) to a Phrase locale. + +- Check that the locale exists in the Phrase project — Project Settings → Locales. +- `locale_id:` accepts both the locale code (`en`, `de-AT`) and the display name (`English`, `Austrian German`). Pick whichever matches Phrase exactly. +- On push, if the path has no `` / `` placeholder and `locale_id` isn't in `params:`, the CLI has nothing to go on — add `locale_id:`. +- On pull, the placeholder expands to the Phrase locale code by default. Android's `values-zh-rCN/` style needs a `locale_mapping:` entry (see `examples.md` → "Android `r`-prefixed regional locales"). + +## "File pattern matches no files" / "no source files found" (push) + +- The pattern is relative to repo root, not to where the user invoked the CLI from. Don't put `./` at the start. +- `*` matches a single path segment, `**` matches any number. They're not shell globs — `*.json` in a `file:` matches one segment, not a depth-3 path. +- Each placeholder appears at most once per `file:` value. +- Push sources can use `*` or `**` (at most once each). Pull targets cannot. + +## "Wildcard not allowed in pull target" + +`pull.targets[].file` rejects `*` and `**` outright. Replace them with placeholders: + +- `src/locales/*/translation.json` → `src/locales//translation.json` +- `**/*.arb` → `lib/l10n/app_.arb` (or whatever your real layout is — pull needs an exact path template, not a glob). + +## "Placeholder appears multiple times" / "duplicate placeholder" + +Each of ``, ``, `` may appear at most once per `file:`. If you legitimately need both `` and `` in the same path, that's allowed — the rule is one of *each*, not one total. See `examples.md` → "Splitting files per tag". + +## "Both placeholder and locale_id specified" + +A pull target may have **either** a locale placeholder in `file:` *or* `locale_id:` in `params:`, not both. Pick one: + +- Placeholder → CLI writes one file per locale. +- `locale_id` (with no placeholder) → CLI writes a single file for that locale. + +## "tags: required when using `` placeholder" + +If a pull target's `file:` contains ``, list the tags to expand under `params.tags:`. Push allows `` without an explicit `tags:` (the CLI infers tags from the matching files); pull does not. + +## "Locale mapping is not reversible" / "duplicate local name" + +In `locale_mapping:`, no two Phrase locales may map to the same on-disk name — the CLI needs to round-trip in both directions. Fix by choosing distinct names for each entry. + +## Push uploaded the wrong locale + +The CLI uses (in order): `params.locale_id`, then the placeholder in `file:`, then a guess from filename. If the source path has a fixed name like `en.yml`, set `locale_id:` explicitly — don't rely on filename guessing. + +## Pull wrote files into the wrong directory + +The path is relative to the directory where you ran `phrase pull`, not the repo root. Run from repo root, or set the working directory in your CI. + +## Push deleted keys I didn't expect + +`delete_unmentioned_keys: true` (or `--cleanup` / `-c`) removes any Phrase key not present in the uploaded files. If push only uploads the source locale, that's the *source* file's key set — anything missing from it gets deleted project-wide. Default is `false`; only enable it deliberately. + +## Pull downloads empty files / very few keys + +- `tags:` is set in `params:` and excludes most of the project. +- `locale_ids:` is set and limits to one locale. +- `updated_since:` is set and filtered out everything. +- `include_empty_translations: false` (the default) drops untranslated keys. +- Custom metadata filter (`custom_metadata_filters:`) is too narrow. + +## "Unauthorized" / 401 / 403 + +`PHRASE_ACCESS_TOKEN` is missing, expired, or wrong. Generate a new token at `https://app.phrase.com/settings/oauth_access_tokens` (or `/us/settings/...` on US) and `export PHRASE_ACCESS_TOKEN=` in your shell. Don't put it in `.phrase.yml`. + +## "Project not found" / 404 + +- `project_id:` is wrong. Find the correct one in Project Settings → API. +- You're hitting the wrong datacenter. EU users omit `host:`; US users set `host: https://api.us.app.phrase.com/v2`. + +## YAML parse errors / "could not load config" + +- The file MUST be wrapped in a top-level `phrase:` key. +- Tabs aren't valid YAML indentation — use spaces. +- VS Code with the YAML extension auto-loads the public schema from the `.phrase.yml` filename and flags issues inline. + +## CSV/XLSX upload skipped most rows + +`first_content_row:` defaults to `1`; if your file has a header row, set it to `2`. Also confirm `key_index` / `key_name_column` and your `translation_columns` map. + +## Android `values/` keeps getting wiped or duplicated + +`values/` (no suffix) is the default-locale folder; `values-/` are translations. Don't pull into `values//` — the path on disk is `values-/`. The source file lives at `values/strings.xml` and needs an explicit push source with `locale_id:` set; it does *not* match the `values-/` placeholder. + +## Plurals turning into separate keys (or vice versa) + +JSON-family formats need `format_options.enable_pluralization: true` to treat `thing_one` / `thing_other` as one plural key instead of two. iOS plural keys live in `.stringsdict` by default; set `include_pluralized_keys: true` on the `strings` source to keep them in `.strings`. + +## XLIFF round-trips lose state / extradata + +Set `include_translation_state: true` to round-trip the `state` attribute, and `export_key_id_as_resname: true` / `export_key_name_hash_as_extradata: true` if you depend on those XLIFF attributes downstream. From 291ec7f3353568d0b14fb30deafcc1e573185847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Behrendt?= Date: Thu, 7 May 2026 11:29:55 +0200 Subject: [PATCH 2/2] Use help center URL for format options --- clients/cli/skills/phrase-config/SKILL.md | 4 +- .../phrase-config/references/formats.md | 286 +++--------------- 2 files changed, 38 insertions(+), 252 deletions(-) diff --git a/clients/cli/skills/phrase-config/SKILL.md b/clients/cli/skills/phrase-config/SKILL.md index 699b7718..e3ac6c6a 100644 --- a/clients/cli/skills/phrase-config/SKILL.md +++ b/clients/cli/skills/phrase-config/SKILL.md @@ -12,7 +12,7 @@ Generates `.phrase.yml` for any project that uses the Phrase Strings CLI or Stri ## References (load on demand) - [`references/schema.md`](./references/schema.md) — every `.phrase.yml` key, placeholder rules, and the validation rules the generated file must satisfy. -- [`references/formats.md`](./references/formats.md) — detection rules, all 51 format identifiers + default file patterns, and per-format `format_options`. +- [`references/formats.md`](./references/formats.md) — detection rules, plus how to look up canonical identifiers and per-format `format_options` live (via `phrase formats list` and the Phrase help center). - [`references/examples.md`](./references/examples.md) — config examples for common project layouts. - [`references/troubleshooting.md`](./references/troubleshooting.md) — keyed by error message: locale not found, wildcard rejected, plural splitting, values/ vs values-en/, etc. - [`references/cli.md`](./references/cli.md) — install instructions, `phrase push` / `phrase pull` flags, and `PHRASE_*` environment variables. @@ -50,7 +50,7 @@ Then infer the path pattern from the discovered files: - iOS `de.lproj/Localizable.strings` → `.lproj/Localizable.strings`. - Flutter `lib/l10n/app_en.arb` → `lib/l10n/app_.arb`. -If detection fails, fall back to the format's default pattern from `formats.md`. +If detection fails, fall back to the `default_file` field from `phrase formats list` for the chosen `api_name`. **Source locale:** infer from the existing locale files (`en` is the most common). Don't ask — write the inferred value into `locale_id:` and tell the user in chat to change it if their Phrase project uses a different name. diff --git a/clients/cli/skills/phrase-config/references/formats.md b/clients/cli/skills/phrase-config/references/formats.md index 84601dbb..7d6a3ac8 100644 --- a/clients/cli/skills/phrase-config/references/formats.md +++ b/clients/cli/skills/phrase-config/references/formats.md @@ -1,266 +1,52 @@ -# Formats: detection, identifiers, and per-format options +# Formats: detection and live lookup -Three things in one file: how to recognize a project's format, the full list of identifiers + default file patterns, and per-format `format_options`. +No hardcoded format list. Identifiers, default file patterns, and `format_options` drift — always look them up live. -## Detection rules +## Step 1 — gather project signals -Walk the project tree from repo root. Ignore `node_modules/`, `vendor/`, `.git/`, `dist/`, `build/`, `target/`, `Pods/`, `.dart_tool/`. Apply rules in order; first match wins. For monorepos, multiple matches are allowed. +Walk repo root. Ignore `node_modules/`, `vendor/`, `.git/`, `dist/`, `build/`, `target/`, `Pods/`, `.dart_tool/`. Collect: -| Signal | `file_format` | Typical project | -|---|---|---| -| `pubspec.yaml` + `*.arb` files (often under `lib/l10n/`) | `arb` | Flutter | -| `*.xcstrings` files | `strings_catalog` | Apple String Catalog (Xcode 15+) | -| `*.lproj/Localizable.strings` | `strings` | iOS/macOS (legacy) | -| `*.lproj/Localizable.stringsdict` (alongside `.strings`) | `stringsdict` | iOS plurals (legacy) | -| `*.plist` files in localized form | `plist` | Objective-C/Cocoa property lists | -| `res/values*/strings.xml` | `xml` | Android | -| `*.po` files | `gettext` | Python/PHP/Django/C | -| `*.pot` template files | `gettext_template` | gettext templates | -| `config/locales/*.yml` + `Gemfile` mentioning `rails` | `yml` | Rails | -| `app/Resources/translations/*.yml` (Symfony layout) | `yml_symfony2` | Symfony | -| `app/Resources/translations/messages.*.xlf` (Symfony) | `symfony_xliff` | Symfony XLIFF | -| `resources/lang//messages.php` | `laravel` | Laravel | -| `*.php` array translation files (non-Laravel) | `php_array` | Generic PHP | -| `package.json` deps include `i18next` | `i18next` (or `i18next_4` if v4+) | i18next (React/Vue/Node) | -| `package.json` deps include `react-intl` — flat files | `react_simple_json` | React Intl flat | -| `package.json` deps include `react-intl` — nested files | `react_nested_json` | React Intl nested | -| `package.json` deps include `vue-i18n` (inspect file) | `simple_json` or `yml` | Vue i18n | -| `package.json` deps include `next-intl` or `next-translate` | `nested_json` | Next.js | -| `package.json` deps include `angular-translate` | `angular_translate` | AngularJS | -| `app/locales/*/translations.js` | `ember_js` | Ember.js | -| `*.go` with `go-i18n` and `*.all.json` | `go_i18n` | Go i18n | -| Chrome extension `_locales//messages.json` | `json` | Chrome i18n | -| `conf/messages.` | `play_properties` | Play Framework | -| `*.properties` under `src/main/resources/` | `properties` | Java | -| Mozilla addon `*.properties` | `mozilla_properties` | Firefox/XUL | -| `*.resx` files | `resx` | .NET / WinForms | -| `*.resw` files | `windows8_resource` | Windows Store apps | -| `Resources/AppResources..resx` | `resx_windowsphone` | Windows Phone | -| `*.xlf` / `*.xliff` (XLIFF 1.2) | `xlf` | Angular i18n, .NET MAUI, generic | -| `*.xlf` / `*.xliff` declared as XLIFF 2.0 (`version="2.0"`) | `xliff_2` | Modern XLIFF | -| `*.ts` (Qt translation source) | `ts` | Qt Linguist | -| `*.tmx` | `tmx` | Translation memory | -| `*.xlsx` | `xlsx` | Excel workflow | -| `*.csv` | `csv` | Spreadsheet workflow | -| `*.html` / `*.htm` | `html` or `html_5` | Marketing/CMS pages | -| `*.docx` | `docx` | Documentation | -| `locales/*.json` (flat key/value) | `simple_json` | Generic | -| `locales/*.json` (nested objects) | `nested_json` | Generic | +- Locale file extensions present (`.arb`, `.xcstrings`, `.strings`, `.stringsdict`, `.xml`, `.po`, `.pot`, `.xlf`/`.xliff`, `.resx`, `.resw`, `.properties`, `.json`, `.yml`/`.yaml`, `.php`, `.csv`, `.xlsx`, `.html`, `.docx`, `.ts`, `.tmx`, `.plist`, etc.). +- Directory layout hints (`*.lproj/`, `res/values*/`, `lib/l10n/`, `config/locales/`, `app/Resources/translations/`, `resources/lang//`, `_locales//`, `conf/messages.`). +- Manifest deps: + - `pubspec.yaml` → Flutter + - `package.json` → check for `i18next`, `react-intl`, `vue-i18n`, `next-intl`, `next-translate`, `angular-translate` + - `Gemfile` mentioning `rails` → Rails + - Symfony / Laravel layout markers + - Go with `go-i18n` +- For ambiguous JSON/XLIFF/Vue: read a sample file. + - Flat `{"key":"value"}` vs nested `{"a":{"b":"value"}}`. + - XLIFF root `version="1.2"` vs `version="2.0"`. + - Vue i18n with `.json` locale files vs `.yml`. -**Always read a sample file when the format is ambiguous:** +If multiple locale roots (e.g. both `ios/` and `android/`, or both `*.arb` and `locales/*.json`), surface that — emit one source/target per platform or ask which to wire. -- Generic JSON: open one locale file. Flat `{"key": "value"}` → `simple_json`. Nested `{"a": {"b": "value"}}` → `nested_json`. -- XLIFF: check the root element's `version=` attribute. `1.2` → `xlf`; `2.0` → `xliff_2`. -- Vue i18n: deps + a `.json` locale file → `simple_json` (or `nested_json`); deps + a `.yml` locale file → `yml`. +## Step 2 — resolve format identifier live -If detection finds **multiple** locale-file roots (e.g. both `*.arb` and `locales/*.json`, or both `ios/` and `android/`), surface that to the user and ask which to wire up — or whether to emit one source/target per platform. +Run: -If nothing matches, ask the user to pick from the identifiers below. +```sh +phrase formats list +``` -## Identifiers and default file patterns +Output is JSON. Each entry: -The CLI accepts any identifier returned by `phrase formats list`. The 51 public formats: +- `api_name` — value for `file_format:` / `params.file_format:`. +- `name` — human-readable display name (matches the support docs). +- `extension` — file extension. +- `default_file` — default pull pattern. Use as fallback when project layout doesn't dictate one. +- `importable` / `exportable` — whether push/pull supported. -| Identifier | Display name | Extension | Default file pattern | -|---|---|---|---| -| `arb` | ARB | `.arb` | `./.arb` | -| `yml` | Ruby/Rails YAML | `.yml` `.yaml` | `./config/locales/.yml` | -| `gettext` | Gettext | `.po` | `./.po` | -| `gettext_template` | Gettext template | `.pot` | `./.pot` | -| `gettext_mo` | Gettext compiled | `.mo` | `./.mo` | -| `xml` | Android Strings | `.xml` | `./values-/strings.xml` | -| `strings` | iOS Localizable Strings | `.strings` | `./.lproj/Localizable.strings` | -| `stringsdict` | iOS Localizable Stringsdict | `.stringsdict` | `./.lproj/Localizable.stringsdict` | -| `strings_catalog` | Apple Strings Catalog | `.xcstrings` | `./.xcstrings` | -| `xlf` | XLIFF 1.2 | `.xlf` `.xliff` | `./.xlf` | -| `xliff_2` | XLIFF 2.0 | `.xlf` `.xliff` | `./.xlf` | -| `symfony_xliff` | Symfony XLIFF | `.xlf` `.xliff` | `./messages..xlf` | -| `qph` | Qt Phrase Book | `.qph` | `./.qph` | -| `ts` | Qt Translation Source | `.ts` | `./.ts` | -| `json` | Chrome JSON i18n | `.json` | `./.json` | -| `simple_json` | Simple JSON (flat) | `.json` | `./.json` | -| `nested_json` | Nested JSON | `.json` | `./.json` | -| `react_simple_json` | React-Intl Simple JSON | `.json` | `./.json` | -| `react_nested_json` | React-Intl Nested JSON | `.json` | `./.json` | -| `i18next` | i18next | `.json` | `./locales//translations.json` | -| `i18next_4` | i18next 4 | `.json` | `./locales//translations.json` | -| `go_i18n` | Go i18n JSON | `.json` | `./.all.json` | -| `node_json` | i18n-node-2 JSON | `.js` | `./locales/.js` | -| `angular_translate` | Angular Translate | `.json` | `./i18n/.json` | -| `ember_js` | Ember.js | `.js` | `./app/locales//translations.js` | -| `genesys_json` | Genesys JSON | `.json` | `./.json` | -| `strings_json` | Strings JSON | `.json` | `./.json` | -| `resx` | .NET ResX | `.resx` | `./.resx` | -| `resx_windowsphone` | Windows Phone ResX | `.resx` | `./Resources/AppResources..resx` | -| `windows8_resource` | Windows 8 Resource | `.resw` | `./Resources/AppResources..resw` | -| `properties` | Java Properties | `.properties` | `./MessagesBundle_.properties` | -| `mozilla_properties` | Mozilla Properties | `.properties` | `./.properties` | -| `properties_xml` | Java Properties XML | `.xml` | `./MessagesBundle_.xml` | -| `play_properties` | Play Framework Properties | `.locale` | `./conf/messages.` | -| `ini` | INI | `.ini` | `./.ini` | -| `plist` | Objective-C/Cocoa Property List | `.plist` | `./.plist` | -| `tmx` | TMX Translation Memory | `.tmx` | `./.tmx` | -| `xlsx` | Excel XLSX | `.xlsx` | `./.xlsx` | -| `csv` | CSV | `.csv` | `./.csv` | -| `txt` | Tab-separated TXT | `.txt` | `./.txt` | -| `zendesk_csv` | Zendesk CSV | `.csv` | `./.csv` | -| `php_array` | PHP Array | `.php` | `./locale_.php` | -| `laravel` | Laravel/F3/Kohana Array | `.php` | `./resources/lang//messages.php` | -| `yml_symfony` | Symfony YAML | `.yml` `.yaml` | `./app/Resources/translations/.yml` | -| `yml_symfony2` | Symfony2 YAML | `.yml` `.yaml` | `./app/Resources/translations/.yml` | -| `episerver` | Episerver XML | `.xml` | `./.xml` | -| `linguist_xml` | Linguist XML | `.xml` | `./linguist.xml` | -| `linguist_xml_2` | Linguist XML 2 | `.xml` | `./linguist.xml` | -| `html` | HTML | `.html` `.htm` | `./.html` | -| `html_5` | HTML 5 | `.html` `.htm` | `./.html` | -| `docx` | Word DOCX | `.docx` | `./.docx` | +Match the project signals from Step 1 against `name` + `extension` + `default_file` to pick `api_name`. Confirm the choice with the user before writing. -The default file pattern is what `phrase init` would suggest for that format — a good fallback when you can't infer the pattern from existing files. +If signals don't disambiguate (e.g. generic `.json` could be `simple_json`, `nested_json`, `react_simple_json`, `i18next`, `go_i18n`, `json`/Chrome, …), narrow by manifest deps + file shape, then ask the user to confirm. -Full format guide with import/export quirks: . +## Step 3 — per-format options via support docs ---- +`format_options` are not in the CLI output. Look them up only when the user wants non-default behavior or asks about an option. -## Per-format options (`params.format_options`) +Help center: -Per-format options go under `params.format_options:` in `.phrase.yml`. They are not interchangeable across formats — only set options that the chosen format declares. - -### `xml` (Android Strings) - -| Option | What it does | Example | -|---|---|---| -| `convert_placeholder` | Convert `%s`-style placeholders to and from Android's `%1$s` style. | `convert_placeholder: true` | -| `escape_linebreaks` | Escape line breaks as `\n` when downloading. | `escape_linebreaks: true` | -| `unescape_linebreaks` | Turn `\n` back into real newlines on upload. | `unescape_linebreaks: true` | -| `enclose_in_cdata` | Wrap each value in ``. | `enclose_in_cdata: true` | -| `preserve_cdata` | Keep existing CDATA wrappers when re-uploading. | `preserve_cdata: true` | -| `escape_tags` / `unescape_tags` | Escape or unescape HTML/XML tags inside values. | `escape_tags: true` | -| `escape_android_chars` / `unescape_android_chars` | Escape or unescape Android-special characters (`'`, `"`, `@`, `?`, `\`). | `escape_android_chars: true` | -| `indent_size` | How many characters to indent. | `indent_size: 4` | -| `indent_style` | Indent with spaces or tabs. | `indent_style: "space"` | -| `include_tools_ignore` | Add `tools:ignore` attributes to keys that have them in Phrase. | `include_tools_ignore: true` | -| `include_tools_locale_definition` | Add `tools:locale` to the `` element. | `include_tools_locale_definition: true` | - -### `strings` (iOS) - -| Option | What it does | Example | -|---|---|---| -| `convert_placeholder` | Normalize placeholder syntax (`%s` ↔ `%@`). | `convert_placeholder: true` | -| `include_pluralized_keys` | Keep keys with plural forms in `.strings` (instead of moving them to `.stringsdict`). | `include_pluralized_keys: true` | -| `multiline_descriptions` | Wrap long key descriptions across multiple comment lines. | `multiline_descriptions: true` | - -### `stringsdict` - -| Option | What it does | Example | -|---|---|---| -| `convert_placeholder` | Normalize placeholder syntax. | `convert_placeholder: true` | - -### `strings_catalog` (Apple `.xcstrings`) - -| Option | What it does | Example | -|---|---|---| -| `convert_placeholder` | Normalize placeholder syntax. | `convert_placeholder: true` | -| `ignore_translation_state` | Don't read or write the translation state field. | `ignore_translation_state: true` | -| `default_extraction_state` | The `extractionState` value for new entries. | `default_extraction_state: "manual"` | - -### `xlf` (XLIFF 1.2) - -| Option | What it does | Example | -|---|---|---| -| `enclose_in_cdata` | Wrap target text in CDATA. | `enclose_in_cdata: true` | -| `content_as_literal` | Treat content as literal text — don't decode XML entities. | `content_as_literal: true` | -| `include_translation_state` | Add the `state` attribute to translation units. | `include_translation_state: true` | -| `replace_target_translations_with_empty_string` | When a translation is missing, write an empty target instead of falling back to the source. | `replace_target_translations_with_empty_string: true` | -| `keep_plural_skeletons` | Preserve the plural-form skeleton on round-trip. | `keep_plural_skeletons: true` | -| `ignore_source_translations` | Skip the source-language text on import. | `ignore_source_translations: true` | -| `ignore_target_translations` | Skip the target-language text on import. | `ignore_target_translations: true` | -| `export_key_id_as_resname` | Put the Phrase key id in the `resname` attribute. | `export_key_id_as_resname: true` | -| `export_key_name_hash_as_extradata` | Put a hash of the key name in the `extradata` attribute. | `export_key_name_hash_as_extradata: true` | -| `delimit_placeholders` | Wrap placeholders with delimiters during export. | `delimit_placeholders: true` | -| `strip_placeholder_delimiters` | Remove placeholder delimiters during import. | `strip_placeholder_delimiters: true` | -| `override_file_language` | Force the file's language attribute to match the downloaded locale. | `override_file_language: true` | -| `key_name_attribute` | Which XML attribute holds the key name. Defaults to `id`. | `key_name_attribute: "resname"` | -| `custom_metadata_columns` | Map your custom metadata fields to XLIFF attributes. |
custom_metadata_columns:
team: extradata
| - -### `xliff_2` (XLIFF 2.0) - -Most of the XLIFF 1.2 options work here too: `enclose_in_cdata`, `include_translation_state`, `replace_target_translations_with_empty_string`, `content_as_literal`, `keep_plural_skeletons`, `ignore_source_translations`, `ignore_target_translations`, `override_file_language`. - -### `symfony_xliff` - -| Option | What it does | Example | -|---|---|---| -| `enclose_in_cdata` | Wrap target text in CDATA. | `enclose_in_cdata: true` | - -### `simple_json` / `nested_json` / `react_nested_json` - -| Option | What it does | Example | -|---|---|---| -| `enable_pluralization` | Treat keys with plural suffixes (`_one`, `_other`, etc.) as plural variants of one key. | `enable_pluralization: true` | - -### `i18next` / `i18next_4` - -| Option | What it does | Example | -|---|---|---| -| `nesting` | Output nested objects (the default). Turn off for flat keys. | `nesting: false` | - -### `gettext` / `gettext_template` - -| Option | What it does | Example | -|---|---|---| -| `msgid_as_default` | When `msgstr` is empty, use `msgid` as the fallback. | `msgid_as_default: true` | -| `is_bilingual_file` | (`gettext` only) Treat the file as bilingual — both source and target. | `is_bilingual_file: true` | - -### `properties` (Java) - -| Option | What it does | Example | -|---|---|---| -| `escape_single_quotes` | Double single-quotes (`'` → `''`) for Java `MessageFormat`. | `escape_single_quotes: true` | -| `omit_separator_space` | Don't put spaces around `=`. | `omit_separator_space: true` | -| `crlf_line_terminators` | Use Windows line endings (`\r\n`). | `crlf_line_terminators: true` | -| `escape_meta_chars` | Escape backslashes, tabs, and other control characters. | `escape_meta_chars: true` | - -### `play_properties` - -| Option | What it does | Example | -|---|---|---| -| `escape_single_quotes` | Double single-quotes for Play's MessageFormat. | `escape_single_quotes: true` | - -### `csv` / `txt` - -| Option | What it does | Example | -|---|---|---| -| `column_separator` | Field separator. Defaults to `,` for CSV, tab for TXT. | `column_separator: ";"` | -| `quote_char` | Quote character. Defaults to `"`. | `quote_char: "'"` | -| `first_content_row` | Row number where actual translation rows start (1-based). | `first_content_row: 2` | -| `key_index` / `key_name_column` | Which column holds the key. Use either an index or a header name. | `key_name_column: "Key"` | -| `key_id_column` | Which column holds the Phrase key id. | `key_id_column: 1` | -| `translation_index` / `translation_indexes` / `translation_columns` | Which column(s) hold translations. Map locale → column. |
translation_columns:
en: 2
de: 3
| -| `comment_index` / `comment_column` | Which column holds key descriptions. | `comment_column: "Notes"` | -| `tag_column` | Which column holds tags. | `tag_column: 5` | -| `max_characters_allowed_column` | Which column holds the per-key character limit. | `max_characters_allowed_column: "Limit"` | -| `custom_metadata_columns` | Map custom metadata fields to columns. |
custom_metadata_columns:
team: 6
| -| `enable_pluralization` | Treat plural-suffixed keys as plurals. | `enable_pluralization: true` | -| `include_headers` | Write a header row when downloading. | `include_headers: true` | -| `export_tags` / `export_system_tags` | Include tag columns when downloading. | `export_tags: true` | -| `export_key_id` | Include the Phrase key id column when downloading. | `export_key_id: true` | -| `export_max_characters_allowed` | Include the character-limit column when downloading. | `export_max_characters_allowed: true` | -| `group_by_key_name` | Group rows by key name in the output. | `group_by_key_name: true` | - -### `xlsx` (Excel) - -These CSV options also work for Excel files: `first_content_row`, `key_name_column`, `translation_column`, `translation_columns`, `custom_metadata_columns`, `comment_column`, `tag_column`, `max_characters_allowed_column`, `enable_pluralization`, `export_tags`, `export_system_tags`, `export_max_characters_allowed`. - -### `strings_json` - -| Option | What it does | Example | -|---|---|---| -| `custom_metadata_columns` | Map custom metadata fields to JSON properties. |
custom_metadata_columns:
team: team_field
| -| `export_description` | Include key descriptions in the output. | `export_description: true` | -| `export_tags` / `export_system_tags` | Include tag fields in the output. | `export_tags: true` | -| `export_max_characters_allowed` | Include the per-key character limit. | `export_max_characters_allowed: true` | -| `include_translation_state` | Include the translation state field. | `include_translation_state: true` | -| `ignore_translation_state` | Skip importing translation state. | `ignore_translation_state: true` | -| `export_use_ordinal_rules` | Use ordinal plural rules (1st, 2nd, 3rd) instead of cardinal. | `export_use_ordinal_rules: true` | - -Formats not listed above (`arb`, `yml`, `json`, `react_simple_json`, `qph`, `ts`, `go_i18n`, `node_json`, `resx`, `resx_windowsphone`, `windows8_resource`, `mozilla_properties`, `properties_xml`, `plist`, `ini`, `tmx`, `zendesk_csv`, `php_array`, `laravel`, `yml_symfony`, `yml_symfony2`, `episerver`, `linguist_xml`, `linguist_xml_2`, `angular_translate`, `ember_js`, `genesys_json`, `html`, `html_5`, `docx`, `gettext_mo`) currently expose **no per-format options**. +1. Fetch the help center page. Find the link for the format's display `name` (e.g. "Android Strings", "Apple Strings Catalog"). +2. Fetch that detail page. Read the **Format Options** section — each option lists the YAML key, accepted values, behavior. +3. Only set options the page declares for that format. If a requested option isn't there, push back — likely belongs to a different format or has been renamed.