Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 238 additions & 35 deletions Readme.md

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions examples/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Example project wording skills

`sync-wording`'s **packaged skill**
(`node_modules/@betomorrow/sync-wording/skills/sync-wording/SKILL.md`) teaches an
agent *how to operate the tool* — commands, configuration, exit codes, workflows.
It is generic, versioned with the library, and `sync-wording init` wires a lazy
pointer to it from your `AGENTS.md` / `CLAUDE.md`.

A **project skill** is the complement: it captures *how your project does
wording* — things the library cannot know:

- your languages and column order, output paths, format;
- your key-naming convention;
- your translation hook (`useI18n`, a `t()` function, an i18n service…);
- project workflows (e.g. "turn hardcoded strings into keys").

The two are orthogonal and both worthwhile.

## The golden rule: delegate, don't duplicate

An efficient project skill **does not repeat the tool's command reference** — it
points to the packaged skill for that and keeps only project knowledge. Why:

- the skill loads on **every** wording task, so keeping it small protects the
agent's context window;
- duplicated command docs **drift** when the tool evolves (a skill that still says
"editing keys is not supported" after `--set` shipped is worse than no skill).

`init` already scaffolds a minimal `.claude/skills/wording/SKILL.md` stub that
points to the packaged skill. These examples show how to enrich that stub.

## Examples

- **`wording-project-context/`** — the minimal useful skill: project context +
delegation. Start here.
- **`wording-from-hardcoded-strings/`** — a richer workflow: detect hardcoded
strings, propose keys, write locally, refactor, verify, then sync.
- **`audit-wording-usage/`** — find unused keys and remaining hardcoded strings.

The first two are two approaches to the same thing — your project's main wording
skill; pick one and name it `wording` in your project. The audit skill is
complementary (and triggers only on an explicit `/wording-audit`).

Copy one into `.claude/skills/<name>/SKILL.md`, replace every `<placeholder>` with
your project's values, and trim what you don't need. If a skill grows past ~100
lines, split reference material (e.g. key conventions) into a sibling file and
link to it.
37 changes: 37 additions & 0 deletions examples/skills/audit-wording-usage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: wording-audit
description: Manual i18n wording audit for <project>, run ONLY when the user explicitly invokes /wording-audit. Does not auto-activate. Finds unused keys, remaining hardcoded strings and missing translations.
---

# /wording-audit — <project>

Find wording problems. For sync-wording commands (e.g. `--set`/`--remove` to fix
them), read `node_modules/@betomorrow/sync-wording/skills/sync-wording/SKILL.md`.

**Trigger:** only on an explicit `/wording-audit` invocation. This skill must not
auto-activate during ordinary wording or coding tasks — auditing is a deliberate,
on-demand action.

## Project context

- Keys live in `<output paths>` (<format>), consumed via `<t("key")>`.
- Source code to scan: `<src/>`.
- Key naming convention: `<feature.screen.element>`.

## Checks

1. **Unused keys** — for each key in the <reference-language> file, grep `<src/>`
for the key string. Also account for dynamic usage (`<t(`prefix.${x}`)>`) that
builds keys at runtime. Report keys with no hit as **candidate** unused —
never delete automatically; dynamic usage is easy to miss.
2. **Remaining hardcoded strings** — scan components for literal user-facing text
not wrapped in `<t(...)>` (JSX text nodes, `title=`/`label=`/`placeholder=`
props). Report with `file:line`.
3. **Missing translations** — keys present in the <reference-language> file but
empty/absent in another language file.

## Output

A report grouped by check, with `file:line` and the offending key/string. Propose
fixes — create a key, remove an unused one (`--remove`), fill a missing
translation (`--set`) — but apply them only after the user confirms.
49 changes: 49 additions & 0 deletions examples/skills/wording-from-hardcoded-strings/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: wording
description: Create i18n keys from hardcoded strings in <project> and sync them. Use when hardcoded UI strings appear, a feature needs translation keys, or the user runs /wording.
---

# /wording — <project>

Turn hardcoded strings into i18n keys and sync them to the source spreadsheet.

For sync-wording commands, configuration and exit codes, read
`node_modules/@betomorrow/sync-wording/skills/sync-wording/SKILL.md`. This skill
adds the project context and the key-creation workflow only.

## Project context

- **Languages (column order):** <fr (B), en (C), …> — values for `--add` must be
given in this exact order.
- **Config:** `<wording_config.json>` — <target(s) and output paths>
- **Format:** <flat-json>
- **Usage in code:** `<t("my.key")>` (import: `<…>`)
- **Key naming convention:** `<feature.screen.element.property>` — examples:
`<auth.login.title>`, `<journey.search.button.label>`

