Adds a translations global to Payload where developers define localized text fields for UI strings, which editors fill in per locale in the admin. A server function and React provider expose a t() helper and locale-aware formatters in Next.js apps. A CLI scanner finds t() calls in code and generates the field definitions, and pages are revalidated automatically when translations change.
Payload CMS 3 plugin for managing UI translations with automatic string collection and full static generation support
Installation
npm i payload-translations4
Stars
0
Forks
0
Open Issues
1 years ago
Last Updated
0
Weekly Downloads
v0.8.2
npm Version
65.4 KB
Unpacked Size
1
Dependencies
Trends appear after 7 days of data. Daily snapshots have not started yet.
Trends appear after 7 days of data. Daily snapshots have not started yet.
Each point is npm's rolling 7-day download count.
Health Score
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1076960199-root)<a href="https://payloaddirectory.dev/plugins/1076960199-root"><img src="https://payloaddirectory.dev/badge/1076960199-root.svg" alt="Payload Directory"></a>README
A Payload CMS 3 plugin for managing UI translations with automatic string collection and full static generation support
t() calls and generates field definitionst('key', 'Context') API for easy adoption (if you used WPML or Polylang in the past)npm install payload-translations
# or
pnpm add payload-translations
# or
yarn add payload-translations
// 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 },
],
},
],
}),
],
})
// 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 (
<TranslationsProvider translations={translations} locale={locale}>
{children}
</TranslationsProvider>
)
}
Client Components:
'use client'
import { useTranslations } from 'payload-translations/react'
export function MyComponent() {
const { translations, t, formatDate } = useTranslations()
return (
<div>
<h1>{translations.home}</h1>
<button>{t('Click me', 'MyComponent')}</button>
<time>{formatDate(new Date(), 'long')}</time>
</div>
)
}
Server Components (WPML-style - same as client!):
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 (
<div>
<h1>{t('Welcome to our site', 'HomePage')}</h1>
<button>{t('Get Started', 'HomePage')}</button>
</div>
)
}
Or use direct access if you prefer:
const { translations } = await getTranslations(locale, config)
return <h1>{translations.home}</h1> // Type-safe
Go to /admin/globals/translations in your Payload admin panel and fill in translations for all locales.
ICU MessageFormat (Object):
// In your CMS, add field 'welcomeMessage' with value:
// "Welcome back, {name}!"
const { t } = useTranslations()
<h1>{t('welcomeMessage', { name: user.name })}</h1>
// Output: "Welcome back, John!"
Sprintf Style (Array):
// In your CMS, add field 'welcomeMessage' with value:
// "Welcome back, %s!"
const { t } = useTranslations()
<h1>{t('welcomeMessage', [user.name])}</h1>
// Output: "Welcome back, John!"
ICU MessageFormat (with automatic locale rules):
// In your CMS, add field 'cartItems' with value:
// "{count, plural, zero {No items} one {# item} other {# items}}"
const { t } = useTranslations()
<p>{t('cartItems', { count: 0 })}</p> // "No items"
<p>{t('cartItems', { count: 1 })}</p> // "1 item"
<p>{t('cartItems', { count: 5 })}</p> // "5 items"
Sprintf Style (simpler but manual):
// Store both singular and plural in CMS, choose manually
const { t } = useTranslations()
const count = 5
<p>{t(count === 1 ? 'item' : 'items', [count])}</p> // "5 items"
ICU MessageFormat:
// In your CMS, add field 'notification' with value:
// "{user} liked your {type}"
const { t } = useTranslations()
<p>{t('notification', { user: 'Sarah', type: 'post' })}</p>
// Output: "Sarah liked your post"
Sprintf Style:
// In your CMS, add field 'notification' with value:
// "%s liked your %s"
const { t } = useTranslations()
<p>{t('notification', ['Sarah', 'post'])}</p>
// Output: "Sarah liked your post"
The plugin is fully generic - you define all translation fields for your project:
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.
translationsPlugin(options){
// 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:
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 },
],
},
],
})
getTranslations(locale, config)Server-side function to fetch translations for a specific locale.
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)
<button>{t('Submit', 'LoginForm')}</button>
// Direct access (type-safe)
<h1>{translations.home}</h1>
Returns:
t(key, context?) - WPML-style translation functiontranslations - Raw translations objectformatDate() - Locale-aware date formattingformatNumber() - Locale-aware number formattingformatCurrency() - Locale-aware currency formattinglocale - Current locale stringuseTranslations()React hook for accessing translations in client components.
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()
t(key, contextOrVars?, vars?)WPML-style translation helper with dual interpolation support - use whichever style you prefer:
Modern, explicit approach with named placeholders:
const { t } = useTranslations()
// Simple variables
<p>{t('Welcome {name}', 'HomePage', { name: 'John' })}</p>
// Output: "Welcome John"
// Without context
<p>{t('Hello {username}', { username: 'Alice' })}</p>
// Output: "Hello Alice"
// Pluralization with automatic locale rules
<p>{t('{count, plural, one {# item} other {# items}}', 'Cart', { count: 1 })}</p>
// Output: "1 item"
<p>{t('{count, plural, one {# item} other {# items}}', 'Cart', { count: 5 })}</p>
// Output: "5 items"
// Complex pluralization
<p>{t('You have {count, plural, zero {no messages} one {# message} other {# messages}}', { count: 0 })}</p>
// Output: "You have no messages"
Familiar WordPress-style positional arguments:
// Simple string substitution
<p>{t('Welcome %s', 'HomePage', ['John'])}</p>
// Output: "Welcome John"
// Without context
<p>{t('Hello %s', ['Alice'])}</p>
// Output: "Hello Alice"
// Multiple values
<p>{t('Hello %s, you have %d new messages', ['John', 5])}</p>
// Output: "Hello John, you have 5 new messages"
// Number formatting
<p>{t('Total: %d items at $%f each', [42, 19.99])}</p>
// Output: "Total: 42 items at $19.99 each"
Format Specifiers:
%s - String%d / %i - Integer (rounds down)%f / %u - Float/NumberThe plugin automatically detects which style you're using:
{ name: 'John' } → ICU MessageFormat['John'] → Sprintf styleNo configuration needed - just use whichever style feels natural!
{variableName}{count, plural, zero {...} one {...} other {...}}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!
formatDate(date, style?)Locale-aware date formatting:
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"
formatNumber(num, options?)Locale-aware number formatting:
const { formatNumber } = useTranslations()
formatNumber(1234.56) // "1,234.56" (en) or "1.234,56" (nl)
formatNumber(0.1234, { style: 'percent' }) // "12.34%"
formatCurrency(amount, currency?)Locale-aware currency formatting:
const { formatCurrency } = useTranslations()
formatCurrency(99.99, 'EUR') // "€99.99" (en) or "€ 99,99" (nl)
formatCurrency(99.99, 'USD') // "$99.99"
The plugin includes a CLI scanner that finds all t() calls in your codebase and generates the field definitions for you:
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:
# Scan and display fields (copy-paste required)
npx payload-translations scan
# Automatically append to your translation fields file
npx payload-translations scan --write
# Specify a custom file to append to
npx payload-translations scan --write src/my-translations.ts
# Scan specific directory and auto-write
npx payload-translations scan "components/**/*.tsx" --write
How it works:
t('key') and t('key', 'Context') calls--write: Automatically appends new fields to your fileAuto-detection of translation files:
When using --write without specifying a file, the CLI looks for:
src/translations/fields.tssrc/translations/fields.jssrc/translations/config.tssrc/translations/config.jstranslations/fields.tstranslations/fields.jspnpm test
The plugin is fully typed. Your IDE will autocomplete translation keys and catch typos:
const { translations } = useTranslations()
translations.home // ✅ Valid
translations.homer // ❌ TypeScript error
To generate types for custom fields:
pnpm payload generate:types
By default, translations are fetched when pages are rendered. In production with Next.js static generation:
The plugin automatically revalidates all pages when translations change. This is enabled by default and requires zero configuration:
translationsPlugin({
revalidateOnChange: true, // ← Default! No setup needed
customFields: [
/* ... */
],
})
How it works automatically:
When you update translations in the Payload admin, the plugin:
afterChange hookrevalidatePath('/', 'layout') to revalidate all pages✨ You don't need to add any hooks or code to your app - it just works!
If you prefer manual control or aren't using Next.js:
translationsPlugin({
revalidateOnChange: false, // Disable auto-revalidation
customFields: [
/* ... */
],
})
Time-Based ISR:
// In your page/layout
export const revalidate = 3600 // Revalidate every hour
Manual On-Demand Revalidation:
// Create a webhook endpoint
import { revalidatePath } from 'next/cache'
export async function POST() {
revalidatePath('/', 'layout')
return Response.json({ revalidated: true })
}
Dynamic Rendering (always fresh):
// Force dynamic rendering for a specific page
export const dynamic = 'force-dynamic'
See the README.md for comprehensive examples and advanced usage patterns.
MIT
payload plugin dictionary
Manage frontend UI translation keys in the Payload admin instead of static JSON files
580/wk3v3
strings
Developer-defined UI strings as a localized global with scoped tabs and a typed translator helper
570/wk0v4
better localized fields
Adds locale tabs to every localized field so editors can view and edit each locale's value inline
380/wk334v3
redirects
Managed redirects collection with a cache-backed Next.js middleware for fast edge-safe matching
560/wk15v3
payload plugin translator
Translate localized Payload content with an AI provider from document, collection or field controls in the admin
550/wk29v3
menu plugin
Menu management collection with nested dropdowns, mega menus, and a public endpoint to fetch menus by slug
420/wk6v3