Multilingual Support
Multilingual Support
Section titled âMultilingual SupportâStelo CMS provides comprehensive multilingual support through a JSON-based localization system that enables you to create content in multiple languages while maintaining type safety and performance.
Architecture Overview
Section titled âArchitecture OverviewâLocalization Strategy
Section titled âLocalization StrategyâStelo CMS uses a field-level localization approach where each content field can contain translations for multiple locales:
// Database schemamodel Page { id String @id @default(cuid()) title Json // { "en": "Welcome", "fr": "Bienvenue", "es": "Bienvenido" } content Json // Localized rich content slug Json // { "en": "welcome", "fr": "bienvenue", "es": "bienvenido" }}Benefits of This Approach
Section titled âBenefits of This Approachâ- Type Safety: Full TypeScript support for all localized content
- Performance: No joins required - all translations in single record
- Flexibility: Different content structures per language
- Atomic Updates: All translations updated together
- Version Control: Complete translation history
Supported Locales
Section titled âSupported LocalesâDefault Configuration
Section titled âDefault Configurationâexport const supportedLocales = ['en', 'fr', 'es'] as const;export type Locale = typeof supportedLocales[number];
export const localeConfig = { en: { label: 'English', flag: 'đșđž', direction: 'ltr', dateFormat: 'MM/dd/yyyy', currency: 'USD' }, fr: { label: 'Français', flag: 'đ«đ·', direction: 'ltr', dateFormat: 'dd/MM/yyyy', currency: 'EUR' }, es: { label: 'Español', flag: 'đȘđž', direction: 'ltr', dateFormat: 'dd/MM/yyyy', currency: 'EUR' }} as const;Adding New Locales
Section titled âAdding New LocalesâTo add a new locale to your project:
- Update Configuration:
// Add to supportedLocales arrayexport const supportedLocales = ['en', 'fr', 'es', 'de'] as const;
// Add locale configurationexport const localeConfig = { // ... existing locales de: { label: 'Deutsch', flag: 'đ©đȘ', direction: 'ltr', dateFormat: 'dd.MM.yyyy', currency: 'EUR' }};- Update Zod Schemas:
export const localizedStringSchema = z.record( z.enum(['en', 'fr', 'es', 'de']), // Add new locale z.string().min(1));- Migrate Existing Content:
-- Add default values for existing contentUPDATE pagesSET title = jsonb_set(title, '{de}', title->'en')WHERE title ? 'en' AND NOT title ? 'de';Content Localization
Section titled âContent LocalizationâBasic Text Fields
Section titled âBasic Text FieldsâSimple text fields are stored as JSON objects with locale keys:
// Creating localized contentconst pageData = { title: { en: "About Our Company", fr: "Ă Propos de Notre Entreprise", es: "Acerca de Nuestra Empresa" }, slug: { en: "about-us", fr: "a-propos", es: "acerca-de" }};Rich Content Fields
Section titled âRich Content FieldsâRich content includes HTML, images, and metadata:
// Rich content structureconst richContent = { en: { body: "<h1>Welcome</h1><p>Our story began...</p>", excerpt: "Learn about our company history", featuredImage: "https://storage.com/about-en.jpg", gallery: ["img1.jpg", "img2.jpg"], metadata: { readingTime: 5, wordCount: 450 } }, fr: { body: "<h1>Bienvenue</h1><p>Notre histoire a commencé...</p>", excerpt: "Découvrez l'histoire de notre entreprise", featuredImage: "https://storage.com/about-fr.jpg", gallery: ["img1-fr.jpg", "img2-fr.jpg"], metadata: { readingTime: 5, wordCount: 480 } }};Media Localization
Section titled âMedia LocalizationâImages and files can be localized by language:
// Localized media structureconst localizedMedia = { hero: { en: { url: "https://storage.com/hero-en.jpg", alt: "Welcome to our website", caption: "Our beautiful office in New York" }, fr: { url: "https://storage.com/hero-fr.jpg", alt: "Bienvenue sur notre site", caption: "Notre beau bureau Ă Paris" } }};API Integration
Section titled âAPI IntegrationâtRPC Queries
Section titled âtRPC QueriesâAll API queries support locale parameters:
// Frontend API callsconst page = await trpc.pages.getBySlug.query({ slug: "about-us", locale: "fr" // Returns French content});
// Multiple localesconst pageAllLocales = await trpc.pages.getBySlug.query({ slug: "about-us", includeAllLocales: true // Returns all translations});Response Format
Section titled âResponse FormatâAPI responses include locale information:
// Single locale response{ id: "page_123", slug: "about-us", title: "à Propos de Notre Entreprise", content: { body: "<h1>Bienvenue</h1>...", excerpt: "Découvrez notre histoire" }, locale: "fr", lastModified: "2024-01-15T10:30:00Z"}
// Multi-locale response{ id: "page_123", translations: { en: { title: "About Our Company", content: { ... } }, fr: { title: "Ă Propos de Notre Entreprise", content: { ... } } }, defaultLocale: "en"}CMS Interface
Section titled âCMS InterfaceâLanguage Switcher
Section titled âLanguage SwitcherâThe CMS interface includes a language switcher for content editing:
// Language switcher componentfunction LanguageSwitcher({ currentLocale, onLocaleChange }: { currentLocale: Locale; onLocaleChange: (locale: Locale) => void;}) { return ( <Select value={currentLocale} onValueChange={onLocaleChange}> {supportedLocales.map(locale => ( <SelectItem key={locale} value={locale}> {localeConfig[locale].flag} {localeConfig[locale].label} </SelectItem> ))} </Select> );}Translation Management
Section titled âTranslation ManagementâThe CMS provides tools for managing translations:
Translation Status Indicator
Section titled âTranslation Status Indicatorâ// Shows completion status for each localefunction TranslationStatus({ content }: { content: LocalizedContent }) { const completionStatus = supportedLocales.map(locale => ({ locale, complete: Boolean(content[locale]?.title && content[locale]?.body), percentage: calculateCompletionPercentage(content[locale]) }));
return ( <div className="translation-status"> {completionStatus.map(({ locale, complete, percentage }) => ( <div key={locale} className={`status-${complete ? 'complete' : 'incomplete'}`}> {localeConfig[locale].flag} {percentage}% </div> ))} </div> );}Copy From Base Language
Section titled âCopy From Base Languageâ// Copy content from default language to target languageasync function copyTranslation(entityId: string, fromLocale: Locale, toLocale: Locale) { const entity = await prisma.page.findUnique({ where: { id: entityId } });
if (!entity) throw new Error('Entity not found');
const sourceContent = entity.content[fromLocale];
await prisma.page.update({ where: { id: entityId }, data: { content: { ...entity.content, [toLocale]: { ...sourceContent, // Mark as machine-translated __metadata: { translatedFrom: fromLocale, needsReview: true } } } } });}Frontend Implementation
Section titled âFrontend ImplementationâNext.js Integration
Section titled âNext.js IntegrationâConfigure Next.js for internationalization:
const nextConfig = { i18n: { locales: ['en', 'fr', 'es'], defaultLocale: 'en', localeDetection: true }};Middleware Setup
Section titled âMiddleware Setupâimport createMiddleware from 'next-intl/middleware';
export default createMiddleware({ locales: ['en', 'fr', 'es'], defaultLocale: 'en', // Redirect strategy localePrefix: 'as-needed' // Only add prefix for non-default locales});
export const config = { matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']};Dynamic Routing
Section titled âDynamic Routingâinterface PageProps { params: { locale: Locale; slug: string; };}
export default async function Page({ params }: PageProps) { const page = await trpc.pages.getBySlug.query({ slug: params.slug, locale: params.locale });
return ( <div> <h1>{page.title}</h1> <div dangerouslySetInnerHTML={{ __html: page.content.body }} /> </div> );}
// Generate static paths for all localesexport async function generateStaticParams() { const pages = await trpc.pages.getAll.query(); const params = [];
for (const page of pages) { for (const locale of supportedLocales) { if (page.title[locale]) { params.push({ locale, slug: page.slug[locale] }); } } }
return params;}SEO Considerations
Section titled âSEO ConsiderationsâHreflang Implementation
Section titled âHreflang Implementationâ// SEO component with hreflangfunction SEOHead({ page, currentLocale }: { page: LocalizedPage; currentLocale: Locale;}) { return ( <Head> {/* Current page */} <link rel="canonical" href={`https://example.com/${currentLocale}/${page.slug[currentLocale]}`} />
{/* Alternate language versions */} {supportedLocales.map(locale => ( page.title[locale] && ( <link key={locale} rel="alternate" hrefLang={locale} href={`https://example.com/${locale}/${page.slug[locale]}`} /> ) ))}
{/* Default language fallback */} <link rel="alternate" hrefLang="x-default" href={`https://example.com/${page.slug.en}`} /> </Head> );}Sitemap Generation
Section titled âSitemap Generationâ// Generate multilingual sitemapexport async function generateSitemap() { const pages = await trpc.pages.getPublished.query(); const urls = [];
for (const page of pages) { for (const locale of supportedLocales) { if (page.title[locale] && page.slug[locale]) { urls.push({ url: `https://example.com/${locale}/${page.slug[locale]}`, lastModified: page.updatedAt, alternateRefs: supportedLocales .filter(l => page.title[l]) .map(l => ({ href: `https://example.com/${l}/${page.slug[l]}`, hreflang: l })) }); } } }
return urls;}Performance Optimization
Section titled âPerformance OptimizationâCaching Strategy
Section titled âCaching Strategyâ// Locale-aware cachingconst cacheKey = `page:${slug}:${locale}`;const cachedPage = await redis.get(cacheKey);
if (cachedPage) { return JSON.parse(cachedPage);}
const page = await prisma.page.findFirst({ where: { slug: { path: [locale], equals: slug } }});
await redis.setex(cacheKey, 3600, JSON.stringify(page));Database Optimization
Section titled âDatabase Optimizationâ-- Indexes for localized queriesCREATE INDEX idx_pages_title_en ON pages USING gin ((title->'en'));CREATE INDEX idx_pages_title_fr ON pages USING gin ((title->'fr'));CREATE INDEX idx_pages_slug_en ON pages USING gin ((slug->'en'));
-- Partial index for published contentCREATE INDEX idx_pages_published_locale ON pages (published)WHERE published = true;Best Practices
Section titled âBest PracticesâContent Management
Section titled âContent Managementâ- Always provide default language content before adding translations
- Use consistent terminology across all languages
- Consider cultural differences beyond just language
- Plan for text expansion (German ~30% longer than English)
- Validate all locales before publishing
Technical Implementation
Section titled âTechnical Implementationâ- Use TypeScript unions for locale types
- Validate locale data with Zod schemas
- Handle missing translations gracefully
- Cache aggressively but invalidate properly
- Monitor translation coverage in analytics
SEO Optimization
Section titled âSEO Optimizationâ- Implement proper hreflang tags
- Create locale-specific sitemaps
- Use local domain structures when possible
- Optimize for local search engines
- Consider regional keyword variations
This multilingual system provides a robust foundation for international websites while maintaining the performance and type safety that makes Stelo CMS powerful for developers.