CentercodePorygon
DittoPorygonAPI

Translations

AI-powered content translation with language inheritance

Safe

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

TriggerMethodUse Case
Content PublishInngest batch jobProduction translations
Preview ModeOn-demand APIReal-time preview
Manual OverridePUT endpointHuman 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 program

Patterns

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 program

Always 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

MethodEndpointDescription
POST/api/v1/translations/batchTrigger batch translation job
GET/api/v1/translations/{entityType}/{entityId}Get translations
GET/api/v1/translations/{entityType}/{entityId}/statusTranslation coverage
PUT/api/v1/translations/{entityType}/{entityId}/{field}Manual update
POST/api/v1/translations/{entityType}/{entityId}/translateOn-demand translate

Key Files

FilePurpose
src/features/translations/repository.tsDatabase queries
src/features/translations/services.tsBusiness logic, language settings
src/features/translations/ai/translator.tsAI translation with aiGenerateObject
src/features/translations/types.tsTranslatableString, TRANSLATABLE_FIELDS
src/lib/jobs/functions/translate-content.tsInngest batch translation job

Related

AI

AI generation patterns

Background Jobs

Inngest batch jobs

Form System

Translatable content