Payload CMS 3 plugin for managing UI translations with automatic string collection and full static generation support
npm install payload-translations> A Payload CMS 3 plugin for managing UI translations with automatic string collection and full static generation support
- Features
- Installation
- Quick Start
- 1. Add the plugin to your Payload config
- 2. Set up the translations provider
- 3. Use translations in your components
- 4. Fill in translations
- Common Use Cases
- Variables in translations
- Pluralization
- Dynamic messages
- Configuration
- API Reference
- translationsPlugin(options)
- getTranslations(locale, config)
- useTranslations()
- t(key, contextOrVars?, vars?)
- formatDate(date, style?)
- formatNumber(num, options?)
- formatCurrency(amount, currency?)
- Development Tools
- Automatically Generate Translation Fields
- Run Tests
- TypeScript Support
- Performance
- Translation Updates & Caching
- How it Works
- Automatic Revalidation (Default)
- Disabling Automatic Revalidation
- Other Strategies
- Examples
- License
- Support
- ⨠Automatic Field Generation - CLI scanner finds all t() calls and generates field definitions
- š Dual Interpolation - Supports both ICU MessageFormat and sprintf-style variables
- š Familiar-Style - Familiar t('key', 'Context') API for easy adoption (if you used WPML or Polylang in the past)
- šÆ Type-Safe - Full TypeScript support with autocomplete
- ā” Zero Runtime Overhead - All translations fetched at build time
- š SSG Compatible - Works with Next.js static generation
- š¦ Tiny Bundle - ~2KB gzipped
- š Missing Translation Detection - Automatically logs missing translations in dev
``bash`
npm install payload-translationsor
pnpm add payload-translationsor
yarn add payload-translations
`typescript
// payload.config.ts
import { buildConfig } from 'payload'
import { translationsPlugin } from 'payload-translations'
export default buildConfig({
// ... your config
plugins: [
translationsPlugin({
// Define your translation fields (required)
customFields: [
{
label: 'Navigation',
fields: [
{ name: 'home', type: 'text', localized: true, required: true },
{ name: 'about', type: 'text', localized: true, required: true },
{ name: 'contact', type: 'text', localized: true, required: true },
],
},
{
label: 'Authentication',
fields: [
{ name: 'loginButton', type: 'text', localized: true, required: true },
{ name: 'logoutButton', type: 'text', localized: true, required: true },
{ name: 'forgotPassword', type: 'text', localized: true, required: true },
],
},
],
}),
],
})
`
`tsx
// app/[locale]/layout.tsx
import { TranslationsProvider } from 'payload-translations/react'
import { getTranslations } from 'payload-translations/server'
export default async function Layout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ locale: string }>
}) {
const { locale } = await params
const translations = await getTranslations(locale)
return (
{children}
)
}
`
Client Components:
`tsx
'use client'
import { useTranslations } from 'payload-translations/react'
export function MyComponent() {
const { translations, t, formatDate } = useTranslations()
return (
Server Components (WPML-style - same as client!):
`tsx
import { getTranslations } from 'payload-translations/server'
import config from '@/payload.config'export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const { t } = await getTranslations(locale, config)
return (
{t('Welcome to our site', 'HomePage')}
)
}
`Or use direct access if you prefer:
`tsx
const { translations } = await getTranslations(locale, config)
return {translations.home}
// Type-safe
`$3
Go to
/admin/globals/translations in your Payload admin panel and fill in translations for all locales.Common Use Cases
$3
ICU MessageFormat (Object):
`tsx
// In your CMS, add field 'welcomeMessage' with value:
// "Welcome back, {name}!"const { t } = useTranslations()
{t('welcomeMessage', { name: user.name })}
// Output: "Welcome back, John!"
`Sprintf Style (Array):
`tsx
// In your CMS, add field 'welcomeMessage' with value:
// "Welcome back, %s!"const { t } = useTranslations()
{t('welcomeMessage', [user.name])}
// Output: "Welcome back, John!"
`$3
ICU MessageFormat (with automatic locale rules):
`tsx
// In your CMS, add field 'cartItems' with value:
// "{count, plural, zero {No items} one {# item} other {# items}}"const { t } = useTranslations()
{t('cartItems', { count: 0 })}
// "No items"
{t('cartItems', { count: 1 })}
// "1 item"
{t('cartItems', { count: 5 })}
// "5 items"
`Sprintf Style (simpler but manual):
`tsx
// Store both singular and plural in CMS, choose manually
const { t } = useTranslations()
const count = 5
{t(count === 1 ? 'item' : 'items', [count])}
// "5 items"
`$3
ICU MessageFormat:
`tsx
// In your CMS, add field 'notification' with value:
// "{user} liked your {type}"const { t } = useTranslations()
{t('notification', { user: 'Sarah', type: 'post' })}
// Output: "Sarah liked your post"
`Sprintf Style:
`tsx
// In your CMS, add field 'notification' with value:
// "%s liked your %s"const { t } = useTranslations()
{t('notification', ['Sarah', 'post'])}
// Output: "Sarah liked your post"
`Configuration
The plugin is fully generic - you define all translation fields for your project:
`typescript
translationsPlugin({
customFields: [
{
label: 'Navigation',
fields: [
{ name: 'home', type: 'text', localized: true, required: true },
{ name: 'about', type: 'text', localized: true, required: true },
],
},
{
label: 'Forms',
fields: [
{ name: 'submit', type: 'text', localized: true, required: true },
{ name: 'cancel', type: 'text', localized: true, required: true },
],
},
],
})
`Organize fields into tabs for better admin UX.
API Reference
$3
#### Options
`typescript
{
// Whether to enable the translations global (default: true)
enabled?: boolean // The slug for the translations global (default: 'translations')
slug?: string
// Translation field tabs (required)
// Each tab groups related translation fields
customFields: Array<{
label: string // Tab label in admin
fields: Field[] // Payload field definitions
}>
}
`Example:
`typescript
translationsPlugin({
customFields: [
{
label: 'UI Components',
fields: [
{ name: 'loading', type: 'text', localized: true, required: true },
{ name: 'error', type: 'text', localized: true, required: true },
{ name: 'success', type: 'text', localized: true, required: true },
],
},
],
})
`$3
Server-side function to fetch translations for a specific locale.
`typescript
import { getTranslations } from 'payload-translations/server'
import config from '@/payload.config'const { t, translations, formatDate, formatNumber, formatCurrency, locale } =
await getTranslations('en', config)
// WPML-style (recommended)
// Direct access (type-safe)
{translations.home}
`Returns:
-
t(key, context?) - WPML-style translation function
- translations - Raw translations object
- formatDate() - Locale-aware date formatting
- formatNumber() - Locale-aware number formatting
- formatCurrency() - Locale-aware currency formatting
- locale - Current locale string$3
React hook for accessing translations in client components.
`typescript
const {
translations, // Translation object
locale, // Current locale
t, // WPML-style helper function
formatDate, // Locale-aware date formatting
formatNumber, // Locale-aware number formatting
formatCurrency, // Locale-aware currency formatting
} = useTranslations()
`$3
WPML-style translation helper with dual interpolation support - use whichever style you prefer:
#### ICU MessageFormat Style (Object) - Recommended
Modern, explicit approach with named placeholders:
`typescript
const { t } = useTranslations()// Simple variables
{t('Welcome {name}', 'HomePage', { name: 'John' })}
// Output: "Welcome John"// Without context
{t('Hello {username}', { username: 'Alice' })}
// Output: "Hello Alice"// Pluralization with automatic locale rules
{t('{count, plural, one {# item} other {# items}}', 'Cart', { count: 1 })}
// Output: "1 item"{t('{count, plural, one {# item} other {# items}}', 'Cart', { count: 5 })}
// Output: "5 items"// Complex pluralization
{t('You have {count, plural, zero {no messages} one {# message} other {# messages}}', { count: 0 })}
// Output: "You have no messages"
`#### Sprintf Style (Array) - WPML Compatible
Familiar WordPress-style positional arguments:
`typescript
// Simple string substitution
{t('Welcome %s', 'HomePage', ['John'])}
// Output: "Welcome John"// Without context
{t('Hello %s', ['Alice'])}
// Output: "Hello Alice"// Multiple values
{t('Hello %s, you have %d new messages', ['John', 5])}
// Output: "Hello John, you have 5 new messages"// Number formatting
{t('Total: %d items at $%f each', [42, 19.99])}
// Output: "Total: 42 items at $19.99 each"
`Format Specifiers:
-
%s - String
- %d / %i - Integer (rounds down)
- %f / %u - Float/Number#### How It Works
The plugin automatically detects which style you're using:
- Pass an object
{ name: 'John' } ā ICU MessageFormat
- Pass an array ['John'] ā Sprintf styleNo configuration needed - just use whichever style feels natural!
#### ICU MessageFormat Features
- Simple variables:
{variableName}
- Pluralization: {count, plural, zero {...} one {...} other {...}}
- Automatic plural rules: Uses Intl.PluralRules for locale-aware pluralizationMissing translations are logged in dev console:
`
š Missing Translations Detected
Copy-paste these fields into your translationFields array: {
name: 'welcome',
type: 'text',
label: 'Welcome {name}',
localized: true,
// Used in: HomePage
}
`Simply copy the logged output and paste it into your
translationFields array in your config!$3
Locale-aware date formatting:
`typescript
const { formatDate } = useTranslations()formatDate(new Date(), 'full') // "Monday, January 15, 2025"
formatDate(new Date(), 'long') // "January 15, 2025"
formatDate(new Date(), 'medium') // "Jan 15, 2025"
formatDate(new Date(), 'short') // "1/15/25"
`$3
Locale-aware number formatting:
`typescript
const { formatNumber } = useTranslations()formatNumber(1234.56) // "1,234.56" (en) or "1.234,56" (nl)
formatNumber(0.1234, { style: 'percent' }) // "12.34%"
`$3
Locale-aware currency formatting:
`typescript
const { formatCurrency } = useTranslations()formatCurrency(99.99, 'EUR') // "ā¬99.99" (en) or "⬠99,99" (nl)
formatCurrency(99.99, 'USD') // "$99.99"
`Development Tools
$3
The plugin includes a CLI scanner that finds all
t() calls in your codebase and generates the field definitions for you:`bash
npx payload-translations scan [pattern]
`Default pattern:
src/*/.{ts,tsx,js,jsx}Example output:
`
š Scanning for translation calls...š Found 14 unique translation calls:
LoginForm: 1 translations
HomePage: 2 translations
Footer: 4 translations
š Copy these field definitions to your translation config:
{
type: 'collapsible',
label: 'LoginForm',
admin: { initCollapsed: true },
fields: [
{
name: 'submit',
type: 'text',
label: 'Submit',
localized: true,
},
],
},
`Simply copy-paste the output into your
translationFields array!Usage examples:
`bash
Scan and display fields (copy-paste required)
npx payload-translations scanAutomatically append to your translation fields file
npx payload-translations scan --writeSpecify a custom file to append to
npx payload-translations scan --write src/my-translations.tsScan specific directory and auto-write
npx payload-translations scan "components/*/.tsx" --write
`How it works:
1. Scans your code for
t('key') and t('key', 'Context') calls
2. Groups translations by context (component name)
3. Converts keys to camelCase field names
4. Generates ready-to-use Payload field definitions
5. With --write: Automatically appends new fields to your file
6. Automatically organizes fields into collapsible groupsAuto-detection of translation files:
When using
--write without specifying a file, the CLI looks for:-
src/translations/fields.ts
- src/translations/fields.js
- src/translations/config.ts
- src/translations/config.js
- translations/fields.ts
- translations/fields.js$3
`bash
pnpm test
`TypeScript Support
The plugin is fully typed. Your IDE will autocomplete translation keys and catch typos:
`typescript
const { translations } = useTranslations()
translations.home // ā
Valid
translations.homer // ā TypeScript error
`To generate types for custom fields:
`bash
pnpm payload generate:types
`Performance
- ā” Zero runtime overhead - All translations fetched at build time
- š Fully static - Works with Next.js static generation
- š¦ Small bundle - ~2KB gzipped
- šÆ No hydration issues - Server and client stay in sync
Translation Updates & Caching
$3
By default, translations are fetched when pages are rendered. In production with Next.js static generation:
- Translations are fetched at build time
- Results are cached in the static HTML
- Changes in Payload admin require revalidation to appear
$3
The plugin automatically revalidates all pages when translations change. This is enabled by default and requires zero configuration:
`typescript
translationsPlugin({
revalidateOnChange: true, // ā Default! No setup needed
customFields: [
/ ... /
],
})
`How it works automatically:
When you update translations in the Payload admin, the plugin:
1. Detects the change via an internal
afterChange hook
2. Calls revalidatePath('/', 'layout') to revalidate all pages
3. Next.js regenerates pages with the new translations
4. Changes appear immediately - no rebuild or app code changes required!⨠You don't need to add any hooks or code to your app - it just works!
$3
If you prefer manual control or aren't using Next.js:
`typescript
translationsPlugin({
revalidateOnChange: false, // Disable auto-revalidation
customFields: [
/ ... /
],
})
`$3
Time-Based ISR:
`typescript
// In your page/layout
export const revalidate = 3600 // Revalidate every hour
`Manual On-Demand Revalidation:
`typescript
// Create a webhook endpoint
import { revalidatePath } from 'next/cache'export async function POST() {
revalidatePath('/', 'layout')
return Response.json({ revalidated: true })
}
`Dynamic Rendering (always fresh):
`typescript
// Force dynamic rendering for a specific page
export const dynamic = 'force-dynamic'
``See the README.md for comprehensive examples and advanced usage patterns.
MIT
- GitHub Issues
- Documentation
- npm Package
- Payload CMS Discord