TypeScript plugins

Write plugins and presets in TypeScript with definePlugin and definePreset, with typed options.

A TypeScript plugin is an object with a name and the capabilities it adds. definePlugin and definePreset from x18n give it types. They return their argument unchanged.

A check

This plugin adds a check that reports messages that still contain TODO:

x18n/no-todo.ts
import { definePlugin } from '@x18n/core'

export default definePlugin({
  name: 'no-todo',
  checks: [{
    id: 'no-todo',
    defaultSeverity: 'warning',
    description: 'A message that still contains TODO.',
    * run(ctx) {
      for (const message of ctx.messages()) {
        if (typeof message.value === 'string' && message.value.includes('TODO'))
          yield { key: message.key, locale: message.locale, message: `"${message.key}" in ${message.locale} contains TODO.` }
      }
    },
  }],
})
x18n.config.ts
import { defineConfig } from '@x18n/core'
import noTodo from './x18n/no-todo'

export default defineConfig({
  plugins: [noTodo],
  checks: {
    'no-todo': 'error',
  },
})

A check is a generator. Each value it yields becomes a diagnostic. Without a location, the diagnostic points at the message of key in locale. The check id is also the diagnostic code, so x18n check --only no-todo and checks: { 'no-todo': 'off' } work as for the built-in checks.

Options

Pass a function to definePlugin to take options. The function returns the plugin:

x18n/no-word.ts
import { definePlugin } from '@x18n/core'

export default definePlugin((options: { word: string }) => ({
  name: 'no-word',
  checks: [{
    id: 'no-word',
    defaultSeverity: 'warning',
    * run(ctx) {
      for (const message of ctx.messages()) {
        if (typeof message.value === 'string' && message.value.includes(options.word))
          yield { key: message.key, locale: message.locale, message: `"${message.key}" contains ${options.word}.` }
      }
    },
  }],
}))

In a TS config, call the function: plugins: [noWord({ word: 'FIXME' })]. In a YAML or JSON config, pass the options as the second item of a tuple. x18n imports the module and calls its default export with them:

x18n.config.yml
plugins:
  - [./x18n/no-word.ts, {word: FIXME}]

A preset

A preset groups plugins and decides with detect when they apply. Use one when your package supports a library that x18n can detect:

x18n/acme.ts
import { definePlugin, definePreset } from '@x18n/core'

const calls = definePlugin({
  name: 'acme-calls',
  config(config) {
    config.usage = {
      ...config.usage,
      calls: [...config.usage.calls, { callee: ['translate', 'i18n.translate'] }],
    }
  },
})

export default definePreset({
  name: 'acme-i18n',
  detect: ctx => ctx.hasDependency('@acme/i18n'),
  plugins: [calls],
})

The config hook changes a draft of the config before it is final. Presets use it to set defaults: the config of the project wins over them. detect receives the dependencies of the project and can test files with ctx.fileExists(glob).

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

Typed config keys

A plugin can add types for its check ids and its options through declaration merging. defineConfig then completes them:

x18n/acme.ts
declare module '@x18n/core' {
  interface CheckIds {
    'no-todo': true
  }
  interface PluginConfigs {
    acme: { strict?: boolean }
  }
}

Users then set the options under pluginConfigs, and the plugin reads them in configResolved:

x18n.config.ts
import { defineConfig } from '@x18n/core'

export default defineConfig({
  pluginConfigs: {
    acme: { strict: true },
  },
})

Types

x18n exports defineConfig, definePlugin, definePreset, and the types Plugin, Preset, Check, Format, LocaleFinder, Scanner, Reporter and UserConfig. The full plugin API, with every context type, is in @x18n/core. Capabilities describes each part.