Payload CMSInternationalizationi18nMultilingualLocalization

Localizing Payload Blocks: Building Multilingual Themes

Learn how to implement internationalization (i18n) in Payload CMS with localized blocks, multilingual content management, and theme localization strategies.

ByDevelopment Team
7 min read

Localizing Payload Blocks: Building Multilingual Themes

Creating multilingual websites with Payload CMS opens up your content to global audiences. This guide covers implementing internationalization (i18n) with localized blocks, managing multilingual content, and building themes that adapt to different languages and cultures.

Setting Up Localization in Payload

1. Basic Localization Configuration

// payload.config.ts
import { buildConfig } from 'payload/config'

export default buildConfig({
  localization: {
    locales: [
      {
        label: 'English',
        code: 'en',
      },
      {
        label: 'Spanish',
        code: 'es',
      },
      {
        label: 'French',
        code: 'fr',
      },
      {
        label: 'German',
        code: 'de',
      },
    ],
    defaultLocale: 'en',
    fallback: true,
  },
  // ... rest of config
})

2. Collection with Localization

// collections/Pages.ts
export const Pages: CollectionConfig = {
  slug: 'pages',
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
      localized: true,
    },
    {
      name: 'slug',
      type: 'text',
      required: true,
      localized: true,
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'content',
      type: 'blocks',
      localized: true,
      blocks: [
        HeroBlock,
        ContentBlock,
        FeatureGridBlock,
      ],
    },
    {
      name: 'seo',
      type: 'group',
      localized: true,
      fields: [
        {
          name: 'title',
          type: 'text',
        },
        {
          name: 'description',
          type: 'textarea',
        },
      ],
    },
  ],
}

Creating Localized Blocks

1. Hero Block with Localization

// blocks/Hero.ts
import { Block } from 'payload/types'

export const HeroBlock: Block = {
  slug: 'hero',
  labels: {
    singular: 'Hero',
    plural: 'Heroes',
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
      localized: true,
    },
    {
      name: 'subtitle',
      type: 'textarea',
      localized: true,
    },
    {
      name: 'backgroundImage',
      type: 'upload',
      relationTo: 'media',
      // Images typically don't need localization
    },
    {
      name: 'cta',
      type: 'group',
      fields: [
        {
          name: 'text',
          type: 'text',
          localized: true,
        },
        {
          name: 'url',
          type: 'text',
          localized: true, // URLs might differ per locale
        },
        {
          name: 'style',
          type: 'select',
          options: [
            { label: 'Primary', value: 'primary' },
            { label: 'Secondary', value: 'secondary' },
          ],
        },
      ],
    },
  ],
}

2. Content Block with Rich Text

// blocks/Content.ts
export const ContentBlock: Block = {
  slug: 'content',
  labels: {
    singular: 'Content',
    plural: 'Content',
  },
  fields: [
    {
      name: 'heading',
      type: 'text',
      localized: true,
    },
    {
      name: 'content',
      type: 'richText',
      localized: true,
    },
    {
      name: 'layout',
      type: 'select',
      options: [
        { label: 'Single Column', value: 'single' },
        { label: 'Two Columns', value: 'two-column' },
      ],
      defaultValue: 'single',
    },
  ],
}

Frontend Localization Implementation

1. Next.js i18n Configuration

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  i18n: {
    locales: ['en', 'es', 'fr', 'de'],
    defaultLocale: 'en',
    localeDetection: true,
  },
}

export default nextConfig

2. Locale Context Provider

// lib/contexts/LocaleContext.tsx
import React, { createContext, useContext } from 'react'
import { useRouter } from 'next/router'

interface LocaleContextType {
  locale: string
  locales: string[]
  switchLocale: (locale: string) => void
}

const LocaleContext = createContext<LocaleContextType | undefined>(undefined)

export function LocaleProvider({ children }: { children: React.ReactNode }) {
  const router = useRouter()
  const { locale = 'en', locales = ['en'] } = router

  const switchLocale = (newLocale: string) => {
    router.push(router.asPath, router.asPath, { locale: newLocale })
  }

  return (
    <LocaleContext.Provider value={{ locale, locales, switchLocale }}>
      {children}
    </LocaleContext.Provider>
  )
}

export const useLocale = () => {
  const context = useContext(LocaleContext)
  if (!context) {
    throw new Error('useLocale must be used within LocaleProvider')
  }
  return context
}

3. Localized Block Components

// components/blocks/HeroBlock.tsx
import React from 'react'
import { Button } from '@/components/ui/button'

interface HeroBlockProps {
  title: string
  subtitle?: string
  backgroundImage?: {
    url: string
    alt: string
  }
  cta?: {
    text: string
    url: string
    style: 'primary' | 'secondary'
  }
}

