Skip to main content

Internationalization & Translations

Terra supports unlimited languages through its internationalization (i18n) system. Every label, placeholder, help text, and error message can be translated—no plugins required.

Architecture Overview

I18nString

Individual translatable strings stored as { en: "...", es: "...", zh: "..." }

LocalizedDictionary

Global UI strings (buttons, errors, placeholders) stored in system settings

Form-Level Translations

Per-form translation matrix for questions, labels, and options

DeepL Integration

Auto-translate missing content with one click

The I18nString Type

Every translatable field in Terra uses the I18nString type:
// types/schema.ts

export type I18nString = string | Record<string, string>;

// Examples:
const simple = "First Name";                    // Shorthand for { en: "First Name" }
const translated = { 
  en: "First Name", 
  es: "Nombre", 
  zh: "名字" 
};
The helper function getLocalizedString resolves the appropriate translation:
import { getLocalizedString } from "@/types/schema";

// Returns "Nombre" for Spanish, falls back to English if missing
const label = getLocalizedString(field.label, "es");

Global Localization Dictionary

System-wide UI strings are stored in the LocalizedDictionary structure:
// lib/i18n.ts

interface I18nDictionary {
  ui: {
    submit: string;
    next: string;
    back: string;
    save: string;
    cancel: string;
    // ... more UI strings
  };
  errors: {
    required: string;
    invalidEmail: string;
    // ... validation messages
  };
  placeholders: {
    email: string;
    phone: string;
    // ... input placeholders
  };
}

type LocalizedDictionary = Record<string, I18nDictionary>;

Managing Global Translations

Access the global translation matrix at Settings → Localization:
Localization Matrix UI
FeatureDescription
Grid ViewEdit translations inline with spreadsheet-style interface
JSON ViewDirect JSON editing for bulk updates
Add LanguageAdd new languages (shows DeepL availability)
Auto-TranslateFill missing translations via DeepL
Missing OnlyFilter to show only incomplete translations
Column VisibilityHide/show language columns

Form-Level Translations

Each form has its own translation matrix for form-specific content:
  • Question labels
  • Help text
  • Placeholders
  • Choice options
  • Section titles
  • Validation messages

Accessing Form Translations

Navigate to Forms → [Form Name] → Settings → Translations:
Form Translation Matrix

Translation Matrix Features

The Field and English columns stay visible when scrolling horizontally, making it easy to work with many languages.
Missing translations show a yellow corner indicator and “Missing…” placeholder. The header displays a count of incomplete fields.
Click the dropdown on any DeepL-supported language column to auto-translate all missing entries for that language.
The “Auto-Translate All” button fills missing translations across all visible languages simultaneously.
Translations are visually grouped by page/section for easier navigation in long forms.

DeepL Integration

Terra uses DeepL for machine translation. DeepL offers superior quality for government documents compared to other services.

Setup

Add your DeepL API key to .env.local:
# Get from: DeepL Dashboard → Account → API Keys
DEEPL_API_KEY=your-api-key:fx
The :fx suffix indicates a free API key. Paid keys don’t have this suffix.

Supported Languages

LanguageCodeDeepL Support
Englishen✅ Source
Spanishes✅ Auto-translate
Frenchfr✅ Auto-translate
Germande✅ Auto-translate
Italianit✅ Auto-translate
Portuguesept✅ Auto-translate
Russianru✅ Auto-translate
Japaneseja✅ Auto-translate
Chinesezh✅ Auto-translate
Koreanko✅ Auto-translate
Arabicar✅ Auto-translate
Dutchnl✅ Auto-translate
Polishpl✅ Auto-translate
Ukrainianuk✅ Auto-translate
Somaliso❌ Manual only
Vietnamesevi❌ Manual only
Tagalogtl❌ Manual only
Hindihi❌ Manual only
Thaith❌ Manual only

Server Actions

Two server actions power the translation features:
// app/actions/translate.ts

// Auto-translate global dictionary
export async function autoTranslateDictionary(targetLanguage: string): Promise<{
  success: boolean;
  count?: number;
  error?: string;
}>;

