Copied to clipboard!

Vibe CMS is built from the ground up to support multiple languages and locales. This chapter covers how to set up, manage, and publish content in multiple languages.

Locale Setup

What is a Locale?

A locale represents a specific language and region combination. Vibe CMS uses BCP 47 format for locale codes:

  • Language code (required): Two-letter ISO 639-1 code

    • en = English
    • fr = French
    • es = Spanish
    • de = German
    • zh = Chinese
    • ar = Arabic
  • Region code (optional): Two-letter ISO 3166-1 code

    • US = United States
    • GB = United Kingdom
    • CA = Canada
    • CN = China
    • TW = Taiwan

Examples:

  • en = English (generic)
  • en-US = English (United States)
  • en-GB = English (United Kingdom)
  • zh-CN = Chinese (Simplified, China)
  • zh-TW = Chinese (Traditional, Taiwan)
  • pt-BR = Portuguese (Brazil)
  • pt-PT = Portuguese (Portugal)

Creating Locales

Via Admin Dashboard:

  1. Navigate to Project SettingsLanguages
  2. Click Add Language
  3. Select from common locales or enter custom BCP 47 code
  4. Set a display name (e.g., "English (US)")
  5. Optionally set as default locale
  6. Click Create

Via API:

const cms = createVibeCMS({ projectId: 'xxx' });

// Create a new locale
await cms.locales.create({
  locale_code: 'es-MX',
  display_name: 'Spanish (Mexico)',
  is_default: false,
  is_active: true
});

Via MCP Server:

vibe-cms-mcp locale create
  --locale-code es-MX
  --display-name "Spanish (Mexico)"

Default Locale

Every project must have exactly one default locale. The default locale:

  • Is required for all content items
  • Used as fallback when content unavailable in requested locale
  • Cannot be deleted while it's the default
  • Pre-populated when creating new content items

Changing default locale:

await cms.locales.setDefault('fr-FR');

Supported Locales List

Vibe CMS provides a comprehensive list of 60+ supported locales. Common ones:

European:

  • en-US, en-GB, en-CA, en-AU
  • fr-FR, fr-CA
  • es-ES, es-MX, es-US
  • de-DE, de-CH, de-AT
  • it-IT, pt-PT, pt-BR

Asian:

  • zh-CN (Simplified Chinese)
  • zh-TW (Traditional Chinese)
  • ja-JP, ko-KR, th-TH, vi-VN

Middle Eastern & Indian:

  • ar-SA (Arabic)
  • he-IL (Hebrew)
  • hi-IN, ta-IN, te-IN

Retrieving supported locales:

const locales = await cms.locales.getSupportedLocales();
console.log(locales);
// Output:
// {
//   'en-US': 'English (United States)',
//   'fr-FR': 'French (France)',
//   'es-ES': 'Spanish (Spain)',
//   ...
// }

Translation Workflow

Content Item Translations

Each content item can have translations in multiple locales. The data structure:

interface ContentItem {
  id: string;                    // Unique content item ID
  collection_id: string;
  version_id: string;            // Current draft version
  translations: {
    [locale: string]: {          // Key is locale code (e.g., 'en-US', 'fr-FR')
      locale: string;
      data: {                    // Content data
        [fieldName: string]: any;  // Fields defined in collection schema
      };
      status: 'draft' | 'published' | 'archived';
    }
  }
}

Creating Content in Multiple Languages

Step 1: Create content item in default locale

const item = await cms.collection('blog-posts').create({
  title: 'My Blog Post',
  content: 'This is the English content...',
  slug: 'my-blog-post'
});
// Returns item ID, content in default locale

Step 2: Add translations

// Add French translation
await cms.collection('blog-posts').id(item.id).addTranslation('fr-FR', {
  title: 'Mon Article de Blog',
  content: 'Ceci est le contenu en français...',
  slug: 'mon-article-de-blog'
});

// Add Spanish translation
await cms.collection('blog-posts').id(item.id).addTranslation('es-ES', {
  title: 'Mi Artículo de Blog',
  content: 'Este es el contenido en español...',
  slug: 'mi-articulo-de-blog'
});

Step 3: Publish when all translations ready

// Publish all translations at once
await cms.collection('blog-posts').id(item.id).publish();

Editing Translations

// Update only the French translation
await cms.collection('blog-posts')
  .id(item.id)
  .updateTranslation('fr-FR', {
    title: 'Mon Article Mis à Jour'
  });

Querying by Locale

// Get content in specific locale
const frenchPost = await cms.collection('blog-posts')
  .slug('my-blog-post')
  .locale('fr-FR');

// Get all posts in French
const frenchPosts = await cms.collection('blog-posts')
  .locale('fr-FR')
  .all();

// Get first item in German
const germanContent = await cms.collection('landing-hero')
  .locale('de-DE')
  .first();

Locale Fallback Strategy

What is Fallback?

Locale fallback defines what happens when requested content isn't available in the requested locale. This provides graceful degradation and ensures users always see content.

