Translation Candidates and Patches
Palamedes Core exposes a provider-neutral boundary for finding local translation work and writing completed translations back to repository-owned catalogs. The API is available from Rust and through @palamedes/core-node.
It deliberately does not choose a translation provider, authenticate remote requests, build prompts, or decide whether a translation is linguistically acceptable. A CLI, editor, hosted workflow, or local script can own those choices while sharing the same catalog semantics.
Translation Foundation And Boundaries
translation_candidates is the implemented local foundation: it enumerates
work with stable identities and optimistic-concurrency fingerprints, then
applies completed values with validation and per-file atomic writes. This page
is the reference for that shipped boundary.
Adjacent local foundations may build on it without moving repository-owned catalog semantics into a separate product stack:
- terminology and protected-term loading for candidate preparation and QA;
- deterministic QA for placeholders, ICU structure, whitespace, and review signals;
- compact PO metadata and stale-metadata detection;
- flagged/unresolved report shapes; and
- bounded batching and retry decision support.
Those concerns belong in Palamedes only while they remain host-neutral, repository-local, and independent of account or provider concerns. Remote translation execution, provider routing, authentication, billing, policy packs, and hosted review or collaboration are product-layer responsibilities.
That split lets an optional managed layer compose discovery, request building, remote calls, retry coordination, incremental writeback, and reporting without reimplementing catalog semantics. Palamedes remains the complete open-source local toolchain and never requires that managed layer.
Enumerating candidates
listTranslationCandidates() scans every configured non-source locale by
default and returns active entries whose target value is missing. Pass
targets to select exact translated, fuzzy, or obsolete entries for a re-run or
review instead.
The source locale is never a translation target, including when it is named in
an explicit locale request. Default enumeration skips a configured target whose
catalog has not been created yet and emits a translation.missing_catalog
diagnostic with locale and catalogPath; run pmds extract to create it.
An explicitly requested missing target remains a hard read error so a caller
cannot mistake an incomplete requested scope for an empty result.
Every candidate includes:
- a stable identity consisting of catalog scope, locale, source message, and optional context;
- a resolved target path and storage format;
- singular content or structured top-level ICU plural branches;
- extracted comments and a bounded origin list;
- translated, fuzzy, and obsolete state;
- native Ferrocat machine provenance when present; and
- a per-entry fingerprint for optimistic concurrency control.
The fingerprint covers the candidate's source, current target, complete origin
state, relevant metadata, and review state. The maxOrigins response limit
only affects the displayed origin list, never the fingerprint. An unrelated
entry can therefore be written in an earlier incremental batch without
invalidating the remaining candidates.
Fingerprint payload changes are versioned. When upgrading across a fingerprint version, discard in-flight candidates and list them again before patching.
Applying completed translations
applyTranslationPatches() validates the complete request before writing any
catalog. Unknown or ambiguous catalogs, unknown messages, duplicate identities,
stale fingerprints, shape mismatches, invalid ICU plural branches, and invalid
provenance are returned as structured diagnostics with the rejected patch
identity. A rejected validation batch leaves every original catalog unchanged.
Source-locale patch identities are likewise rejected as
translation.source_locale, before any catalog write.
On success, Palamedes preserves unrelated translations, comments, origins,
flags, obsolete entries, and PO headers. Each changed PO or FCL file is replaced
atomically through Ferrocat's format-aware writer. A batch spanning several
files has per-file atomicity; it is not a filesystem transaction across files.
If a later replacement fails, the call still returns a hard error. Rust callers
can recover the completed per-file outcomes from
PalamedesError::translation_patch_result(); remaining patches are reported as
notApplied. Node callers receive an Error with code
ERR_PALAMEDES_TRANSLATION_PATCH_WRITE; its report property is the completed
TranslationPatchResult, and its cause describes the failed catalog write.
This remains an error rather than a successful partial result.
A patch without machine provenance is an authored completion and clears the
entry's fuzzy marker, the way gettext tools do when a translator confirms a
guessed entry; without it a fuzzy entry could never be finished through the API
and stayed incomplete in coverage. A patch carrying machine provenance leaves
review flags as they were — it records lock and ai instead. Every other flag
is preserved in both cases, and setting review flags remains the caller's
responsibility.
Provider-neutral TypeScript example
import {
applyTranslationPatches,
isTranslationPatchWriteError,
listTranslationCandidates,
type CatalogArtifactConfig,
type TranslationPatch,
} from "@palamedes/core-node"
declare function completedSingular(source: string): string
declare function completedPluralBranch(selector: string, source: string): string
const config: CatalogArtifactConfig = {
rootDir: process.cwd(),
sourceLocale: "en",
locales: ["en", "de"],
catalogs: [
{
path: "src/locales/{locale}/messages",
include: ["src"],
},
],
}
const { candidates, diagnostics } = listTranslationCandidates({
config,
locales: ["de"],
maxOrigins: 5,
})
if (diagnostics.length > 0) {
throw new Error(diagnostics.map(({ message }) => message).join("\n"))
}
// A provider, translation memory, editor, or human review step can produce
// these completed values. Core only validates and persists them.
const patches: TranslationPatch[] = candidates.map((candidate) => ({
id: candidate.id,
fingerprint: candidate.fingerprint,
translation:
candidate.source.kind === "singular"
? { kind: "singular", value: completedSingular(candidate.source.value) }
: {
...candidate.source,
values: Object.fromEntries(
Object.entries(candidate.source.values).map(([selector, source]) => [
selector,
completedPluralBranch(selector, source),
])
),
},
}))
let result
try {
result = applyTranslationPatches({ config, patches })
} catch (error) {
if (isTranslationPatchWriteError(error)) {
console.error(error.code, error.message, error.cause, error.report)
}
throw error
}
if (result.diagnostics.length > 0) {
// Re-enumerate stale candidates before retrying them.
console.error(result.diagnostics)
}When a machine produced a value, a patch can add native provenance without supplying its integrity lock. Core computes the lock from the completed value:
const patch: TranslationPatch = {
id: candidate.id,
fingerprint: candidate.fingerprint,
translation: { kind: "singular", value: "Zur Kasse" },
machine: {
ai: { model: "example/model", confidence: 0.92 },
},
}