Skip to content

Localization & Multi-Language

Build multi-language websites with BlocksWeb's built-in localization system. Manage translations for 76+ locales directly in the visual editor.

How Localization Works

BlocksWeb automatically handles translations for your content. Content editors can translate text without code changes.

What Can Be Translated

Only two option types are translatable:

  • type: 'text' - Short text fields
  • type: 'richtext' - Formatted content

Other option types (images, colors, numbers, etc.) are not locale-specific - they remain the same across all languages.

Component Schema

typescript
import { IBlockswebComponent } from '@blocksweb/core';

const Hero: IBlockswebComponent = ({ title, subtitle, image }) => {
  return (
    <section>
      <img src={image} alt="" />
      <h1>{title}</h1>
      <div dangerouslySetInnerHTML={{ __html: subtitle }} />
    </section>
  );
};

Hero.schema = {
  displayName: 'Hero Section',
  options: [
    {
      type: 'text',        // ✅ Translatable
      name: 'title',
      label: 'Title',
      default: 'Welcome'
    },
    {
      type: 'richtext',    // ✅ Translatable
      name: 'subtitle',
      label: 'Subtitle',
      default: '<p>Get started today</p>'
    },
    {
      type: 'image',       // ❌ NOT translatable
      name: 'image',
      label: 'Background Image',
      default: '/hero.jpg'
    }
  ]
};

export default Hero;

Page Content Structure

BlocksWeb stores translations in a structured format:

typescript
{
  blocks: [
    {
      id: "hero-1",
      type: "Hero Section",
      props: {
        title: "Welcome",           // Default values
        subtitle: "<p>Get started</p>",
        image: "/hero.jpg"
      }
    }
  ],
  locales: {
    DEFAULT: {
      "hero-1": {
        title: "Welcome",
        subtitle: "<p>Get started today</p>"
      }
    },
    FR: {
      "hero-1": {
        title: "Bienvenue",
        subtitle: "<p>Commencez aujourd'hui</p>"
      }
    },
    ES: {
      "hero-1": {
        title: "Bienvenido",
        subtitle: "<p>Comience hoy</p>"
      }
    }
  }
}

How It Works

  1. DEFAULT locale always exists - this is the fallback
  2. Each locale (FR, ES, etc.) stores translations per block
  3. Only text/richtext fields are stored in locales
  4. BlocksWeb automatically resolves the correct translation when rendering

Using the Editor

Adding Locales

  1. Click the locale selector in the editor toolbar
  2. Click "Add Locales"
  3. Select from 76+ supported locales
  4. Click "Add Selected Locales"

Supported Locales

BlocksWeb supports 76 locales including:

  • en-US - English (United States)
  • en-GB - English (United Kingdom)
  • es-ES - Spanish (Spain)
  • fr-FR - French (France)
  • de-DE - German (Germany)
  • it-IT - Italian (Italy)
  • pt-BR - Portuguese (Brazil)
  • ja-JP - Japanese (Japan)
  • zh-CN - Chinese (Simplified)
  • nl-NL - Dutch (Netherlands)
  • ar-SA - Arabic (Saudi Arabia)
  • ru-RU - Russian (Russia)
  • ko-KR - Korean (South Korea)
  • And 63 more...

Translation Panel

The Translation Panel appears when you select a component:

Features:

  • Current Language Selector - Switch between locales
  • Translatable Content List - Shows all text/richtext fields
  • Default Value Reference - See the DEFAULT locale value
  • Copy from Default - Quick copy button
  • Translation Status Badges:
    • Translated ✓ - Has translation
    • Missing ⚠ - No translation, using default
    • Empty - No content

Workflow:

  1. Select a component with text fields
  2. Switch to target locale (e.g., FR)
  3. See which fields need translation
  4. Click "Copy from Default" if you want to start with the default text
  5. Edit the translation
  6. Changes auto-save

Switching Locales

Use the locale selector in the editor toolbar to:

  • Preview your site in different languages
  • Edit translations for the selected locale
  • See which content needs translation

Fallback Chain

BlocksWeb uses a smart fallback system:

