Docs navigation

loccy.yaml

loccy.yaml at your repo root is the single source of truth for both the IDE extension and the linter. One file covers the mechanical setup, organized as one or more modules (each pairs code usages with stored translations), plus the hand-authored AI guidance (styleguide).

An empty file is valid — it means "detect everything." That only covers the mechanical setup, though: the styleguide used by AI actions in the extension 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; 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.

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:
  code: |
    Keys grouped by page, then component. camelCase segments.
  global: |
    Toasts and tooltips: no trailing period.
    Ellipsis is three dots (...), never the … character.
  locales:
    en: |
      No contractions. Sentence case for buttons and titles.
    en-US:
      extends: en
      style: American spelling (colour → color), otherwise inherit.
    de: |
      Informal "du", never "Sie".
  doNotTranslate:
    - term: Loccy
      caseSensitive: true
      definition: Product name, always verbatim including capitalization
  glossary:
    - definition: Where projects live
      terms:
        en: Workspace
        de: Arbeitsbereich
    - definition: The main overview screen after signing in
      terms:
        en: Dashboard
        de:
          preferred: Dashboard
          deprecated: [Armaturenbrett, Übersichtsseite]

#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
noUntranslatedKeysbooleantrueLint: every locale has a non-empty translation. Partial-override locales in styleguide.locales are skipped
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.code, 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.

#styleguide

Global. The styleguide section steers every AI translation Loccy produces: global and per-locale style, partial-override locales, keypath-naming guidance, plus the glossary and doNotTranslate terms. It's sent as context with each AI action (adding, translating, or refining a translation; see AI features) and read fresh from your repo every time.

Add one for consistent terminology, a specific tone of voice, or locale conventions (formality, casing, punctuation). Every key under styleguide is optional.

#code

styleguide:
  code: |
    Keys grouped by page, then component. camelCase segments.

Code-side i18n guidance: key naming, file organization, and coding conventions. Free-form markdown: the segment structure, casing, and any project conventions. Injected into Loccy's code-aware features (e.g. naming a keypath when extracting a string).

#global and locales

styleguide:
  global: |
    Toasts and tooltips: no trailing period.
    Ellipsis is three dots (...), never the … character.
  locales:
    en: |
      No contractions. Sentence case for buttons and titles.
    en-US:
      extends: en
      style: American spelling (colour → color, organise → organize); date format MM/DD/YYYY; currency $ not £.
    de: |
      Informal "du", never "Sie".

global is free-form markdown, briefed the way you'd brief a human translator: applies everywhere, keep each point short and concrete. locales is keyed by locale code, and each entry is one of two shapes:

  • plain string → per-locale style prose (language-specific rules, same spirit as global)
  • { extends, style? } → a partial-override locale: holds only what differs from extends, inheriting the rest at runtime. extends can pair any two locales (e.g. nb extending da), not just a regional variant of the same code (en-USen). style is a plain-language diff — write only what changes, not a full styleguide.

Every AI action treats it specially: it writes a key only where style calls for a deviation, using text already present in extends — never invented, never blanket-filled. Everything else stays empty and inherits extends at runtime. noUntranslatedKeys lint rule skips its empty keys too.

FieldTypeDescription
locales[locale]string | { extends, style? }Per-locale prose, or a partial-override entry naming the locale it extends and its diff-style deviation rule

#glossary

styleguide:
  glossary:
    - definition: Where projects live      # simplest: one string per locale
      terms:
        en: Workspace
        de: Arbeitsbereich
    - definition: The main overview screen after signing in
      terms:
        en: Dashboard
        de:
          preferred: Dashboard              # object form adds a deprecated list
          deprecated: [Armaturenbrett, Übersichtsseite]

A list of terms with approved per-locale translations, for consistency. Each entry carries a definition (what the term means and when it applies) and terms: a locale code mapping to the preferred rendering for that locale.

FieldTypeRequiredDescription
definitionstringyesWhat the term means, when it applies
terms{ [locale]: string | { preferred, deprecated? } }yesRendering per locale

A locale's value is a plain string (the preferred form), or an object: preferred (required, same as the plain-string form), deprecated (string; alternatives that must never appear, always flagged).

#doNotTranslate

styleguide:
  doNotTranslate:
    - term: Loccy
      caseSensitive: true
      definition: Product name, always verbatim including capitalization
    - term: Pro
      caseSensitive: true
      definition: Paid plan name, as shown on the pricing page

A list of terms whose literal form must survive in every locale: brand names, plan names, established technical terms.

FieldTypeRequiredDescription
termstringyesLiteral text to preserve
caseSensitivebooleannoRespect casing
definitionstringnoWhy it's preserved (spelling detail, usage note)