raisindb-translations

Installation
SKILL.md

RaisinDB Translations

How Translations Work

RaisinDB uses a file-overlay system for multi-language content:

  • Base content lives in .node.yaml and serves as the default language (typically English).
  • Translation overlays live in .node.{locale}.yaml files alongside the base file (e.g., .node.fr.yaml, .node.de.yaml).
  • Only fields marked translatable: true in their archetype or element type definition appear in translation files.
  • At query time, the server merges the base content with the requested locale's overlay.
content/launchpad/home/
  .node.yaml        # Base (English)
  .node.fr.yaml     # French overlay
  .node.de.yaml     # German overlay

Mark Fields as Translatable

Add translatable: true to fields that need translation in archetypes and element types. Fields without this flag must not appear in translation files.

Archetype (archetypes/landing-page.yaml):

fields:
  - $type: TextField
    name: title
    required: true
    translatable: true          # Translated
  - $type: TextField
    name: slug
    required: true              # NOT translated — same across locales
  - $type: TextField
    name: description
    translatable: true          # Translated
  - $type: SectionField
    name: content
    allowed_element_types: [launchpad:Hero, launchpad:TextBlock]

Element type (elementtypes/hero.yaml):

fields:
  - { $type: TextField, name: headline, translatable: true }
  - { $type: TextField, name: subheadline, translatable: true }
  - { $type: TextField, name: cta_text, translatable: true }
  - { $type: ReferenceField, name: cta_link }  # NOT translated (references are structural)

Translation File Format

Translation files contain only translated properties. They omit node_type, archetype, and non-translatable fields. Section elements are matched by uuid.

Base file (.node.yaml):

node_type: launchpad:Page
archetype: launchpad:LandingPage
properties:
  title: Welcome to Launchpad
  slug: home
  description: Your gateway to launching amazing projects
  content:
    - uuid: hero-1
      element_type: launchpad:Hero
      headline: Launch Your Vision
      subheadline: Build, deploy, and scale your ideas with Launchpad
      cta_text: Get Started
      cta_link:
        raisin:ref: /launchpad/contact
        raisin:workspace: launchpad
    - uuid: intro-1
      element_type: launchpad:TextBlock
      heading: Why Launchpad?
      content: |
        Launchpad is your all-in-one platform for turning ideas into reality.

French (.node.fr.yaml):

title: Bienvenue sur Launchpad
description: Votre passerelle pour lancer des projets exceptionnels
content:
  - uuid: hero-1
    headline: Lancez votre vision
    subheadline: Construisez, deployez et faites evoluer vos idees avec Launchpad
    cta_text: Commencer
  - uuid: intro-1
    heading: Pourquoi Launchpad ?
    content: |
      Launchpad est votre plateforme tout-en-un pour concretiser vos idees.

German (.node.de.yaml):

title: Willkommen bei Launchpad
description: Ihr Tor zum Start grossartiger Projekte
content:
  - uuid: hero-1
    headline: Starten Sie Ihre Vision
    subheadline: Bauen, deployen und skalieren Sie Ihre Ideen mit Launchpad
    cta_text: Jetzt starten
  - uuid: intro-1
    heading: Warum Launchpad?
    content: |
      Launchpad ist Ihre All-in-One-Plattform, um Ideen in die Realitat umzusetzen.

Key rules:

  • No node_type or archetype -- those belong only in the base file.
  • No non-translatable fields (slug, cta_link, etc.).
  • uuid must match the base file exactly; element_type can be omitted.

Repeatable CompositeField Translations

When a CompositeField has multiple: true and contains sub-fields marked translatable: true, each item in the array must have a uuid — in both the base content and translation overlays. This allows the server to merge only translatable fields per-item instead of replacing the entire array (which would lose non-translatable fields).

Element type (elementtypes/feature-grid.yaml):