## Workflow

1. **Analyze** — find hardcoded user-facing strings in the given files (JSX text,
`title=`/`label=`/`placeholder=` props, error messages). Check the
<reference-language> file for an existing key that already covers the need
before creating a new one.
2. **Propose** — for each new string, a convention-following key + translations in
all languages (reference language first). Show a table and **ask for
confirmation** before writing anything.
3. **Write locally** — insert the keys into the per-language files, keeping them
sorted.
4. **Refactor** — replace the hardcoded strings with `<t("…")>`; add the
import/hook if missing.
5. **Verify** — `<your type-check or build command>` confirms the new keys
resolve. This needs only the local files, not the sheet. Proceed only when green.
6. **Sync** — the sheet write is the only irreversible, shared step, so do it last:
```bash
npx sync-wording <--target X> --add "<key>" <one value per language, in column order>
```
Several keys can go in one command (repeat `key + values`). If it exits with
code 3, run `npx sync-wording auth` once (browser, interactive).

## Edit / remove existing keys

`--set` updates a key in place; `--remove` deletes its row (confirm first). See
the packaged skill for exact semantics.
28 changes: 28 additions & 0 deletions examples/skills/wording-project-context/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
name: wording-project-context
description: Project wording context for <project>. Use for any i18n/translation, wording or sync task. Pairs with the packaged sync-wording skill.
---

# /wording — <project>

For sync-wording commands, configuration semantics, exit codes and workflows,
read `node_modules/@betomorrow/sync-wording/skills/sync-wording/SKILL.md`.
This skill only adds <project>-specific context.

## Project context

- **Languages (Google Sheet column order):** <fr (B), en (C), …>
- **Config:** `<wording_config.json>` — <flat, or multi-target: list each target
and its output, e.g. `app` → `src/assets/i18n/`, `accessibility` → `src/assets/a11y/`>
- **Format:** <json | flat-json | angular-json>
- **How translations are consumed in code:**
```ts
<e.g. const { t } = useTranslation(); t("my.key")>
```
- **Key naming convention:** <e.g. feature.screen.element.property>

## Notes

- The Google Sheet is the source of truth; generated files are build output —
never edit them by hand (a sync overwrites them).
- <Any post-sync step, files to commit together, validation column owner, etc.>
6 changes: 1 addition & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@betomorrow/sync-wording",
"version": "1.2.8",
"version": "2.0.0",
"description": "Provide tool to retrieve app wording from Google Sheet and process it to generate i18n json files",
"main": "lib/index.js",
"bin": "lib/index.js",
Expand Down Expand Up @@ -37,6 +37,8 @@
"typescript": "^6.0.3"
},
"files": [
"lib/"
"lib/",
"skills/",
"examples/"
]
}
155 changes: 155 additions & 0 deletions skills/sync-wording/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
name: sync-wording
description: Sync application wordings (i18n) from a Google Sheet to local translation files using the @betomorrow/sync-wording CLI. Use when asked to update, sync, pull, add or fix translations, wordings, or i18n keys.
---

# sync-wording

Sync app wordings from a Google Sheet to local i18n files. The Google Sheet
is the source of truth; generated files must never be edited by hand.

## When to use

- The user asks to update, sync, refresh, or pull translations/wordings
- The user asks to add or change a translation key
- A `wording_config.json` (or a file passed via `--config`) exists in the project

## Commands

```bash
npx sync-wording --upgrade # download the sheet as xlsx, then generate all files
npx sync-wording --update # regenerate files from the local xlsx (no download)
npx sync-wording --check # validate config and report what would be written (writes nothing)
npx sync-wording --target <name> ... # restrict any command to one target
npx sync-wording --add <key> <values...> # append key + one value per language to the sheet
npx sync-wording --set <key> <values...> # update an existing key in the sheet
npx sync-wording --remove <keys...> # delete row(s) from the sheet by key — destructive
npx sync-wording --sheet-name <tab> ... # pick a sheet tab for --add / --set / --remove

npx sync-wording auth # interactive Google login (stores a token, run once)
npx sync-wording init # scaffold config and agent wiring
```

Prefer `--check` before a real sync to validate the setup, and read the
`Summary:` block printed at the end of a sync to verify the result.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Invalid translations found (with `--invalid error`) |
| 2 | Configuration error (missing/invalid config, unknown target, missing sheet) |
| 3 | Authentication error — run `npx sync-wording auth` |
| 10 | Unexpected error |

## Configuration

Read `wording_config.json` at the project root — it is the live source of
truth, never assume its content. Two shapes exist:

**Flat (single target):**

