# Loccy documentation
> Open-source i18n devtooling for React and Vue: inline translation previews, AI extraction and translation in VS Code, and a CLI linter for CI/CD.
# Loccy documentation
Loccy is open-source i18n devtooling for React & Vue web apps. It ships as two tools that share one configuration:
- **IDE extension**: preview, edit, extract, and translate strings inline in VS Code (and Cursor, Windsurf, and other VS Code-compatible editors). Core features are free; AI assistance comes with a Pro subscription.
- **Linter**: catches unused keys, missing translations, and unsorted translation files, locally and in CI — free and offline.
Both read the same [`loccy.yaml`](/docs/config) from your repository: configure once, use everywhere.
::info
Still on `loccy.config.json`? Loccy now configures via `loccy.yaml`. Run **Loccy: Migrate loccy.config.json to loccy.yaml** from the Command Palette to migrate.
::
## [Extension](/docs/extension)
- [Installation](/docs/extension/installation): install the extension and sign in
- [Core features](/docs/extension/core-features): inline previews, in-place editing, usage maps, renaming
- [AI features](/docs/extension/ai-features): extract, translate, and refine copy in the editor
## [Linter](/docs/linter)
- [Get started](/docs/linter): install the offline linter and set up `loccy.yaml`
- [Command reference](/docs/linter/commands): `init`, `lint`, and `format`
## Reference
- [Config](/docs/config): generate the config, plus every key, translation layout, and lint rules
- [Styleguide](/docs/config/styleguide): the AI guidance half of `loccy.yaml` (voice, per-language rules, glossary, key naming)
---
# Loccy for VS Code
The Loccy extension brings i18n into your editor: see translations inline, edit them in place, jump to every usage, rename keys across code and locale files, and, with a Pro subscription, extract and translate strings with AI. Works in VS Code, Cursor, Windsurf, and other VS Code-compatible editors.
It reads your [`loccy.yaml`](/docs/config), the same file the [linter](/docs/linter) uses.
- [Installation](/docs/extension/installation): install the extension and sign in
- [Core features](/docs/extension/core-features): inline previews, in-place editing, usage maps, renaming
- [AI features](/docs/extension/ai-features): extract, translate, and refine copy in the editor
- [Troubleshooting](/docs/extension/troubleshooting): keys not detected, previews missing, and more
---
# Installation
::info
Still on `loccy.config.json`? Loccy now configures via `loccy.yaml`. Run **Loccy: Migrate loccy.config.json to loccy.yaml** from the Command Palette to migrate.
::
The Loccy extension works in any VS Code-compatible editor:
- Visual Studio Code
- Cursor
- Windsurf
- Kiro
- Antigravity
Open the Extensions view (`Cmd+Shift+X` / `Ctrl+Shift+X`), search **Loccy**, and click Install. Available on both the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=loccy.loccy) and [Open VSX](https://open-vsx.org/extension/loccy/loccy).
Once installed, Loccy auto-detects the i18n setup of any open project using react-i18next, next-intl, or vue-i18n.
Auto-detection is zero-config and covers most projects. Still, add a [`loccy.yaml`](/docs/config): it's where you set the AI [styleguide](/docs/config/styleguide), and it also lets you fine-tune or override detection when needed. If previews still don't show, see [Troubleshooting](/docs/extension/troubleshooting).
## Sign in
The [core features](/docs/extension/core-features) work without an account.
[AI features](/docs/extension/ai-features) require one: the first time you run an AI action, the extension opens your browser to authenticate.
---
# Core features
Everything on this page is free, runs locally, and requires no account. For AI actions like extract, translate, and refine, see [AI features](/docs/extension/ai-features).
## Inline translation previews
Loccy renders the resolved translation next to every keypath in your code, so you never have to open a JSON file to see what `t('reservation.title')` actually says.

### Customizing previews
Preview locale and color are personal preferences, a good fit for your global user `settings.json` (**Preferences: Open User Settings (JSON)** from the Command Palette). The preview defaults to your system locale, falling back to whichever of your project's locales is most widely spoken; `loccy.annotation` is the theme color (default `#797D8699`).
```json
{
"loccy.annotations.previewLocale": "en",
"workbench.colorCustomizations": {
"loccy.annotation": "#6A9955AA"
}
}
```
## Hover menu
Hover over any keypath in your code to open Loccy's hover menu. It shows all translations for the message and is the entry point for most quick actions.

### Edit in place
Click the pencil button next to a translation in the hover menu to edit it directly. The change is written back to the corresponding JSON translation file without leaving your component.
To edit every locale at once, click the three-dots button and choose **Edit all translations manually**. All locales open together for bulk editing, written back in one operation.
:demo-video{src="/extension-features/video/inline-editor.mp4"}
### Usage map
The hover menu always displays every place in the project where the keypath is used. Use it to check whether a message is shared before changing its wording, and to jump straight to any usage.
:demo-video{src="/extension-features/video/usages-navigation.mp4"}
A popular bonus use: find a place in code by text. Paste a string copied from the rendered page into VS Code's search, then hover the matching value in the JSON file. Loccy shows every place it's used.
:demo-video{src="/extension-features/video/find-place-in-code-by-text.mp4"}
::info
In JSON translation files, Loccy renders hover annotations only in a small window around the cursor (for performance). If one isn't showing, click the target string first to focus the cursor there.
::
### Global keypath rename
Click the three-dots button in the hover menu and choose rename. Loccy renames the keypath everywhere, in all translation files and all code usages, in one operation.
:demo-video{src="/extension-features/video/rename-key.mp4"}
## Insert existing message
Reuse a translation that already exists instead of creating a duplicate. Place your cursor where the keypath should go and run `Loccy: Insert Existing Message...` from the Command Palette, then pick the message to insert.
:demo-video{src="/extension-features/video/use-existing.mp4"}
## Missing-translation warnings
When a keypath has at least one empty or missing translation, the inline preview shows a warning listing the affected locales.

The warning covers the same locales [`noUntranslatedKeys`](/docs/config#translations) lints: every detected locale by default, or exactly the ones that rule names. Scope it there to stop warning about a locale that is incomplete on purpose.
Plus, two editor settings available to control this missing-translation warnings:
| Setting | Default | Description |
| :--- | :--- | :--- |
| `loccy.annotations.showMissingTranslationsWarning` | `true` | Show a warning in the preview when at least one translation is missing for a key |
| `loccy.annotations.minMissingLocalesToDisplay` | `3` | Guarantees this minimum number of locales in the missing-translations list; more may be shown based on available space. Set `0` to show only the warning |
---
# AI features
Beyond the free [core features](/docs/extension/core-features), Loccy adds AI workflows for extracting, translating, and refining copy, available directly in the editor, right where you work.
AI features require signing in. New accounts get free requests to start.
All AI features follow the rules in your [`loccy.yaml` styleguide](/docs/config/styleguide) (glossary, voice, per-locale conventions, keypath naming), so results match your project's voice without repeating instructions in every request.
## Extract and Translate
Turns static text into a message in one step: generates the keypath and translations for all project locales.
1. Place your cursor inside the static text.
2. Run `Loccy: Extract and Translate` from the Command Palette (also available in the editor context menu).
No selection needed: other extensions make you select the full text, Loccy resolves the boundaries from a cursor sitting anywhere inside the string. A small detail, and a lot of work went into getting it right.
:demo-video{src="/extension-features/video/extract-and-translate.mp4"}
::info
**Tip:** for a locale that should mostly mirror another (a regional variant like `en-GB`/`en-US`, or any other pairing), give it an `{ extends, style }` entry in [`styleguide.localeRules`](/docs/config/styleguide). Across every AI action, not just extraction, Loccy leaves it empty unless an override is needed from the base locale.
::
## Suggest Contextual Translation
Creates a message based on the surrounding code. Loccy writes the copy for you when you know a spot needs text but haven't drafted it yet. Place your cursor in the desired location and run `Loccy: Suggest Contextual Translation`.
:demo-video{src="/extension-features/video/insert-contextual.mp4"}
## Edit one translation, sync the rest
When you edit a translation from the hover menu, Loccy offers to update all other locales to match. Adjust the copy once in your native language and keep every locale consistent.
:demo-video{src="/extension-features/video/sync-others.mp4"}
## Fill empty translations
Auto-fills all empty translations in a message. Click the 3 dots in the hover menu; the option appears when at least one translation is empty and at least one is filled.
:demo-video{src="/extension-features/video/fill-missing.mp4"}
## Adjust with a prompt
Rewrite text with a free-form instruction:
- All translations in a message: click the 3 dots in the hover menu.
- A single translation: click the pencil button in the hover menu, then choose "Adjust with prompt...".
:demo-video{src="/extension-features/video/prompt.mp4"}
---
# Troubleshooting
When Loccy is set up correctly, it renders each translation inline next to its key, so you read the actual copy, not `t('reservation.title')`:

If yours doesn't look like this, find your symptom below.
## First: Loccy expects a standard setup
Loccy works by **static analysis**: it reads your JSON translation files and your source code as written, without running your app. When your i18n setup follows the framework's official docs, everything is detected automatically. Almost every issue reported in the community comes from a setup that diverges from it:
- **Translations in JS/TS, not JSON.** Loccy reads JSON files only.
- **Translations assembled at runtime.** Loccy only sees what's on disk, not files merged, namespaced, or reshaped in JS/TS.
- **next-intl treated like react-i18next.** next-intl has no namespaces: the argument to `useTranslations` is only a key prefix, and messages stay one file per locale.
- **The t-function passed in from another scope.** Loccy misses the namespace/prefix bound to it, so keys look unresolved or missing.
The closer you stay to the documented setup, the less configuration Loccy needs. The two symptoms below expand on these causes.
## No previews shown
No inline text appears next to your keys at all:

Loccy shows previews only once it has found both your translation files and the calls that use them in your code.
### Are your translation files detected?
Check these in `loccy.yaml` ([how to create one](/docs/config#generating-the-config)):
| Key | What to verify |
| --- | --- |
| `translations.glob` | Glob must actually match your files. Loccy reads **JSON only**. |
| `translations.layout` | Pattern must match your layout: `{locale}.json` for per-locale files, `{locale}/{namespace}.json` for per-locale folders. |
| `framework` | Must match your i18n library: `react-i18next`, `next-intl`, `vue-i18n`, or `custom`. |
```yaml
framework: vue-i18n
translations:
glob: src/locales/**/*.json
layout: "{locale}.json"
```
### Are your translation calls detected?
If files are detected but keys still render bare, Loccy isn't matching the `t()` calls in your code:
| Setting | Default | Fix |
| --- | --- | --- |
| `usages.customTFunctions` | `[]` | Add your wrapper's t-function names if you don't call the framework's built-in t-function (usually `t` / `$t`) directly. |
| `usages.detectKeysInStrings` | `true` | Recognizes keys passed as bare strings (when a matching translation exists). Leave on unless you have many key-shaped non-translation strings. |
| `usages.include` | framework default | Must cover the files with your calls. Git-ignored files are skipped automatically. |
### Is only the hover menu missing?
Inline previews show, but hovering a key opens nothing. The hover menu uses VS Code's hover system. Re-enable it:
```json
{ "editor.hover.enabled": true }
```
## Missing translations shown
Previews render, but with a ⚠ warning listing locales that are missing the key:

### The t-function is passed in from an outer scope
Loccy computes a keypath from the `t` binding in the same file. If `t` arrives with a namespace/prefix already bound (passed in as a prop, or from a parent), Loccy resolves the bare keypath **without** that prefix, doesn't find it, and reports the key as missing:
```tsx
// ❌ t is passed in: its "reservation" namespace lives only in the type,
// so Loccy looks up 'guestName' (not 'reservation.guestName') and reports it missing
function GuestNameField({ t }: { t: TFunction<'reservation'> }) {
return
}
// ✅ create the binding where the keys are used
function GuestNameField() {
const { t } = useTranslation('reservation')
return
}
```
## Getting help
- **Community**: ask anything in our [Discord](https://discord.gg/btztGrejXU).
- **Pro subscribers**: reach the developer directly through the support form in the [Loccy app](https://loccy.dev/login).
---
# Get started
A free, offline command-line linter that catches i18n problems before they merge: unused keys, missing translations, and unsorted translation files. Run it locally or wire it into CI to fail the build when locales drift.
It reads the same [`loccy.yaml`](/docs/config) as the [extension](/docs/extension): configure once, use everywhere.
## Install
Requirements: Node.js 18 or later.
```bash
npm i -D @loccy-dev/lint
```
## Set up loccy.yaml
Loccy needs a `loccy.yaml` in your repo root: commit it once, then the linter and the IDE extension both read it.
**Fastest: let your coding agent do it.** This prints a step-by-step setup prompt: it has your agent run `init`, read the `lint` output, and resolve false positives (dynamic keys, deliberately incomplete locales) on its own:
```bash
npx loccy init-prompt | pbcopy
```
Then paste into your coding agent.
**Or do it yourself:**
```bash
npx loccy init
```
## Lint
```bash
npx loccy lint
```
See [Command reference](/docs/linter/commands) for every command, flag, and exit code.
---
# Command reference
The linter installs as the npm package [`@loccy-dev/lint`](https://www.npmjs.com/package/@loccy-dev/lint) and runs as the `loccy` command. It has three subcommands:
```bash
npx loccy init # scaffold loccy.yaml
npx loccy lint # lint translations and usages
npx loccy format # sort translation files
```
Run `npx loccy --help` or `npx loccy --help` for inline help, `npx loccy --version` to print the version. See [Get started](/docs/linter) for installation and setup.
## loccy init
Detects your project setup and generates [`loccy.yaml`](/docs/config) in the repo root: project configuration auto-detected from your codebase, plus commented styleguide and glossary examples for AI translations.
```bash
npx loccy init
```
This is the CLI equivalent of the IDE command "Loccy: Create Config File". Both generate the same file. See [`loccy.yaml`](/docs/config) for all options, and [Styleguide](/docs/config/styleguide) for the AI guidance half.
## loccy lint
Checks your translation files and key usage. With `--fix`, removes unused keys (the only auto-fixable issue). Sorting is a separate command; see [`loccy format`](#loccy-format).
```bash
npx loccy lint
npx loccy lint --fix
```
### Checks
Lint runs up to two check groups, controlled by the lint rules on each module's [`usages`](/docs/config#usages) and [`translations`](/docs/config#translations):
| Check group | Rule | Lives on | What it finds | Fixable with `--fix` |
| --- | --- | --- | --- | --- |
| Missing translations | `noUntranslatedKeys` | `translations` | Keys with missing or empty values in checked locales | No |
| Usage | `noUnusedKeys` | `usages` | Keys defined in translations but never used in code | Yes, removes the keys from all translation files |
| Usage | `noUnresolvedKeys` | `usages` | Keys used in code but missing from translations | No |
#### Missing translations
Checks every key across locales. `noUntranslatedKeys: true` checks every locale detected in your translation files. Set it to a list of locale codes to check exactly those instead, per module:
```yaml
modules:
default:
translations:
noUntranslatedKeys: ['en', 'de']
```
Use the list for locales that are deliberately incomplete: a language still being filled in, or a regional variant that inherits most of its keys from another locale at runtime. `false` turns the check off; an empty list does the same. Each reported key has one of three kinds:
| Kind | Meaning |
| --- | --- |
| `missing` | Key is absent in one or more checked locales |
| `empty` | Key exists but its value is blank in one or more checked locales |
| `incomplete` | Both: absent in some locales and blank in others |
For every reported key, the value (or its absence) is printed per locale. These issues require a manual fix, or use the IDE extension's [Fill empty translations](/docs/extension/ai-features#fill-empty-translations) to fill the gaps.
#### Usage
Runs only when usage scanning is configured (`usages` in the config). It scans your code for translation key usages and reports:
- `unused`: keys present in translation files but never referenced in code. Fixable: `--fix` removes them from all translation files, cleaning up empty parent objects.
- `missing`: keys referenced in code but absent from translation files, with the `file:line` locations where they are used. Not fixable automatically.
Both rules support key exclusion patterns in the config. See [loccy.yaml](/docs/config).
### Sample output
```terminal
✔ Loaded loccy.yaml (framework: react-i18next)
✔ Extracted 482 translations
Missing translations
missing common:settings.title locales/en/common.json
de (missing)
en Settings
empty common:profile.bio locales/en/common.json
de (empty)
en Tell us about yourself
Usage
unused legacy.banner
missing nav.reports src/components/Nav.tsx:42
1 fixable issue, run loccy lint --fix
3 issues require manual fix
```
### Exit code
Lint exits with code 1 when any issues remain after the run (including fixable ones on a run without `--fix`), so it fails any CI pipeline that runs it as a merge gate.
### Debugging
Set `LOCCY_LINT_DEBUG=1` to print extra diagnostics, such as which locales were detected in translation files and which are being checked by the missing-translations rule:
```bash
LOCCY_LINT_DEBUG=1 npx loccy lint
```
## loccy format
Deeply sorts keys in the translation files of every module that declares [`translations.sortKeys: true`](/docs/config#translations), rewriting each file sorted while preserving its indentation and trailing newline. Modules without `sortKeys` are left untouched; if no module opts in, `format` is a no-op.
```bash
npx loccy format # sort files in place
npx loccy format --check # report unsorted files and exit 1, writing nothing (for CI)
```
Sorting is deliberately split from `loccy lint`: lint reports problems and `--fix` only removes unused keys, while `format` is the one command that reorders keys: run it on demand or in a pre-commit hook. In CI, use `--check`: it verifies sort order and exits 1 if any file needs sorting, without rewriting anything. The IDE enforces the same `sortKeys` invariant automatically on every write. Like the other commands, `format` needs a `loccy.yaml` and accepts `--config `.
---
# Config
`loccy.yaml` at your repo root is the single source of truth for both the [IDE extension](/docs/extension) and the [linter](/docs/linter). It has two halves: the mechanical setup on this page, organized as one or more **modules** (each pairs code `usages` with stored `translations`), plus the hand-authored [`styleguide`](/docs/config/styleguide) that steers every AI translation.
An empty file is valid — it means "detect everything." That only covers the mechanical half, though: the `styleguide` used by [AI actions](/docs/extension/ai-features) cannot be auto-detected.
## Generating the config
Run **Loccy: Create Config File** (IDE Command Palette) or [`npx loccy init`](/docs/linter/commands#loccy-init). Both write a single `loccy.yaml` with the detected mechanical config, plus a commented-out styleguide scaffold; edit the written values to pin or change what Loccy inferred.
The written values come from inspecting your repo:
| Key | Detected from |
| --- | --- |
| `framework` | Dependencies: `react-i18next`, `next-intl`, `vue-i18n`, or others |
| `translations.glob` | The most likely translation directory |
| `translations.layout` | Whether files are per-locale (`{locale}.json`) or per-namespace (`{locale}/{namespace}.json`) |
| `usages.include` | A glob matching your code (e.g. `.vue`, `.tsx`) |
If no supported i18n library or translation files are found, a placeholder is written; set `framework` and `translations.glob` by hand.
To verify, open a file with translation keys and check the inline previews (IDE), or run [`npx loccy lint`](/docs/linter/commands#loccy-lint).
The generated file starts with a `# yaml-language-server: $schema=...` comment. Keep it: editors with the YAML language server use it for autocomplete, inline docs, and validation.
## Full example
Nothing is required: every key below is an override of an auto-detected value. This is a single-module setup, with one entry under `modules`, conventionally named `default`; see [Modules](#modules) for the multi-module form. The [`styleguide`](/docs/config/styleguide) sits alongside `modules`, at the top level.
```yaml
modules:
default:
framework: react-i18next
translations:
glob: 'public/locales/**/*.json'
exclude: ['public/locales/en-US/**.json']
layout: '{locale}/{namespace}.json'
# storage-side lint rules
noUntranslatedKeys: true
sortKeys: true
usages:
include: ['src/**/*.{js,ts,jsx,tsx}']
exclude: ['src/**/*.test.tsx']
customTFunctions: ['translate']
detectKeysInStrings: true
defaultNamespace: translation
# usage-side lint rules
noUnresolvedKeys:
enabled: true
excludeKeys:
- 'Common.Language.*' # dynamic $t(`Common.Language.${code}`)
noUnusedKeys: true
styleguide:
# see /docs/config/styleguide
```
## Modules
A **module** is one i18n setup: how keys are used in code (`usages`) paired with how they're stored (`translations`). Splitting the two axes lets a module scan usages one way and store translations another: e.g. a custom t-function on the usage side with a non-JSON storage format on the storage side.
Every repo defines `modules` explicitly, even a single-setup repo, as one entry conventionally named `default`:
```yaml
modules:
default:
framework: react-i18next
translations:
glob: 'public/locales/**/*.json'
layout: '{locale}/{namespace}.json'
usages:
include: ['src/**/*.{ts,tsx}']
```
Multi-setup repos add more entries to the same `modules` map, keyed by module name. Each entry takes the same `framework`/`translations`/`usages` keys:
```yaml
modules:
frontend:
framework: vue-i18n
translations:
glob: 'apps/web/src/locales/**/*.json'
layout: '{locale}/{namespace}.json'
usages:
include: ['apps/web/**/*.{vue,ts}']
backend:
# Decoupled setup: no framework preset, so usages come only from a custom
# wrapper, and translations are stored as YAML rather than JSON (the
# `.yaml` extension on `glob` is what picks the resource format).
translations:
glob: 'services/api/locales/*.yaml'
layout: '{locale}.yaml'
usages:
include: ['services/api/**/*.ts']
customTFunctions: ['i18n.translate']
detectKeysInStrings: false
noUnusedKeys: false # opt out for this module only
```
The global key, [`styleguide`](/docs/config/styleguide), always sits at the top level, alongside `modules`. Lint rules aren't global either — each lives on the [`translations`](#translations)/[`usages`](#usages) axis it checks.
**Module selection.** Multi-module setups are fully supported in the IDE: each file resolves to its module automatically, by matching `translations.glob`/`usages.include`. [`loccy lint`](/docs/linter/commands#loccy-lint) and [`loccy format`](/docs/linter/commands#loccy-format) iterate every module — there's no way yet to target just one.
The next three sections document a module's fields, which live under each `modules.` entry: [`framework`](#framework), [`translations`](#translations), [`usages`](#usages).
## `framework`
```yaml
framework: react-i18next
```
The usage-detection preset (how keys are read from source code). One of `react-i18next`, `next-intl`, `vue-i18n`, or `custom`: no preset, scans only `t(...)`, any `usages.customTFunctions`, and bare-string matches. Defaults to `custom` when omitted; auto-detected from dependencies for the `default` module.
`framework` also seeds the default for [`messageFormat`](#messageformat) but is otherwise decoupled from storage — any framework can pair with any storage format (see [`translations`](#translations)).
## `translations`
```yaml
translations:
glob: 'public/locales/**/*.json'
exclude: ['public/locales/en-US/**.json']
layout: '{locale}/{namespace}.json'
```
The storage side: where translation files live and how paths map to locales and namespaces.
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `glob` | string | auto-detected | Single glob for this module's translation files |
| `layout` | string \| map | auto-detected | Pattern mapping paths to locales and namespaces (see [Layout patterns](#layout-patterns)) |
| `messageFormat` | string | auto-resolved | Plural encoding (see [`messageFormat`](#messageformat)) |
| `exclude` | string[] | `[]` | Globs to exclude from `glob` |
| `noUntranslatedKeys` | boolean \| string[] | `true` | Lint: every locale has a non-empty translation. `true` checks every detected locale; a list of locale codes checks exactly those, for locales that are deliberately incomplete |
| `sortKeys` | boolean | `false` | Lint: keep keys deeply sorted — the IDE enforces it on every write, [`loccy format`](/docs/linter/commands#loccy-format) on demand |
The resource file format (`json`, `yaml`, `ts-object`, `php-array`, `properties`) is not a config field: it's always derived from `glob`'s file extension.
Key-naming and code-organization guidance for the AI lives in [`styleguide.keys`](/docs/config/styleguide), not here.
### Layout patterns
Tells Loccy which path segments are the locale and namespace. The static prefix of `glob` is stripped, then the remainder is matched against the pattern: with `glob: 'src/locales/**/*.json'`, `src/locales/en/common.json` matches as `en/common.json`.
| Placeholder | Matches |
| --- | --- |
| `{locale}` | A locale code (`en`, `de`, `en-US`, `zh-Hans`) |
| `{namespace}` | A single path segment |
Within a segment, `*` matches any run of non-slash characters. Files that don't match fall back to treating the filename as the locale.
| Layout | `layout` |
| --- | --- |
| `locales/en.json` (one file per locale) | `{locale}.json` |
| `public/locales/en/common.json` (file per namespace) | `{locale}/{namespace}.json` |
For irregular conventions, such as a suffix-less default file, `layout` may instead be a **per-locale map** keyed by locale code, with `*` as the fallback:
```yaml
translations:
glob: 'src/i18n/**/*.properties'
layout:
en: 'messages.properties'
'*': 'messages_{locale}.properties'
```
### Nested vs flat keys
Both are supported and detected per file: nested when any top-level value is an object, flat when all are primitives. Loccy preserves the detected structure, indentation, and trailing newlines when writing.
```json
{ "settings": { "title": "Settings" } }
```
```json
{ "settings.title": "Settings" }
```
### `messageFormat`
```yaml
translations:
messageFormat: icu
```
How plurals are encoded (`translations.messageFormat`). **Auto-resolved** from `framework` and your dependencies (e.g. `icu` when `i18next-icu` is installed); set it only to override.
| Format | Plural shape | Typical setup |
| --- | --- | --- |
| `suffix-cldr` | Sibling keys `items_one`, `items_other`, … | i18next (default) |
| `icu` | Inline `{count, plural, one {…} other {…}}` | next-intl; i18next with `i18next-icu` |
| `vue-pipe` | Pipe segments `no items \| one item \| {count} items` | vue-i18n |
Loccy uses the message format to expand plural key usages (for key-based formats) and to derive the resource storage shape. AI-generated plurals and the lint plural-completeness check are both temporarily off while plural-arity detection is reworked; `messageFormat` today only drives usage-key expansion and resource shape.
## `usages`
```yaml
usages:
include: ['src/**/*.{js,ts,jsx,tsx}']
exclude: ['src/**/*.test.tsx']
customTFunctions: ['translate']
detectKeysInStrings: true
defaultNamespace: translation
```
The usage side: how source code is scanned for key usages.
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `include` | string[] | auto-detected | Globs for source files to scan |
| `exclude` | string[] | `[]` | Globs to exclude |
| `customTFunctions` | string[] | `[]` | Extra t-function names beyond framework defaults |
| `detectKeysInStrings` | boolean | `true` | Match bare string literals against known keys |
| `quoteType` | `'single'` \| `'double'` | auto | Quote style for inserted t-function calls. Omit to auto-detect from the codebase |
| `defaultNamespace` | string | none | Default t-function namespace. `react-i18next` only |
| `noUnresolvedKeys` | boolean \| object | `true` | Lint: keys used in code exist in translations. Object form: `{ enabled, excludeKeys }` |
| `noUnusedKeys` | boolean | `true` | Lint: flag unused keys — removed by `loccy lint --fix` |
Both only count keys that resolve to a static string: runtime keys like `` t(`status.${state}`) `` are skipped, not flagged, but the translation keys they cover (`status.active`, `status.done`, …) look unreferenced. Exclude `noUnresolvedKeys` false positives with `excludeKeys` (patterns are `namespace:keypath`, or bare `keypath` with no namespace; `*` globs match a key or `prefix.` segment). For `noUnusedKeys`, whitelist the construction site instead with a `// loccy-used-keys: status.*` comment — config can't reach into source, and the comment goes away with the code it annotates.
---
# Styleguide
The `styleguide` section of [`loccy.yaml`](/docs/config) holds the hand-authored rules every AI translation is written against. It sits at the top level, alongside `modules`, and is sent as context with each AI action. Every field is optional.
## Full example
Every field, and every shape a field's value can take, appears once here.
```yaml
styleguide:
product: |
Whisker Café: staff app for a real cat café.
Used by baristas mid-shift, on a phone, one hand free.
voice: |
Warm, lightly cheeky, cat-first. Address the user informally.
No marketing filler, no fake urgency. Exclamation marks only for genuine surprise.
mechanics: |
Buttons and menu labels max ~25 characters.
No emoji.
localeRules:
en: |
Sentence case for headings and buttons ("Book now" instead of "Book Now").
Contractions are fine.
de: |
Avoid anglicisms when a natural German word exists.
German runs long: compress rather than truncate.
de-CH:
extends: de
style: |
Replace ß with ss (schliessen).
Use Swiss guillemets «…».
doNotTranslate:
- term: Whisker Café
definition: Café brand name
caseSensitive: true
- term: Mister Mittens
glossary:
- definition: A cat that lives at the café
terms:
en: Resident
de: Bewohner
- definition: One staff member's working block, opening to closing handover
terms:
en: Shift
de: Schicht
- definition: A booked seating slot (the booking itself, not the act of reserving)
terms:
en: Reservation
de: Reservierung
de-CH:
preferred: Reservation
deprecated: [Buchung]
keys: |
Group keys by feature, dot-separated ("checkout.button.submit").
```
`npx loccy init` and **Loccy: Create Config File** write exactly this block, commented out, as a scaffold to adapt.
## Fields
| Field | What it describes |
| --- | --- |
| `product` | What the product is and who uses it |
| `voice` | How the product speaks: personality, register, how the user is addressed |
| `mechanics` | Formatting that holds in every locale: casing, punctuation, length limits, markup |
| `localeRules` | What each language decides for itself, keyed by locale code |
| `doNotTranslate` | Terms that stay verbatim everywhere: brand names, plan names |
| `glossary` | Terms with one approved translation per locale |
| `keys` | How keypaths are named and organized |