Skip to content
Langnostic
Esc
↑↓navigate↵open⌘Jpreview
On this page

Plugin API

Implement format plugins with typed file metadata, translation arguments, lifecycle hooks, and results.

Implement LangnosticPlugin to support another file format. A plugin discovers pending strings, supplies format-specific instructions, and writes translated content. The runner owns AI requests, response validation, and concurrency.

For official plugin setup, see Using plugins. For the full translation pipeline, see Architecture.

LangnosticPlugin

Import the type from langnostic and return a new instance for each translation group.

Member Required Type or return value
type Yes Non-empty string identifying the format, such as 'plain-text'.
stringsPerRequest Yes Positive integer used when the group has no batch-size override.
trackFiles Yes Promise<void>
getTranslationStrings Yes Promise resolving to pending strings.
onTranslationStart No Promise<void>
getInstructions No Promise<string>
onTranslationBatchComplete Yes Promise<void>

All hooks are asynchronous. Keep the readonly metadata and result arrays unchanged; store plugin state separately.

Data types

The package exports these types for plugin authors:

Type Shape and purpose
TranslationLocale A locale string, such as 'zh-CN'.
TranslationFileItem Readonly { locale, path } for one source or target file.
TranslationFileMetadata Readonly { source, targets }, where source is a file item and targets is a readonly array of file items.
TranslationStringArg Readonly { batchId, id, source, targets } describing one pending string.

Derive hook parameter and result types directly from the exported contract:

import type { LangnosticPlugin } from 'langnostic';

export type TrackedFiles = Parameters<LangnosticPlugin['trackFiles']>[0];
export type PendingStrings = Awaited<
  ReturnType<LangnosticPlugin['getTranslationStrings']>
>;
export type CompletedStrings = Parameters<
  LangnosticPlugin['onTranslationBatchComplete']
>[0];

Pending strings

TranslationStringArg.source contains locale, string, and an optional description providing translator context. Its targets array contains locale strings for only the translations needed:

import type { TranslationStringArg } from 'langnostic';

export const pending = {
  batchId: 'messages/en-US.json',
  id: 'welcome',
  source: {
    locale: 'en-US',
    string: 'Welcome, {name}!',
    description: 'Greeting shown after sign-in',
  },
  targets: ['zh-CN'],
} satisfies TranslationStringArg;

Use a stable batchId for strings that should be written together. The official plugins use the source file path. Give each string a unique id within its batch and request each target locale once.

Completed strings

The completion hook receives the original batchId, id, and source, with targets replaced by readonly { locale, string } objects:

{
  "batchId": "messages/en-US.json",
  "id": "welcome",
  "source": {
    "locale": "en-US",
    "string": "Welcome, {name}!",
    "description": "Greeting shown after sign-in"
  },
  "targets": [
    {
      "locale": "zh-CN",
      "string": "欢迎,{name}!"
    }
  ]
}

The AI response uses a separate translations field. The runner validates that response and converts it to the plugin result shape before invoking your hook.

Lifecycle

trackFiles(filesMetadata)

Called once for each group after source discovery and destination validation. Its argument is ReadonlyArray<TranslationFileMetadata>. Store the resolved paths and locales for subsequent hooks, including when discovery returns no files.

getTranslationStrings()

Called after file tracking on both real and dry runs. Read source, target, and tracking files to return the pending TranslationStringArg objects. Return an empty array when nothing needs translation. The return type is Promise<ReadonlyArray<TranslationStringArg>>.

Leave files unchanged. Plan removal or reordering maintenance in memory for onTranslationStart(). The runner groups the returned strings by batchId and splits each batch into requests using the configured stringsPerRequest.

onTranslationStart()

Optional hook called after discovery on real runs, before any AI requests. Apply queued maintenance here. It runs even when no strings need translation and is skipped during dry runs.

getInstructions()

Optional hook called before each AI request. Return format-specific translation instructions, such as how to handle ICU placeholders or Markdown syntax. When omitted, the runner uses an empty plugin instruction string.

Instructions do not guarantee syntax preservation. Validate your format as part of writing or in the project’s review workflow.

onTranslationBatchComplete(translatedStrings)

Called once after every request in a batch has completed successfully. Its argument contains all translated strings in that batch, including strings split across multiple requests. Map results by id and locale; request completion order can differ from discovery order.

Merge the results with existing content, write the target files, and update any tracking data. The runner does not call this hook for a batch with a failed request, or for an empty batch. Dry runs skip it.

Requests run under the global concurrency limit, and hooks for different batches can overlap. Keep batch state independent and avoid writing the same file from multiple batches.

Errors and file writes

A rejected discovery or maintenance hook fails the command. A rejected instruction or completion hook marks that batch as failed; independent batches can still complete. The command exits with status 1 when a batch fails.

Completed file writes are not rolled back. The atomic write helpers replace individual files after their complete contents are written; they do not create a transaction covering every file in a batch or run.

Complete example

This plugin translates each missing plain-text target as one string. It keeps existing targets and does not track source edits or removals:

import { access, readFile } from 'node:fs/promises';
import type {
  LangnosticPlugin,
  TranslationFileMetadata,
  TranslationStringArg,
} from 'langnostic';
import { writeFileAtomic } from 'langnostic';

export default function TextPlugin(): LangnosticPlugin {
  let files: ReadonlyArray<TranslationFileMetadata> = [];

  return {
    type: 'plain-text',
    stringsPerRequest: 1,
    async trackFiles(metadata) {
      files = metadata;
    },
    async getTranslationStrings() {
      const strings: TranslationStringArg[] = [];

      for (const file of files) {
        const targets: string[] = [];
        for (const target of file.targets) {
          try {
            await access(target.path);
          } catch (error) {
            if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
              throw error;
            }
            targets.push(target.locale);
          }
        }

        if (targets.length > 0) {
          strings.push({
            batchId: file.source.path,
            id: 'content',
            source: {
              locale: file.source.locale,
              string: await readFile(file.source.path, 'utf8'),
            },
            targets,
          });
        }
      }

      return strings;
    },
    async getInstructions() {
      return 'Translate the plain text, retaining its paragraph structure.';
    },
    async onTranslationBatchComplete(results) {
      for (const result of results) {
        const file = files.find((item) => item.source.path === result.batchId);
        for (const translation of result.targets) {
          const target = file?.targets.find(
            (item) => item.locale === translation.locale,
          );
          if (!target) {
            throw new Error('Translation has no matching target file');
          }
          await writeFileAtomic(target.path, translation.string);
        }
      }
    },
  };
}

Import TextPlugin into your configuration and assign TextPlugin() to a group. Map source .txt files to target .txt paths using {locale}. See Configuration for the surrounding AI and locale settings.

For formats with multiple messages per file, buildTranslationStrings builds pending arguments and buildTranslatedContentMap groups completed results by locale and ID.

Was this page helpful?