Current Locale → DEFAULT Locale → Schema Default → Empty String

Example:

typescript
// Component schema default
{ type: 'text', name: 'title', default: 'Welcome' }

// Page locales
{
  DEFAULT: { 'block-1': { title: 'Welcome to Our Site' } },
  FR: { 'block-1': { title: 'Bienvenue' } },
  ES: { 'block-1': {} }  // No Spanish translation
}

// What users see:
// EN: "Welcome to Our Site"  (from DEFAULT locale)
// FR: "Bienvenue"            (from FR locale)
// ES: "Welcome to Our Site"  (falls back to DEFAULT)
// IT: "Welcome to Our Site"  (falls back to DEFAULT, IT not defined)

Automatic Resolution

BlocksWeb automatically resolves translations:

typescript
// You don't write this code - BlocksWeb does it for you!
function resolveLocalizedProps(block, locales, currentLocale) {
  let props = { ...block.props };

  Object.keys(props).forEach((propName) => {
    const option = findOption(block.type, propName);

    // Only resolve text and richtext
    if (!['text', 'richtext'].includes(option.type)) {
      return;
    }

    // Fallback chain
    if (locales[currentLocale]?.[block.id]?.[propName]) {
      props[propName] = locales[currentLocale][block.id][propName];
    } else if (locales.DEFAULT?.[block.id]?.[propName]) {
      props[propName] = locales.DEFAULT[block.id][propName];
    } else {
      props[propName] = option.default || '';
    }
  });

  return props;
}

Your component simply receives the correct props:

typescript
const Hero = ({ title, subtitle }) => {
  // title and subtitle are already translated!
  return (
    <section>
      <h1>{title}</h1>
      <p>{subtitle}</p>
    </section>
  );
};

RichText Component

The RichText utility component handles translations automatically:

typescript
import { RichText } from '@blocksweb/core/client';

const BlogPost: IBlockswebComponent = ({ content }) => {
  return (
    <article>
      <RichText
        propName="content"
        text={content}  // Already translated by BlocksWeb!
      />
    </article>
  );
};

BlogPost.schema = {
  displayName: 'Blog Post',
  options: [
    {
      type: 'richtext',
      name: 'content',
      label: 'Post Content',
      default: '<p>Write your post...</p>'
    }
  ]
};

Collections & Localization

Important: Collections are NOT locale-aware. They store data independently of the page locale system.

Option 1: Manual Locale Fields

Create separate fields for each language:

typescript
const productCollection: CollectionDefinition = {
  name: 'products',
  displayName: 'Products',
  fields: [
    { name: 'nameEN', type: 'string', label: 'Name (English)' },
    { name: 'nameFR', type: 'string', label: 'Name (French)' },
    { name: 'nameES', type: 'string', label: 'Name (Spanish)' },
    { name: 'descriptionEN', type: 'richtext', label: 'Description (EN)' },
    { name: 'descriptionFR', type: 'richtext', label: 'Description (FR)' },
    { name: 'descriptionES', type: 'richtext', label: 'Description (ES)' }
  ]
};

Component usage:

typescript
import { useEditorContext } from '@blocksweb/core/client';

const ProductCard: IBlockswebComponent = ({ product }) => {
  const { locale } = useEditorContext();

  // Select the right field based on locale
  const name = product[`name${locale}`] || product.nameEN;
  const description = product[`description${locale}`] || product.descriptionEN;

  return (
    <div>
      <h3>{name}</h3>
      <div dangerouslySetInnerHTML={{ __html: description }} />
    </div>
  );
};

Option 2: JSON Locale Field

Store all translations in a single JSON field:

typescript
const productCollection: CollectionDefinition = {
  name: 'products',
  fields: [
    { name: 'name', type: 'string', label: 'Name (structured)' },
    // Store as: { "EN": "Product", "FR": "Produit", "ES": "Producto" }
  ]
};

Component usage:

typescript
const ProductCard: IBlockswebComponent = ({ product }) => {
  const { locale } = useEditorContext();

  const nameData = JSON.parse(product.name);
  const name = nameData[locale] || nameData.EN;

  return <div><h3>{name}</h3></div>;
};