export function HeroBlock({ title, subtitle, backgroundImage, cta }: HeroBlockProps) {
  return (
    <section className="relative h-screen flex items-center justify-center">
      {backgroundImage && (
        <div 
          className="absolute inset-0 bg-cover bg-center"
          style={{ backgroundImage: `url(${backgroundImage.url})` }}
        />
      )}
      <div className="relative z-10 text-center max-w-4xl mx-auto px-4">
        <h1 className="text-4xl md:text-6xl font-bold mb-6 text-white">
          {title}
        </h1>
        {subtitle && (
          <p className="text-xl md:text-2xl mb-8 text-white/90">
            {subtitle}
          </p>
        )}
        {cta && (
          <Button
            size="lg"
            variant={cta.style === 'primary' ? 'default' : 'secondary'}
            asChild
          >
            <a href={cta.url}>{cta.text}</a>
          </Button>
        )}
      </div>
    </section>
  )
}

Language Switcher Component

// components/LanguageSwitcher.tsx
import React from 'react'
import { useLocale } from '@/lib/contexts/LocaleContext'
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'

const localeNames = {
  en: 'English',
  es: 'Español',
  fr: 'Français',
  de: 'Deutsch',
}

export function LanguageSwitcher() {
  const { locale, locales, switchLocale } = useLocale()

  return (
    <Select value={locale} onValueChange={switchLocale}>
      <SelectTrigger className="w-32">
        <span>{localeNames[locale as keyof typeof localeNames]}</span>
      </SelectTrigger>
      <SelectContent>
        {locales.map((loc) => (
          <SelectItem key={loc} value={loc}>
            {localeNames[loc as keyof typeof localeNames]}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}

Fetching Localized Content

// lib/api.ts
export async function getLocalizedPage(slug: string, locale: string) {
  const payload = await getPayloadClient()
  
  const pages = await payload.find({
    collection: 'pages',
    where: {
      slug: {
        equals: slug,
      },
    },
    locale,
    fallbackLocale: 'en',
  })
  
  return pages.docs[0] || null
}

// Usage in pages
export async function getStaticProps({ params, locale }: GetStaticPropsContext) {
  const page = await getLocalizedPage(params?.slug as string, locale || 'en')
  
  if (!page) {
    return { notFound: true }
  }
  
  return {
    props: {
      page,
      locale,
    },
    revalidate: 60,
  }
}

Advanced Localization Features

1. RTL Language Support

// components/Layout.tsx
import { useLocale } from '@/lib/contexts/LocaleContext'

const rtlLocales = ['ar', 'he', 'fa']

export function Layout({ children }: { children: React.ReactNode }) {
  const { locale } = useLocale()
  const isRTL = rtlLocales.includes(locale)
  
  return (
    <div dir={isRTL ? 'rtl' : 'ltr'} className={isRTL ? 'font-arabic' : ''}>
      {children}
    </div>
  )
}

2. Date and Number Formatting

// lib/formatters.ts
export function formatDate(date: string, locale: string): string {
  return new Date(date).toLocaleDateString(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  })
}

export function formatCurrency(amount: number, locale: string, currency: string): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  }).format(amount)
}

3. Localized URL Structure

// lib/url-helpers.ts
export function getLocalizedPath(path: string, locale: string): string {
  if (locale === 'en') return path
  return `/${locale}${path}`
}

export function generateAlternateLinks(path: string, locales: string[]) {
  return locales.map(locale => ({
    hrefLang: locale,
    href: `${process.env.NEXT_PUBLIC_SITE_URL}${getLocalizedPath(path, locale)}`
  }))
}

Best Practices

1. Content Fallback Strategy

// Always provide fallback content
{
  name: 'title',
  type: 'text',
  required: true,
  localized: true,
  admin: {
    description: 'Falls back to default locale if not provided',
  },
}

2. Translation Management

// Create a translation status field
{
  name: 'translationStatus',
  type: 'select',
  options: [
    { label: 'Not Started', value: 'not-started' },
    { label: 'In Progress', value: 'in-progress' },
    { label: 'Complete', value: 'complete' },
    { label: 'Needs Review', value: 'needs-review' },
  ],
  defaultValue: 'not-started',
  admin: {
    position: 'sidebar',
  },
}

3. SEO for Multilingual Sites

// Generate hreflang tags
export function generateHrefLangTags(slug: string, locales: string[]) {
  return locales.map(locale => (
    <link
      key={locale}
      rel="alternate"
      hrefLang={locale}
      href={`${process.env.NEXT_PUBLIC_SITE_URL}/${locale}/${slug}`}
    />
  ))
}

Conclusion

Implementing multilingual support in Payload CMS requires careful planning of your content structure, block design, and frontend implementation. By leveraging Payload's built-in localization features and following best practices for internationalization, you can create websites that serve global audiences effectively.

Key takeaways:

  • Use Payload's localization config for consistent multilingual support
  • Implement proper fallback strategies for missing translations
  • Consider cultural differences beyond just language
  • Use proper URL structures for SEO
  • Test thoroughly with real content in multiple languages

This completes our series on Payload CMS. You now have the knowledge to build powerful, flexible, and globally accessible websites with modern CMS capabilities.

Tagged with:Payload CMSInternationalizationi18nMultilingualLocalization
More Articles