Fallback Chain

Vibe CMS uses a hierarchical fallback chain:

  1. Requested locale (e.g., fr-CA)
  2. Language base (e.g., fr if requested fr-CA)
  3. Project default locale (e.g., en-US)
  4. Global fallback (always en-US)

Example:

User requests: fr-CA (French Canadian)
Content available in: en-US, fr-FR

Fallback chain:
1. Try fr-CA (not available) ✗
2. Try fr (maps to fr-FR) ✓ Use French content

Another example:

User requests: pt-PT (Portuguese Portugal)
Content available in: en-US, pt-BR

Fallback chain:
1. Try pt-PT (not available) ✗
2. Try pt (maps to pt-BR) ✓ Use Brazilian Portuguese

Configuring Fallback

In project settings:

await cms.projects.updateSettings('project-id', {
  locale_fallback: {
    enabled: true,
    strategy: 'hierarchical',  // or 'explicit'
    explicit_map: {
      'fr-CA': 'fr-FR',      // French (Canada) → French (France)
      'pt-PT': 'pt-BR',      // Portuguese → Brazilian Portuguese
      'zh-HK': 'zh-TW'       // Hong Kong → Traditional Chinese
    }
  }
});

SDK Fallback Handling

const cms = createVibeCMS({
  projectId: 'xxx',
  fallbackLocale: 'en-US',   // Default fallback
  fallbackChain: ['fr-CA', 'fr', 'en-US']  // Custom chain
});

// SDK automatically uses fallback if requested locale unavailable
const content = await cms.collection('posts')
  .slug('my-post')
  .locale('fr-CA');  // Returns fr-FR or en-US if fr-CA unavailable

Language-Specific Fields

Some content might need different field values per language.

Example: Slug Translation

URL slugs should be localized:

English (en-US): /blog/my-post
French (fr-FR): /blog/mon-article
German (de-DE): /blog/mein-artikel

In collection schema:

{
  name: 'slug',
  type: 'text',
  translatable: true,  // Allow per-locale values
  unique_per_locale: true,  // Each locale has unique slug
}

In content:

await cms.collection('blog-posts').create({
  // These fields vary per locale
  title: 'My Post',
  slug: 'my-post',
  content: '...'
});

await cms.collection('blog-posts').id(item.id).addTranslation('fr-FR', {
  title: 'Mon Article',
  slug: 'mon-article',  // Different slug per locale
  content: '...'
});

Non-Translatable Fields

Some fields should be shared across all locales:

{
  name: 'featured_image',
  type: 'file',
  translatable: false,  // Same image for all languages
}
// Set on default locale, used by all locales
await cms.collection('blog-posts').create({
  featured_image: 'image-asset-id',
  title: 'English title'
});

// French translation
await cms.collection('blog-posts').id(item.id).addTranslation('fr-FR', {
  title: 'Titre français'
  // featured_image inherited from default locale
});

Content Publishing & Locales

Publishing All Translations at Once

// Publish entire content item (all locales)
await cms.collection('blog-posts').id(item.id).publish();

Publishing Specific Locales

// Publish only English
await cms.collection('blog-posts')
  .id(item.id)
  .locale('en-US')
  .publish();

// Can have different publish status per locale
// en-US = published
// fr-FR = draft
// de-DE = published

Check Translation Completeness

const item = await cms.collection('blog-posts').id(item.id);

console.log(item.translations);
// {
//   'en-US': { status: 'published', ... },
//   'fr-FR': { status: 'draft', ... },
//   'de-DE': { status: 'draft', ... }
// }

// Get publishing status summary
const status = await cms.collection('blog-posts')
  .id(item.id)
  .getTranslationStatus();

console.log(status);
// {
//   total_locales: 3,
//   locales_with_content: 3,
//   published_locales: 1,
//   draft_locales: 2,
//   publication_ready: false  // Not all locales published
// }

RTL (Right-to-Left) Language Support

Vibe CMS supports right-to-left languages:

  • Arabic (ar-*)
  • Hebrew (he-IL)
  • Farsi/Persian (fa-IR)
  • Urdu (ur-PK)

RTL Content Handling

The SDK automatically detects RTL locales:

const locale = 'ar-SA';  // Arabic (Saudi Arabia)

const isRTL = cms.isRTLLocale(locale);  // true
const direction = cms.getLocaleDirection(locale);  // 'rtl'

Frontend RTL Implementation

In Astro:

---
const currentLocale = 'ar-SA';
const isRTL = ['ar', 'he', 'fa', 'ur'].some(lang => 
  currentLocale.startsWith(lang)
);
---

<html dir={isRTL ? 'rtl' : 'ltr'} lang={currentLocale}>
  <body>
    {/* Content automatically adapts to RTL */}
  </body>
</html>

CSS for RTL:

/* Use logical CSS properties */
body {
  margin-inline-start: 1rem;  /* Right margin in RTL, left in LTR */
  padding-inline-end: 1rem;   /* Left padding in RTL, right in LTR */
  text-align: start;           /* Right-aligned in RTL, left in LTR */
}