// Translate individual content
export async function translateContent(
  text: string,
  targetLanguages: string[]
): Promise<Record<string, string | null>>;
Example usage:
import { translateContent } from "@/app/actions/translate";

// Translate "Hello" to Spanish and Chinese
const result = await translateContent("Hello", ["es", "zh"]);
// Result: { es: "Hola", zh: "你好" }

Dev Mode Behavior

When DEEPL_API_KEY is not set, translations return mock values for testing:
// Without API key:
translateContent("Hello", ["es"]);
// Returns: { es: "[ES] Hello" }

Field-Level Auto-Translate

The form builder’s Properties Panel includes inline translation for individual fields:
1

Edit a Field

Select any field in the form builder and open the Properties Panel.
2

Enter English Text

Type the label, help text, or placeholder in English.
3

Click the Sparkle Icon

Click the ✨ icon next to the input to auto-translate to all supported languages.
4

Review Translations

Switch languages in the form builder to verify translations.
The sparkle button only appears for fields that support translation (labels, help text, placeholders, option labels).

Adding a New Language

To the Global Dictionary

  1. Go to Settings → Localization
  2. Click + Add Language
  3. Select from the categorized list (DeepL-supported vs Manual)
  4. Click Add Language
  5. Use Auto-Translate or enter translations manually

To a Form

  1. Go to Forms → [Form] → Settings → Translations
  2. Click + Add Language
  3. Select the language
  4. Click Auto-Translate All or translate manually
  5. Click Save

Best Practices

Always Review Auto-Translations

Machine translation is a starting point. Have native speakers review critical content like eligibility requirements.

Use Simple English

Write source text in plain language. Avoid idioms, jargon, and complex sentence structures that translate poorly.

Test with Real Users

Conduct usability testing with speakers of each supported language, especially for high-stakes forms.

Maintain Consistency

Use the same terminology throughout. “Submit” should always translate the same way across the form.

Writing for Translation

"You have " + count + " items""You have {count} items" (use interpolation)Word order varies by language. Let the translation handle placement.
German text is ~30% longer than English. Spanish is ~20% longer. Design UI with flexible widths.
"Click **here** to continue"✅ Keep formatting separate from translatable strings when possible.
“Save” as a noun vs verb translates differently. Use clear, unambiguous labels.

Technical Reference

Schema Flattening

Form translations use a flattening algorithm to create the translation matrix:
// lib/form-translation-utils.ts

interface FlattenedTranslationRow {
  id: string;           // Unique row ID
  pageId: string;       // Page this belongs to
  pageTitle: string;    // Page display name
  key: string;          // Property key (label, helpText, etc.)
  displayLabel: string; // Human-readable label
  path: string;         // JSON path to the property
  translations: Record<string, string>;
}

// Flatten a form schema
const rows = flattenFormSchema(schema);

// Apply translations back
const updatedSchema = applyTranslationsToSchema(schema, updatedRows);

Language Configuration

Add new languages in lib/i18n.ts:
export const SUPPORTED_LANGUAGES = [
  { code: "en", name: "English", deepl: true },
  { code: "es", name: "Spanish", deepl: true },
  { code: "so", name: "Somali", deepl: false },
  // Add more languages here
];

DeepL Language Mapping

Some language codes need mapping to DeepL’s format:
// app/actions/translate.ts

const DEEPL_LANGUAGE_MAP: Record<string, string> = {
  en: "en-US",
  pt: "pt-BR",
  zh: "zh-HANS",
  // Add mappings for regional variants
};

Troubleshooting

Check that DEEPL_API_KEY is set in .env.local. Mock data (e.g., [ES] Hello) indicates missing or invalid API key.
DeepL free tier has character limits. Upgrade to Pro or wait for monthly reset.
  1. Ensure it’s added to SUPPORTED_LANGUAGES in lib/i18n.ts
  2. Check column visibility settings (it may be hidden)
  3. Verify the language was saved to the form/dictionary
Click Save after editing. Changes are not auto-saved. Check browser console for errors.
The translation matrix uses optimized rendering with memoization. If experiencing performance issues with many languages, hide unused columns.

Back to Setup Guide

Return to the local development setup guide.