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.