Auto-Translation (AI-Powered)

Vibe CMS includes optional AI-powered translation:

Enable Auto-Translation

await cms.projects.enableAutoTranslation('project-id', {
  provider: 'openai',  // or 'anthropic'
  model: 'gpt-4',
  auto_translate_fields: ['title', 'description', 'content'],
  skip_fields: ['slug'],  // Never auto-translate slugs
});

Using Auto-Translation

// Create content in English
const item = await cms.collection('blog-posts').create({
  title: 'My Blog Post',
  content: 'This is the English content...'
});

// Auto-translate to French
await cms.collection('blog-posts')
  .id(item.id)
  .autoTranslate({
    target_locales: ['fr-FR', 'de-DE', 'es-ES'],
    provider: 'openai'
  });

// Translations now in draft status - review before publishing

Manual Review Workflow

// Get auto-translated draft
const frenchDraft = await cms.collection('blog-posts')
  .id(item.id)
  .locale('fr-FR');

// Review translation
console.log(frenchDraft.data.title);
// "Mon Article de Blog" (auto-translated)

// Edit if needed
await cms.collection('blog-posts')
  .id(item.id)
  .updateTranslation('fr-FR', {
    title: 'Mon Article de Blog (Titre Révisé)'  // Corrected
  });

// Then publish
await cms.collection('blog-posts').id(item.id).locale('fr-FR').publish();

Multi-Region Content Strategies

Strategy 1: Single Locale per Region

One content version serves entire region:

Europe:       en-GB
North America: en-US
Latin America: es-MX
Asia:         zh-CN

Pros: Simple, scalable
Cons: Less cultural customization

Strategy 2: Locale + Content Variants

Multiple locales with region-specific content:

es-ES (Spanish Spain) - Use Spain address format
es-MX (Spanish Mexico) - Use Mexico address format

Field schema:
{
  name: 'region_code',  // Separate from locale
  type: 'select',
  options: ['us', 'eu', 'apac']
}

Implementation:

// Get content for Mexican users (Spanish language, regional formatting)
const localizedContent = await cms.collection('guides')
  .filter({
    locale: 'es-MX',
    region_code: 'latam'
  })
  .first();

Strategy 3: Fallback Hierarchy

Use fallback for graceful degradation:

Requested: de-CH (German Switzerland)
Available: de-DE (German Germany)
Fallback:  de-DE auto-selected

Configure:

await cms.projects.updateSettings('project-id', {
  locale_fallback: {
    explicit_map: {
      'de-CH': 'de-DE',     // Swiss German → German
      'en-NZ': 'en-GB',     // New Zealand → British English
      'pt-AO': 'pt-PT'      // Angola → Portugal Portuguese
    }
  }
});

Best Practices

Translation Guidelines

  1. Always translate all key fields

    • Never publish incomplete translations
    • Use draft status while translating
  2. Maintain consistent terminology

    • Create glossary for product terms
    • Share with all translators
  3. Test in actual context

    • Preview on staging site
    • Check for UI overflow in different languages
  4. Use professional translators

    • Machine translation for quick drafts only
    • Professional review before publishing
    • Native speakers for RTL languages

Content Organization

  1. Plan locales upfront

    • Decide target markets
    • Create all locales before content
  2. Translation workflow

    Create English → Auto-translate draft → Professional review → Publish
    
  3. Manage complexity

    • Start with 2-3 core locales
    • Expand gradually
    • Reuse existing translations as templates

Performance Considerations

// Efficient locale querying
// Bad: Fetch all locales then filter
const allItems = await cms.collection('posts').all();
const frenchItems = allItems.filter(item => item.locale === 'fr-FR');

// Good: Query specific locale
const frenchItems = await cms.collection('posts')
  .locale('fr-FR')
  .all();

Troubleshooting

Missing Translation Falls Back Too Far

Problem: Requesting fr-CA falls back to en-US instead of fr-FR

Solution: Configure explicit fallback mapping

await cms.projects.updateSettings('project-id', {
  locale_fallback: {
    explicit_map: {
      'fr-CA': 'fr-FR'  // Canadian French → France French
    }
  }
});

Translation Status Confusion

Problem: Not sure which translations are published

Solution: Use translation status API

const status = await cms.collection('posts')
  .id(item.id)
  .getTranslationStatus();

console.log(status);
// Shows which locales are published vs draft

RTL Content Display Issues

Problem: Arabic text displays left-to-right

Solution: Set HTML dir attribute and use logical CSS

<html dir="rtl" lang="ar-SA">
  <!-- CSS with logical properties -->
</html>

Next Steps

Now that you understand multi-language content:

  • Set up locales for your target markets
  • Create content in your default locale first
  • Add translations using auto-translation or professional translators
  • Configure locale fallback for graceful degradation
  • Test RTL languages on actual devices

Need help? Join our Discord community or check the Locales API Reference.