Translation projects
Overview
A translation project groups the files being translated from one source
language into one or more target languages. Projects, targets, files,
and segments are defined in prisma/schema/product.prisma and exposed through
src/server/routers/translation.router.ts.
Every query is scoped to the caller's organization — there is no cross-org
visibility, and a target from another project can never be paired with a
file's segments (an id mismatch is rejected as a BAD_REQUEST, not silently
ignored).
Targets
A project can have any number of target languages, added or removed from the
project settings screen. Adding a target after files already exist backfills
a FileTarget row for every existing file, so progress tracking and export
work immediately — you don't need to re-upload anything.
Uploading XLIFF files
Files are uploaded as .xlf/.xliff (XLIFF 1.2 only, up to 10 MB) via
POST /api/translation/upload, multipart with a projectId field and a
file field. The route:
- validates the session and organization membership,
- rate-limits per user (
checkRouteRateLimit), - rejects anything that doesn't parse as XLIFF 1.2,
- stores the raw file in the configured storage provider under a server-derived key (the client never controls the storage path),
- creates the
TranslationFilerow inimportingstatus, and - enqueues an
importjob.
The worker parses the file asynchronously (src/lib/translation/import-processor.ts)
and bulk-inserts segments, then flips the file to ready. A parse failure
(malformed XML, an empty file, a nesting depth beyond the engine's cap) marks
the file failed with a human-readable importError — never a raw stack
trace.
Segments
XLIFF <trans-unit> elements are parsed into one or more segments
(a unit is split further when it contains multiple whitespace-separated
sentences). Each segment carries its source text, its position in the file,
and one SegmentTranslation row per target language. Placeholder markup
inside a unit ({1}text{/1}, {2/}) is preserved as literal tokens end to
end — the export always writes back exactly what it read, modulo the
translated text.
Exporting
Once translations exist, POST the exportFile mutation (or the export
button in the project screen) to enqueue an export job. The worker
regenerates a translated XLIFF from the stored segments and writes it to
storage; GET /api/translation/download?fileId=...&targetId=... streams it
back once FileTarget.exportedFileKey is set. Both upload and download are
rate-limited and re-validate organization ownership from the database, never
from the request.