Best Practices

1. Always Provide DEFAULT Content

typescript
// ✅ Good - DEFAULT locale always has content
{
  DEFAULT: { 'block-1': { title: 'Welcome' } },
  FR: { 'block-1': { title: 'Bienvenue' } }
}

// ❌ Bad - DEFAULT locale empty
{
  DEFAULT: { 'block-1': {} },
  FR: { 'block-1': { title: 'Bienvenue' } }
}

2. Use Clear Field Labels

typescript
// ✅ Good - Clear which language
{ name: 'nameEN', label: 'Product Name (English)' }
{ name: 'nameFR', label: 'Product Name (French)' }

// ❌ Bad - Unclear
{ name: 'name1', label: 'Name 1' }
{ name: 'name2', label: 'Name 2' }

3. Test All Locales

Always test your site in:

  • DEFAULT locale (fallback)
  • All configured locales
  • A locale you haven't added yet (tests fallback)

4. Keep Non-Text Content Shared

typescript
// ✅ Good - Image is NOT in locales
{
  type: 'image',
  name: 'heroImage',
  label: 'Hero Image'
}

// ❌ Bad - Don't create multiple image fields per locale
{
  type: 'image',
  name: 'heroImageEN',
  label: 'Hero Image (English)'
}

Common Patterns

Multi-Language Navigation

typescript
const Navigation: IBlockswebComponent = ({ links }) => {
  return (
    <nav>
      {links.map((link, index) => (
        <a key={index} href={link.url}>
          {link.label}  {/* Automatically translated */}
        </a>
      ))}
    </nav>
  );
};

Navigation.schema = {
  displayName: 'Navigation',
  options: [
    {
      type: 'component',
      name: 'links',
      label: 'Navigation Links',
      multiple: true,
      allowedComponents: ['NavLink']
    }
  ]
};

// NavLink component
const NavLink: IBlockswebComponent = ({ label, url }) => {
  return <span>{label}</span>;
};

NavLink.schema = {
  displayName: 'Nav Link',
  options: [
    { type: 'text', name: 'label', label: 'Label', default: 'Home' },
    { type: 'text', name: 'url', label: 'URL', default: '/' }
  ]
};

Multi-Language Forms

typescript
const ContactForm: IBlockswebComponent = ({
  title,
  nameLabel,
  emailLabel,
  messageLabel,
  submitButton
}) => {
  return (
    <form>
      <h2>{title}</h2>
      <label>{nameLabel}</label>
      <input type="text" />
      <label>{emailLabel}</label>
      <input type="email" />
      <label>{messageLabel}</label>
      <textarea />
      <button type="submit">{submitButton}</button>
    </form>
  );
};

ContactForm.schema = {
  displayName: 'Contact Form',
  options: [
    { type: 'text', name: 'title', default: 'Contact Us' },
    { type: 'text', name: 'nameLabel', default: 'Name' },
    { type: 'text', name: 'emailLabel', default: 'Email' },
    { type: 'text', name: 'messageLabel', default: 'Message' },
    { type: 'text', name: 'submitButton', default: 'Send' }
  ]
};

// Translations in editor:
// EN: "Contact Us", "Name", "Email", "Message", "Send"
// FR: "Nous Contacter", "Nom", "E-mail", "Message", "Envoyer"
// ES: "Contáctenos", "Nombre", "Correo", "Mensaje", "Enviar"

Accessing Current Locale

Use the editor context to access the current locale:

typescript
import { useEditorContext } from '@blocksweb/core/client';

const MyComponent: IBlockswebComponent = ({ title }) => {
  const { locale } = useEditorContext();

  return (
    <div>
      <p>Current locale: {locale}</p>
      <h1>{title}</h1>
    </div>
  );
};

Next Steps

Key Takeaways

✅ Only text and richtext options are translatable ✅ DEFAULT locale is always the fallback ✅ BlocksWeb automatically resolves translations ✅ 76+ locales supported out of the box ✅ Translation Panel makes managing translations easy ✅ Collections are NOT locale-aware (use manual fields) ✅ Components receive translated props automatically