Overview
The translation system provides AI-powered translations with language inheritance from platform to program to project. Translations track source content hashes for change detection, and batch jobs run via Inngest on content publish.
What it is
A translation system with language settings inheritance, AI translation via gpt-5-nano, and sourceHash tracking for efficient re-translation.
Why we use it
Cost-efficient AI translations, automatic re-translation on content change, flexible language settings at each scope level.
When to use
When content needs to be available in multiple languages. Translations trigger automatically on publish or on-demand for preview.
Language Inheritance
Platform → Program → Project ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Platform │ ──▶ │ Program │ ──▶ │ Project │ │ SystemLang │ │ LangSettings │ │ LangSettings │ │ (available) │ │ (optional) │ │ (optional) │ └──────────────┘ └──────────────┘ └──────────────┘ If project has settings → use project settings Else if program has settings → inherit from program Else → use platform defaults
Translation Workflow
| Trigger | Method | Use Case |
|---|---|---|
| Content Publish | Inngest batch job | Production translations |
| Preview Mode | On-demand API | Real-time preview |
| Manual Override | PUT endpoint | Human corrections |
Quick Start
Language Settings
Get effective language settings with inheritance.
// Get effective language settings for a resource
import { getResourceLanguageSettings } from '@/features/translations';
const { supportedLanguages, primaryLanguage } = await getResourceLanguageSettings(
projectId,
programId
);
// Returns project settings if set, otherwise inherits from programPatterns
Get Translations
Fetch translations for a resource or block.
// Get translations for an entity import * as translationsRepo from '@/features/translations/repository'; const translations = await translationsRepo.getTranslationsForEntity( 'RESOURCE', // entityType: 'RESOURCE' | 'BLOCK' resourceId, 'es' // optional locale filter );
AI Translation
Translate strings using AI (gpt-5-nano for cost efficiency).
// AI-powered translation service
import { translateStrings } from '@/features/translations/ai/translator';
const result = await translateStrings({
strings: [
{ key: 'title', value: 'Welcome Survey' },
{ key: 'description', value: 'Tell us about yourself' },
],
sourceLocale: 'en',
targetLocale: 'es',
});
// Returns:
// {
// translations: [{ key: 'title', value: 'Encuesta de bienvenida' }, ...],
// tokenUsage: { prompt: 120, completion: 45 }
// }Batch Translation
Trigger batch translation via Inngest.
// Trigger batch translation on publish via Inngest
import { dispatch } from '@/lib/jobs';
await dispatch({
name: 'translations/batch.translate',
data: {
entityType: 'RESOURCE',
entityIds: [resourceId],
sourceLocale: primaryLanguage,
targetLocale,
forceRetranslate: false, // Only translate changed content
},
});On-Demand Translation
Immediate translation for preview mode.
// On-demand translation API for preview mode
const response = await fetch(
`/api/v1/translations/RESOURCE/${resourceId}/translate`,
{
method: 'POST',
body: JSON.stringify({
sourceLocale: 'en',
targetLocale: 'es',
}),
}
);Upsert Translations
Store translations with change tracking.
// Upsert translations with change tracking
import * as translationsRepo from '@/features/translations/repository';
import { calculateSourceHash } from '@/features/translations';
await translationsRepo.upsertTranslations([
{
entityType: 'RESOURCE',
entityId: resourceId,
field: 'title',
locale: 'es',
value: 'Encuesta de bienvenida',
sourceHash: calculateSourceHash(originalValue), // For change detection
isAI: true,
needsReview: true,
},
]);Watch Out
Use getResourceLanguageSettings for inheritance
Don't
// WRONG - Query project settings only
const settings = await prisma.projectLanguageSettings.findUnique({
where: { projectId },
});
// Misses program-level fallback!Do
// RIGHT - Use inheritance helper
const { supportedLanguages, primaryLanguage } = await getResourceLanguageSettings(
projectId,
programId
);
// Returns project settings if set, otherwise inherits from programAlways track sourceHash for change detection
Don't
// WRONG - No change tracking
await translationsRepo.upsertTranslations([{
entityType: 'RESOURCE',
entityId: resourceId,
field: 'title',
locale: 'es',
value: translatedValue,
// Missing sourceHash!
}]);Do
// RIGHT - Track source changes
await translationsRepo.upsertTranslations([{
entityType: 'RESOURCE',
entityId: resourceId,
field: 'title',
locale: 'es',
value: translatedValue,
sourceHash: calculateSourceHash(originalValue),
isAI: true,
needsReview: true,
}]);- Primary language is the source for all translations - never translate from a translated version
- Background jobs use withRLSBypass for database access
- Set needsReview: true for AI translations to flag for human review
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/translations/batch | Trigger batch translation job |
| GET | /api/v1/translations/{entityType}/{entityId} | Get translations |
| GET | /api/v1/translations/{entityType}/{entityId}/status | Translation coverage |
| PUT | /api/v1/translations/{entityType}/{entityId}/{field} | Manual update |
| POST | /api/v1/translations/{entityType}/{entityId}/translate | On-demand translate |
Key Files
| File | Purpose |
|---|---|
| src/features/translations/repository.ts | Database queries |
| src/features/translations/services.ts | Business logic, language settings |
| src/features/translations/ai/translator.ts | AI translation with aiGenerateObject |
| src/features/translations/types.ts | TranslatableString, TRANSLATABLE_FIELDS |
| src/lib/jobs/functions/translate-content.ts | Inngest batch translation job |