fields:
  - $type: TextField
    name: heading
    translatable: true
  - $type: CompositeField
    name: features
    multiple: true
    fields:
      - { $type: TextField, name: title, required: true, translatable: true }
      - { $type: TextField, name: description, required: true, translatable: true }
      - { $type: TextField, name: icon }  # NOT translatable

Base content (.node.yaml) — each composite item needs a uuid:

content:
  - uuid: features-1
    element_type: launchpad:FeatureGrid
    heading: Features
    features:
      - uuid: feat-fast
        icon: zap
        title: Fast Development
        description: Build and iterate quickly
      - uuid: feat-scale
        icon: trending-up
        title: Scalable
        description: Grows with your needs

Translation overlay (.node.fr.yaml) — only translatable fields + uuid:

content:
  - uuid: features-1
    heading: Fonctionnalites
    features:
      - uuid: feat-fast
        title: Developpement rapide
        description: Construisez et iterez rapidement
      - uuid: feat-scale
        title: Evolutif
        description: Grandit avec vos besoins

The icon field is NOT in the translation because it is not translatable. The server preserves it from the base content during merge. Without UUIDs on composite items, the validator will reject the content with COMPOSITE_MISSING_UUID.

Rule: If a repeatable CompositeField has ANY sub-field with translatable: true, every item must have a unique uuid.

Frontend Locale Store

Track the active language and generate SQL clauses. The key function is localeClause():

// lib/stores/locale.ts
export type Locale = 'en' | 'de' | 'fr';

export const locale = writable<Locale>(getInitialLocale());

export function getCurrentLocale(): Locale {
  if (browser) {
    const stored = localStorage.getItem(STORAGE_KEY);
    if (stored === 'de' || stored === 'fr') return stored;
  }
  return 'en';
}

/**
 * Returns a SQL AND clause for the current locale.
 * English (default) returns empty string — no filtering needed.
 */
export function localeClause(): string {
  const current = getCurrentLocale();
  if (current === 'en') return '';
  return `AND locale = '${current}'`;
}

The default locale (English) returns an empty string so the base content is used without filtering.

Querying with Locale

Append localeClause() to any SQL query that returns translatable content:

import { localeClause } from '$lib/stores/locale';

export async function getPageByPath(path: string): Promise<PageNode | null> {
  const sql = `
    SELECT id, path, name, node_type, archetype, properties
    FROM ${WORKSPACE_NAME}
    WHERE path = $1 ${localeClause()}
    LIMIT 1
  `;
  return queryOne<PageNode>(sql, [nodePath]);
}

export async function getNavigation(): Promise<NavItem[]> {
  const sql = `
    SELECT id, path, name, node_type, properties
    FROM ${WORKSPACE_NAME}
    WHERE CHILD_OF('/${WORKSPACE_NAME}')
      AND node_type = 'launchpad:Page'
      ${localeClause()}
  `;
  return query<NavItem>(sql);
}

The server merges the locale overlay onto the base content before returning results.

Supported Locales

RaisinDB uses BCP 47 language codes: en, fr, de, es, pt-BR, zh-Hans, ja, ko, ar, it, and any valid BCP 47 code. Add a new locale by creating .node.{locale}.yaml files alongside your base content.

Validation

MANDATORY — run after every translation file change:

npm run validate

Common errors:

Error Cause Fix
TRANSLATION_FIELD_NOT_TRANSLATABLE Translation includes a non-translatable field Remove the field or add translatable: true to the type definition
TRANSLATION_MISSING_UUID Element uuid has no match in the base file Ensure uuid matches an element in .node.yaml
TRANSLATION_INVALID_LOCALE Invalid BCP 47 code in filename Use a valid locale code (e.g., fr, de, pt-BR)
COMPOSITE_MISSING_UUID Repeatable composite with translatable sub-fields has an item without uuid Add a unique uuid to each item in the composite array
COMPOSITE_DUPLICATE_UUID Two items in the same composite array share the same uuid Ensure each item has a unique uuid value
Related skills
Installs
3
GitHub Stars
1
First Seen
Apr 3, 2026