The foony-translate CLI ships inside @foony/translate. It parses your sources with a real AST (ts-morph), so it extracts exactly what the runtime will hash, and it errors with file:line on patterns it cannot translate.
npx foony-translate <init|scan|translate|check> [--dry-run]
Configuration: foony-translate.json
init writes a skeleton at your project root. The CLI finds it from any working directory below it; paths are relative to the config file’s directory.
{
"locales": ["fr", "de", "es"],
"defaultLocale": "en",
"src": ["src"],
"dictionaries": [],
"out": "src/translations",
"apiUrl": "https://translate-api.foony.io"
}
| Field | Meaning |
|---|---|
locales |
The target locales, and the source of truth for them: every translate run translates exactly these and syncs the project’s list shown in the dashboard. |
defaultLocale |
Source locale. Only en is supported for now. |
src |
Directories scanned for <T> and t() usage (.ts/.tsx). |
dictionaries |
Files containing defineDict calls or exported object literals. |
out |
Directory the per-locale JSON files are written to. Commit it. |
apiUrl |
The Foony Translate API. Leave the default unless you are told otherwise. |
Authentication
The API key comes exclusively from the environment, never the config file:
export FOONY_TRANSLATE_API_KEY=your-project.kid_xxx:sk_xxx
Create a prod key on the project’s API keys tab. scan and check need no key at all.
Commands
init
Writes foony-translate.json and creates the output directory. Fails if the config already exists.
scan
Extracts every entry and prints counts plus any diagnostics. Exits non-zero on errors like ternaries inside <T>, bare dynamic expressions, or dynamic t() messages.
214 entries (167 <T>, 47 strings), 0 error(s)
translate
The whole loop: scan, upload new entries, wait for the LLM worker, download and write out/<locale>.json.
npx foony-translate translate
- Only entries the service has not seen count as new. Unchanged content re-runs free.
--dry-runprints every entry (id or hash plus a source preview) and uploads nothing.- Failed translations are reported at the end; inspect and retry them from the dashboard’s Translations tab.
check
The offline CI gate: verifies every scanned entry has a translation in every configured locale, using only the committed JSON. No network, no key. Exits non-zero and prints file:line for anything missing.
CI recipe
Fail any PR that adds copy without translating it:
# .github/workflows/i18n.yml
name: i18n
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with: { node-version: 22 }
- run: npm ci
- run: npx foony-translate check
When the check fails, a developer runs translate locally (or a scheduled job does) and commits the updated JSON in the same PR. Because check is offline, the workflow needs no secret, so it is safe on forks.