diff --git a/clients/cli/skills/phrase-config/SKILL.md b/clients/cli/skills/phrase-config/SKILL.md new file mode 100644 index 00000000..e3ac6c6a --- /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, 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. + +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 `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. + +#### 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..7d6a3ac8 --- /dev/null +++ b/clients/cli/skills/phrase-config/references/formats.md @@ -0,0 +1,52 @@ +# Formats: detection and live lookup + +No hardcoded format list. Identifiers, default file patterns, and `format_options` drift — always look them up live. + +## Step 1 — gather project signals + +Walk repo root. Ignore `node_modules/`, `vendor/`, `.git/`, `dist/`, `build/`, `target/`, `Pods/`, `.dart_tool/`. Collect: + +- 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`. + +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. + +## Step 2 — resolve format identifier live + +Run: + +```sh +phrase formats list +``` + +Output is JSON. Each entry: + +- `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. + +Match the project signals from Step 1 against `name` + `extension` + `default_file` to pick `api_name`. Confirm the choice with the user before writing. + +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. + +## 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. + +Help center: + +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. 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.