Skip to main content
Localizia

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 TranslationFile row in importing status, and
  • enqueues an import job.

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.