Docs navigation

Config

loccy.yaml at your repo root is the single source of truth for both the IDE extension and the 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 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 cannot be auto-detected.

#Generating the config

Run Loccy: Create Config File (IDE Command Palette) or npx 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:

KeyDetected from
frameworkDependencies: react-i18next, next-intl, vue-i18n, or others
translations.globThe most likely translation directory
translations.layoutWhether files are per-locale ({locale}.json) or per-namespace ({locale}/{namespace}.json)
usages.includeA 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.

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 for the multi-module form. The styleguide sits alongside modules, at the top level.

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:

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:

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, always sits at the top level, alongside modules. Lint rules aren't global either — each lives on the translations/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 and 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.<name> entry: framework, translations, usages.

#framework

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 but is otherwise decoupled from storage — any framework can pair with any storage format (see translations).

#translations

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.

KeyTypeDefaultDescription
globstringauto-detectedSingle glob for this module's translation files
layoutstring | mapauto-detectedPattern mapping paths to locales and namespaces (see Layout patterns)
messageFormatstringauto-resolvedPlural encoding (see messageFormat)
excludestring[]Globs to exclude from glob
noUntranslatedKeysboolean | stringtrueLint: 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
sortKeysbooleanfalseLint: keep keys deeply sorted — the IDE enforces it on every write, 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, 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.

PlaceholderMatches
{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.

Layoutlayout
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:

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.

{ "settings": { "title": "Settings" } }
{ "settings.title": "Settings" }

#messageFormat

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.

FormatPlural shapeTypical setup
suffix-cldrSibling keys items_one, items_other, …i18next (default)
icuInline {count, plural, one {…} other {…}}next-intl; i18next with i18next-icu
vue-pipePipe segments no items | one item | {count} itemsvue-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

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.

KeyTypeDefaultDescription
includestringauto-detectedGlobs for source files to scan
excludestring[]Globs to exclude
customTFunctionsstring[]Extra t-function names beyond framework defaults
detectKeysInStringsbooleantrueMatch bare string literals against known keys
quoteType'single' | 'double'autoQuote style for inserted t-function calls. Omit to auto-detect from the codebase
defaultNamespacestringnoneDefault t-function namespace. react-i18next only
noUnresolvedKeysboolean | objecttrueLint: keys used in code exist in translations. Object form: { enabled, excludeKeys }
noUnusedKeysbooleantrueLint: 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.