```json
{
"sheetId": "<Google Sheet id>",
"sheetNames": ["Commons", "MyApp"],
"format": "angular-json",
"output_dir": "src/assets/i18n/",
"languages": { "fr": { "column": "D" }, "en": { "column": "C" } }
}
```

**Multi-target (one document, several syncs — e.g. app strings, accessibility
keys, iOS permission messages):**

```json
{
"sheetId": "<Google Sheet id>",
"targets": {
"app": { "sheetNames": ["MyApp"], "format": "angular-json", "output_dir": "src/assets/i18n/", "languages": { "fr": { "column": "D" } } },
"accessibility": { "sheetNames": ["Accessibility"], "format": "flat-json", "output_dir": "src/assets/a11y/", "languages": { "fr": { "column": "C" } } }
}
}
```

Field semantics:

| Field | Level | Meaning |
|---|---|---|
| `sheetId` | document | Google Sheet id (from its URL). Required |
| `credentials` | document | path to a Google OAuth client file; embedded credentials by default |
| `wording_file` | document | local xlsx cache path, default `./wording.xlsx` |
| `sheetNames` | target | sheet tabs to read, merged in order — **on duplicate keys, the last sheet wins**. Default: all tabs |
| `sheetStartIndex` | target | first data row, default 2 (row 1 = headers) |
| `keyColumn` | target | column holding the keys, default `A` |
| `format` | target | `json` (nested), `flat-json`, or `angular-json` |
| `output_dir` | target | where files are written, one `<language>.json` per language |
| `languages.<name>.column` | target | sheet column for that language |
| `languages.<name>.output` | target | optional per-language output path override |
| `ignoreEmptyKeys` | target | skip keys with empty values, default false |
| `validation` | target | `{ "column": "E", "expected": "OK" }` — keys whose validation cell differs are reported invalid |

Document-level values act as defaults for all targets; a target can override
any of them. A flat config is equivalent to a single target named `default`.

**Discovery:** a project may use several Google Sheet documents — check
`package.json` scripts for `sync-wording --config <other-file>` invocations
before assuming `wording_config.json` is the only configuration.

## Workflows

**Routine sync** (most common request):
1. `npx sync-wording --upgrade`
2. Verify the `Summary:` block; report invalid keys to the user if any
3. Commit the regenerated files together with the updated `wording.xlsx`

**Add a key:** `npx sync-wording --add my.key "English value" "Valeur française"`
— one value per configured language, in the order they appear in the config.
With multiple targets, add `--target <name>`. Then run `--upgrade` to
regenerate local files.

**Remove a key:** `npx sync-wording --remove my.key` — deletes the whole row
from the spreadsheet. This is destructive and not recoverable from the tool:
confirm with the user before removing, and run `--upgrade` afterwards to
regenerate local files.

**Sheet tab selection for `--add`/`--set`/`--remove`:** `--add` writes to the
first `sheetNames` entry of the target unless `--sheet-name <tab>` picks
another; `--set` and `--remove` search the key in all the target's sheets
unless `--sheet-name` narrows it to one. If the targeted key exists in several
sheets, the command refuses to guess and asks for `--sheet-name` (duplicates
of other keys never block the operation). `--sheet-name` must belong to the
selected target — the tool rejects a tab from another target (exit 2),
because it would write translations with the wrong column mapping.

**Add a language:** add the entry under `languages` in the config (column from
the sheet), then `--update` or `--upgrade`.

**First setup:** `npx sync-wording init --sheet-id <id> --languages fr:D,en:C`,
then `npx sync-wording auth` (interactive — the user must do this step),
then `npx sync-wording --check`.

## Upgrading from 1.x

Run `npx sync-wording init` — non-destructive (keeps the config, wires the
skill, validates). Keep the flat config as-is; do not convert it to `targets`.
Auth is now explicit: exit 3 → run `npx sync-wording auth`. Full guide in the
project README.

While upgrading, check whether the project has several config files syncing
the **same** `sheetId` (grep the repo — package.json scripts, CI, Makefiles —
for `--config <file>`). If so, offer to consolidate them into one multi-target
config: the document is downloaded once and `--target` runs one or all. Only
with the user's confirmation, and you MUST replace every `--config <file>` call
with `--target <name>` and remove the merged files, or builds break. Configs on
different documents stay separate; a single flat config stays as-is.

## Troubleshooting

- **Exit 3 / authentication error**: a stored token is missing. Ask the user to
run `npx sync-wording auth` — it opens a browser and cannot run unattended.
- **Exit 2 / sheet not found**: the tab names in `sheetNames` don't match the
document; the error lists the available tabs.
- **Invalid translations reported**: cells in the validation column don't match
the expected value. This is a content issue for the wording owner, not a bug.
Loading