YAML plugins

Describe a library in a *.x18n-plugin.yml file - resources, translate calls, regex scanners, scopes, placeholders and templates.

A YAML plugin is a data file that holds the declarative part of the plugin API. Name it <name>.x18n-plugin.yml, and list its path in plugins:

x18n.config.yml
plugins:
  - ./x18n/acme.x18n-plugin.yml

A full example

This plugin supports a made-up library, @acme/i18n, with YAML locale files, a translate function, a useTranslate factory and Handlebars templates:

x18n/acme.x18n-plugin.yml
# yaml-language-server: $schema=https://x18n.antfu.dev/schema/plugin.json
name: acme-i18n
config:
  sourceLocale: en
  resources:
    - pattern: 'src/i18n/{locale}.yml'
scanners:
  calls:
    - callee: [translate, i18n.translate]
    - callee: useTranslate
      factory: true
      returns: [translate]
      keyPrefix: {arg: 0}
  regex:
    - extensions: [hbs]
      patterns:
        - '\{\{t "({key})"'
placeholders:
  - '%{name}'
derivedKeys:
  - '{key}.zero'
  - '{key}.other'
extract:
  templates:
    - template: "translate('{key}'{args})"
      kinds: [js-string, js-template]
      imports:
        - "import { translate } from '@acme/i18n'"

The first line points the YAML language server at the JSON Schema. With the Red Hat YAML extension, VS Code then validates the file and completes the keys. x18n schema plugin prints the same schema. A file that does not match the schema stops the load with X18N1004.

Sections

name

The name of the plugin. It is required and must be unique.

config

Defaults for the config of the project: sourceLocale, locales, resources, namespace and keyStyle. The config file of the project wins over them. resources applies only when the config sets none.

Scanners

scanners.calls lists translate calls for the AST scanners of JS, TS, JSX, Vue and Svelte files. Each entry has the shape of an item of usage.calls:

FieldMeaning
calleeA name or member path, or a list of them: t, i18n.t, intl.formatMessage.
keyWhich argument holds the key: an index (default 0), or { arg, property } for a property of an object argument.
namespaceWhere the namespace comes from: { arg, property }.
keyPrefixWhere a key prefix comes from: { arg, property }.
factoryThe callee returns translate functions, as useI18n() does.
returnsFor a factory: the names of the returned functions. Default [t].
typesType names that make a parameter a translate function, such as TFunction.
kindcall (default), selector for t($ => $.a.b), or accessor for a member chain on a root.

scanners.regex lists regex scanners for other file types. Each entry has extensions (without the dot) and patterns. A pattern is a regular expression whose first group is the key. Write {key} inside the group, as in ({key}): x18n replaces {key} with a pattern for key characters. This is the same format as usageMatchRegex of i18n-ally.

In YAML, write regular expressions in single quotes, so that backslashes stay as they are.

scopes

scopes.regex is a regular expression whose first group is a namespace. The code after each match, up to the next match, uses that namespace. This is the same as scopeRangeRegex of i18n-ally.

derivedKeys

Rules such as {key}_plural. When a key is used, the keys that a rule derives from it count as used too. A * matches any text: {key}_*.

normalizeKey

Rules that rewrite the raw key text of a usage before x18n looks it up. This rule turns t('pages/home/title') into the key pages.home.title:

normalizeKey:
  - replace:
      pattern: /
      with: .

pattern is a regular expression. Every match is replaced.

Placeholders

placeholders lists placeholder shapes. Each shape holds the word name where the placeholder name goes: {name}, {{name}}, %{name} or :name.

localeInfo

The display name and flag of locale codes, for the UI and the editor:

localeInfo:
  zh_Hant:
    displayName: Traditional Chinese
    flag: 🇹🇼

extract

extract.templates adds extraction templates. An entry is a template string, or an object with template, kinds, files and imports.

detect and exclusive

detect and exclusive turn the plugin into a preset:

detect:
  dependencies:
    any: ['@acme/i18n']
  files: ['src/i18n/*.yml']
exclusive: true

The preset applies when the project depends on any package of dependencies.any, on every package of dependencies.all, and has a file for each glob of files. With exclusive: true, x18n tests no preset after it.

x18n runs detect only for a preset inside the plugins of another preset. A preset that you list in plugins or presets always applies.

Move to TypeScript

x18n plugin eject prints the TypeScript version of a YAML plugin. It uses the same built-in functions, so the result behaves the same:

x18n plugin eject x18n/acme.x18n-plugin.yml > x18n/acme.ts

Then list ./x18n/acme.ts in plugins instead of the YAML file. See TypeScript plugins.

The schema

The plugin file has this shape:

/** A YAML plugin (`*.x18n-plugin.yml`): the declarative subset of the plugin API. */
export interface PluginFile {
  readonly $schema?: string | undefined
  readonly name: string
  readonly detect?: {
    readonly dependencies?: { readonly any?: readonly string[] | undefined, readonly all?: readonly string[] | undefined } | undefined
    readonly files?: readonly string[] | undefined
  } | undefined
  readonly exclusive?: boolean | undefined
  readonly config?: {
    readonly sourceLocale?: string | undefined
    readonly locales?: readonly string[] | undefined
    readonly resources?: readonly ResourceSpec[] | undefined
    readonly namespace?: NamespaceOptions | undefined
    readonly keyStyle?: 'auto' | 'nested' | 'flat' | undefined
  } | undefined
  readonly scanners?: {
    readonly calls?: readonly CallSpec[] | undefined
    readonly regex?: readonly RegexScannerSpec[] | undefined
  } | undefined
  readonly scopes?: {
    /** Regex whose first group is the namespace of the code after it (i18n-ally `scopeRangeRegex`). */
    readonly regex: string
  } | undefined
  readonly derivedKeys?: readonly string[] | undefined
  readonly normalizeKey?: readonly { readonly replace: { readonly pattern: string, readonly with: string } }[] | undefined
  /** Placeholder shapes with `name` as the name: `{name}`, `{{name}}`, `%{name}`. */
  readonly placeholders?: readonly string[] | undefined
  readonly localeInfo?: { readonly [code: string]: { readonly flag?: string | undefined, readonly displayName?: string | undefined } } | undefined
  readonly extract?: { readonly templates?: readonly (string | ExtractTemplate)[] | undefined } | undefined
}