Skip to Content
⚠️Active Development Notice: TimeTiles is under active development. Information may be placeholder content or not up-to-date.

Content Localization

TimeTiles uses two complementary systems for internationalization:

  • next-intl — translates UI strings (buttons, labels, navigation chrome)
  • Payload CMS localization — translates CMS-managed content (pages, menus, footer, branding)

This page covers the Payload CMS content localization system. For import language detection (CSV column header matching), see Language Support.

Supported Locales

CodeLanguageURL PatternNotes
enEnglish/exploreDefault locale, no URL prefix
deGerman/de/explorePrefixed with /de/

Configured in i18n/config.ts and payload-config-factory.ts.

Architecture Overview

┌─────────────────────────────────────────────────────┐ │ Browser Request │ │ /de/about or /about │ └──────────────────────┬──────────────────────────────┘ ┌────────▼────────┐ │ middleware.ts │ Detects locale from URL/cookie/header │ (next-intl) │ Sets locale for the request └────────┬────────┘ ┌──────────────┼──────────────┐ │ │ │ ┌────▼────┐ ┌─────▼─────┐ ┌────▼────┐ │ UI │ │ CMS │ │ API │ │ Strings │ │ Content │ │ Routes │ │ │ │ │ │ │ │messages/│ │ Payload │ │ No │ │en.json │ │ findGlobal│ │ locale │ │de.json │ │ ({locale})│ │ │ └─────────┘ └───────────┘ └─────────┘

What Gets Localized

CMS Content (Payload localization)

These are admin-managed content stored in the database with per-locale values:

Collection/GlobalLocalized Fields
Pagestitle, all block text fields (hero titles, descriptions, button text, etc.)
MainMenunavItems[].label
Footertagline, columns[].title, columns[].links[].label, newsletter text, copyright, credits
BrandingsiteName, siteDescription

NOT Localized

ContentReason
CatalogsUser-generated content — users create in their own language
DatasetsHas a language field (ISO 639-3) describing the data language, not UI locale
EventsUser data
Slugs, URLsMust be stable across locales
Select options, IDsStructural, not user-facing

How Payload Localization Works

Field-Level Storage

When a field has localized: true, Payload stores values in a separate _locales table instead of the main table:

-- Before localization: value in main table SELECT title FROM pages WHERE id = 1; -- "Home" -- After localization: values in locale table SELECT title FROM pages_locales WHERE _parent_id = 1 AND _locale = 'en'; -- "Home" SELECT title FROM pages_locales WHERE _parent_id = 1 AND _locale = 'de'; -- "Startseite"

Fallback Behavior

The Payload config uses fallback: true:

localization: { locales: [ { label: "English", code: "en" }, { label: "Deutsch", code: "de" }, ], defaultLocale: "en", fallback: true, }

If a German translation is missing for a field, Payload returns the English value. This means content works immediately — you can translate gradually without broken pages.

Querying with Locale

Frontend server components pass the current locale to Payload queries:

import { getLocale } from "next-intl/server"; import type { Locale } from "@/i18n/config"; const locale = (await getLocale()) as Locale; // Globals const footer = await payload.findGlobal({ slug: "footer", locale }); // Collections const pages = await payload.find({ collection: "pages", where: { slug: { equals: "home" } }, locale });

The locale parameter tells Payload which translation to return. Without it, the default locale (English) is used.

Admin Dashboard

In the Payload dashboard at /dashboard, localized fields show locale tabs (EN / DE). Editors can switch between locales to enter translations for each field.

Adding a Localized Field

Step 1: Mark the Field

Add localized: true to any text, textarea, or richText field in a collection or global:

// In a collection config fields: [ { name: "title", type: "text", required: true, localized: true }, { name: "slug", type: "text" }, // NOT localized — stable identifier ];

Step 2: Create Migration

cd apps/web && pnpm payload:migrate:create

This generates a migration that creates locale tables and moves existing data.

Step 3: Pass Locale in Queries

Ensure all frontend queries for this collection pass the locale parameter (see Querying with Locale).

Step 4: Update Seeds (if applicable)

Add translations to seed files for the new localized fields. See Seed Data below.

Seed Data

Seed scripts provide content in both English and German:

The seeding operation calls updateGlobal twice — once for English (default), once for German:

// English (default locale) await payload.updateGlobal({ slug: "main-menu", data: mainMenuSeed }); // German await payload.updateGlobal({ slug: "main-menu", data: mainMenuSeedDe, locale: "de" });

Pages

Pages are created in English first, then updated with German translations:

const doc = await payload.create({ collection: "pages", data: englishData }); await payload.update({ collection: "pages", id: doc.id, data: germanData, locale: "de" });

German seed data is defined in pagesSeedDe (keyed by slug) in lib/seed/seeds/pages.ts.

Adding a New Locale

To add a third locale (e.g., French):

  1. i18n config — add "fr" to SUPPORTED_LOCALES in i18n/config.ts
  2. Payload config — add { label: "Français", code: "fr" } to localization.locales in payload-config-factory.ts
  3. next-intl messages — create messages/fr.json with UI string translations
  4. Migration — run pnpm payload:migrate:create to update the _locales enum
  5. Seed data — add French translations to seed files (optional — fallback covers missing translations)
  6. Middleware — no changes needed; next-intl auto-detects from the routing config

Key Files

FilePurpose
i18n/config.tsLocale definitions (SUPPORTED_LOCALES, Locale type)
i18n/routing.tsURL routing strategy (localePrefix: "as-needed")
i18n/request.tsPer-request message loading
i18n/navigation.tsLocale-aware Link, useRouter, etc.
middleware.tsLocale detection and routing
messages/en.jsonEnglish UI strings
messages/de.jsonGerman UI strings
lib/config/payload-config-factory.tsPayload localization config
lib/globals/main-menu.tsMainMenu with localized label
lib/globals/footer.tsFooter with 8 localized fields
lib/globals/branding.tsBranding with localized siteName, siteDescription
lib/blocks/*.tsBlock definitions with localized text fields
lib/seed/seeds/pages.tsEnglish + German page content
lib/seed/seeds/footer.tsEnglish + German footer content
lib/seed/seeds/main-menu.tsEnglish + German nav labels

Localization vs Language Support

These are two separate systems:

AspectContent Localization (this page)Language Support
PurposeTranslate UI and CMS contentDetect CSV column headers
CodesISO 639-1 (en, de)ISO 639-3 (eng, deu)
ScopePages, menus, footer, brandingImport field mapping
HowPayload localized: true + next-intlfranc detection + regex patterns
AffectsWhat visitors seeHow imports are processed
Last updated on