# @theorchard/frontend-cli-i18n

CLI tool for managing translation files in a front-end project. It discovers translation fragments next to source code, compiles them into locale files, generates TypeScript type definitions, and synchronizes with [POEditor](https://poeditor.com/) to handover for the translation.

## Installation

```sh
npm install --save-dev @theorchard/frontend-cli-i18n
# or
pnpm add -D @theorchard/frontend-cli-i18n
```

Requires Node.js >= 20.

## Usage

```sh
frontend-i18n <command> [options]
```

## Commands

### `init`

Scans input paths for translation fragments, compiles them into a single `default.json`, and generates index and type files in the output directory.

```sh
frontend-i18n init [options]
```

### `sync`

Uploads the compiled `default.json` to POEditor, then downloads all translations back and compiles them into per-language JSON files.

Requires `POEDITOR_API_TOKEN` and `POEDITOR_PROJECT_ID` environment variables (or a `.env` file).

```sh
frontend-i18n sync [options]
```

### `watch`

Runs `init` once and then watches input paths for changes to fragment files, recompiling automatically on every change.

```sh
frontend-i18n watch [options]
```

## Options

### Shared options (all commands)

| Option                        | Description                                                                   | Default  |
| ----------------------------- | ----------------------------------------------------------------------------- | -------- |
| `-i, --input-paths <path...>` | One or more paths to scan for fragment files (space-separated)                | —        |
| `-o, --output-path <path>`    | Directory where compiled locale files are written                             | `locale` |
| `-g, --global-scope`          | Assign the locale map to `window.ORCHARD_LOCALES` in the generated index file | `false`  |
| `-ts, --typescript`           | Generate `index.ts` and `types.ts` instead of `.js` files                     | `false`  |

### `sync`-only options

| Option                | Description                                                             | Default |
| --------------------- | ----------------------------------------------------------------------- | ------- |
| `-d, --download-only` | Skip uploading local terms; only download translations from POEditor    | `false` |
| `--delete-terms`      | Delete terms in POEditor that are not found in the local `default.json` | `false` |

## Configuration file

Options can also be set in a config file. The tool looks for configuration in the following locations (via [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig)):

-   `i18n.config.js`
-   `i18n.config.json`
-   `.i18nrc`
-   `.i18nrc.json`
-   `.i18nrc.js`
-   `package.json` under the `"i18n"` key

CLI arguments take precedence over config file values, which take precedence over defaults.

### CLI / standalone defaults

When using the CLI directly, only `outputPath` has an explicit default. All other options fall back to the Workspace defaults listed in the table below.

```js
// i18n.config.js — CLI standalone
export default {
    outputPath: 'locale', // all other options fall to Workspace defaults
};
```

### Plugin defaults

When the tool runs as a Vite/frontend-cli plugin (`createI18nConfig`), the following options are set automatically and **differ from the standalone CLI**. They can still be overridden via the `"i18n"` key in `frontend.json`.

```js
// defaults injected by createI18nConfig (plugin path only)
{
    typescript: true,
    globalScope: true,
    enableGlobalFormatters: false,
    defaultLanguage: 'en',
    disableFragmentOutput: true,
    fragmentKeyByFolder: 'self',
    fragmentKeyByFilename: false,
    fragmentKeyByInputFolder: true,
    fragmentRootKey: false,
    // inputPaths and outputPath are derived from project context
}
```

### All options

| Option                     | Type                                    | CLI/standalone default | Plugin default           | Description                                                            |
| -------------------------- | --------------------------------------- | ---------------------- | ------------------------ | ---------------------------------------------------------------------- |
| `inputPaths`               | `string[]`                              | `[]`                   | _(derived from context)_ | Paths to scan for fragment files                                       |
| `outputPath`               | `string`                                | `'locale'`             | _(derived from context)_ | Directory where compiled locale files are written                      |
| `typescript`               | `boolean`                               | `false`                | `true`                   | Generate `.ts` files instead of `.js`                                  |
| `globalScope`              | `boolean`                               | `false`                | `true`                   | Assign locale map to `window.ORCHARD_LOCALES`                          |
| `enableGlobalFormatters`   | `boolean`                               | _(unset)_              | `false`                  | Expose formatter functions in global scope                             |
| `target`                   | `'web' \| 'node'`                       | `'web'`                | `'web'`                  | Build target                                                           |
| `defaultLanguage`          | `string`                                | `'en'`                 | `'en'`                   | Language code used as the default                                      |
| `defaultCode`              | `string`                                | `'default'`            | `'default'`              | Filename used for the default translation (e.g. `i18n/default.json`)   |
| `locales`                  | `string[]`                              | `[]`                   | `[]`                     | Restrict sync to specific locale codes (empty = all)                   |
| `fragmentFileName`         | `string`                                | `'i18n.json'`          | `'i18n.json'`            | Name of JSON fragment files to discover                                |
| `enableInlineFragments`    | `boolean`                               | `false`                | `false`                  | Allow inline fragment definitions                                      |
| `fragmentKeyByFolder`      | `false \| 'self' \| 'self-and-parents'` | `false`                | `'self'`                 | Derive key from folder name                                            |
| `fragmentKeyByFilename`    | `boolean`                               | `true`                 | `false`                  | Use the part before `.i18n.` in filenames as the key                   |
| `fragmentKeyByInputFolder` | `boolean`                               | `false`                | `true`                   | Prefix key with input directory name                                   |
| `fragmentRootKey`          | `string \| false`                       | `'app'`                | `false`                  | Wrap top-level string values under this key; `false` disables wrapping |
| `fragmentKeyCasing`        | `'camel' \| 'pascal'`                   | `'camel'`              | `'camel'`                | Casing applied to fragment keys                                        |
| `disableFragmentOutput`    | `boolean`                               | `false`                | `true`                   | Do not write per-fragment files under `locale/fragments/`              |
| `disableFragmentMeta`      | `boolean`                               | `false`                | `false`                  | Do not include `__meta__` field in fragment files                      |
| `fallbackToDefault`        | `boolean`                               | `false`                | `false`                  | Use default language value when a translation is missing               |
| `mergeDefault`             | `boolean`                               | `false`                | `false`                  | Merge into existing `default.json` instead of replacing it             |

## Fragment files

The tool discovers three types of fragment files in your source tree:

| Pattern                      | Description                                                                       |
| ---------------------------- | --------------------------------------------------------------------------------- |
| `**/i18n.json`               | A single JSON file named `i18n.json` at any depth                                 |
| `**/i18n/<defaultCode>.json` | A JSON file inside an `i18n/` folder (e.g. `i18n/default.json`)                   |
| `**/*.i18n.*`                | Any file whose name contains `.i18n.` (e.g. `Button.i18n.json`, `Button.i18n.ts`) |

### Fragment key resolution

Each fragment is assigned a key that determines where its contents are nested in the compiled output. Key resolution depends on config:

-   **`fragmentKeyByFolder: 'self'`** — uses the immediate parent folder name
-   **`fragmentKeyByFolder: 'self-and-parents'`** — uses the full folder path from the input root
-   **`fragmentKeyByFilename: true`** (CLI/standalone default; plugin default is `false`) — uses the part before `.i18n.` in the filename (e.g. `Button.i18n.json` → key `button`)
-   **`fragmentKeyByInputFolder: true`** — prefixes the key with the name of the input directory
-   **`fragmentRootKey`** — wraps bare string values at the root of a fragment under this key (CLI/standalone default `'app'`; plugin default is `false`, which disables wrapping)

Key casing is controlled by `fragmentKeyCasing`: `'camel'` (default) or `'pascal'`.

## Output structure

After running `init`, the output directory contains:

```
locale/
├── default.json          # Compiled default-language translations
├── <lang>.json           # Per-language translations (after sync)
├── index.js              # Locale map export (index.ts with --typescript)
├── types.ts              # TypeScript type definitions (with --typescript)
└── fragments/
    └── <fragmentKey>/
        ├── default.json
        ├── <lang>.json
        └── index.js
```

### Generated `index.js`

```js
import default_ from './default.json';
import es from './es.json';

export const locales = {
    en: default_,
    es: es,
};

export default locales;
```

With `--global-scope`, the file additionally assigns `window.ORCHARD_LOCALES = locales`.

### TypeScript types (`--typescript`)

When TypeScript mode is enabled, a `types.ts` file is generated with a typed formatter for each translation key. Placeholders use `{name}` syntax and plural forms are expressed with `one`/`other` keys:

```json
{
    "greeting": "Hello, {name}!",
    "itemCount": {
        "one": "{count} item",
        "other": "{count} items"
    }
}
```

## POEditor sync

The `sync` command requires two environment variables:

```sh
POEDITOR_API_TOKEN=<your-api-token>
POEDITOR_PROJECT_ID=<your-project-id>
```

These can be set in a `.env` file at the project root. The tool uploads the compiled `default.json` as the source of truth, then downloads all available translations and writes them to the output directory.

Use `--download-only` to skip the upload step (useful in CI to download the latest translations without modifying the POEditor project).

Use `--delete-terms` with caution — it will permanently remove from POEditor any terms not present in the local `default.json`.

## Examples

```sh
# Compile fragments from src/ into locale/
frontend-i18n init -i src -o locale

# Same, generating TypeScript files
frontend-i18n init -i src -o locale --typescript

# Watch src/ for changes and recompile automatically
frontend-i18n watch -i src

# Full sync with POEditor (upload + download)
frontend-i18n sync -i src -o locale

# Download translations only (no upload)
frontend-i18n sync -i src -o locale --download-only

# Sync and remove stale terms from POEditor
frontend-i18n sync -i src -o locale --delete-terms
```
