Features

Internationalization (i18n)

Add multi-language support to your Crouton collections with automatic translation handling
Status: Stable

Add comprehensive multi-language support to your Nuxt Crouton applications with the @fyit/crouton-i18n addon package. This package provides team-specific translation overrides, development tools, and automatic locale management.

Quick Start

Installation

First, install the i18n package:

pnpm add @fyit/crouton-i18n

Then add it to your Nuxt config:

// nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    '@fyit/crouton',
    '@fyit/crouton-i18n'
  ]
})
Important: The i18n addon is a layer extension, not a standalone package. You must extend both the base layer and the i18n addon.

Mark Fields as Translatable

Configure which fields in your collections should be translatable:

// crouton.config.js
export default {
  translations: {
    collections: {
      products: ['name', 'description'],  // These fields are translatable
      posts: ['title', 'body', 'excerpt']
    }
  }
}

Generated Forms with Translations

When you generate forms with translations enabled, Crouton automatically creates translation inputs:

<!-- Auto-generated when using --no-translations=false -->
<template>
  <UForm @submit="handleSubmit">
    <!-- Regular fields -->
    <UFormField label="SKU" name="sku">
      <UInput v-model="state.sku" />
    </UFormField>

    <!-- Translatable fields -->
    <CroutonI18nInput
      v-model="state.translations"
      :fields="['name', 'description']"
      :default-values="{
        name: state.name,
        description: state.description
      }"
    />
  </UForm>
</template>

Query with Locale

Queries automatically fetch data for the current locale:

<script setup lang="ts">
const { locale } = useI18n()

// Auto-fetches for current locale
const { items } = await useCollectionQuery('shopProducts', {
  query: computed(() => ({ locale: locale.value }))
})

// Auto-refetches when locale changes!
</script>

Display Translated Fields

Use the useEntityTranslations composable to display translated content:

<script setup lang="ts">
const { t } = useEntityTranslations()
const product = { name: 'Product', translations: { fr: { name: 'Produit' } } }
</script>

<template>
  <!-- Shows "Product" in English, "Produit" in French -->
  <div>{{ t(product, 'name') }}</div>
</template>

Package Overview

Package: @fyit/crouton-i18nVersion: 0.1.0 Type: Nuxt Layer (Addon) Dependencies: @nuxtjs/i18n v9.0.0

Features

  • 🌍 Multi-language input components with automatic locale switching
  • 🔄 Auto-sync with English as primary language
  • 📝 Team-specific translation overrides
  • 🎯 Built-in support for EN, NL, FR (configurable)
  • ⚡ Inherits all CRUD features from base layer
  • 🛠️ Development mode for inline translation editing
  • 💾 Database-backed translation management
  • 🎨 Rich text editor support for translated content

Locale File Sources

Crouton's i18n system merges translations from multiple sources. Understanding this merge order is essential for managing translations effectively.

Translation Merge Order

Translations are loaded and merged in this order (last wins for conflicting keys):

1. @fyit/crouton/i18n/locales/          (Crouton component strings)
2. @fyit/crouton-auth/i18n/locales/     (Auth strings: signIn, register, etc.)
3. @fyit/crouton-admin/i18n/locales/    (Admin strings: superAdmin, etc.)
4. @fyit/crouton-i18n/i18n/locales/     (i18n admin UI strings)
5. layers/[domain]/i18n/locales/        (Domain-specific strings)
6. app/i18n/locales/                    (App-level overrides)
7. Database (translationsUi table)      (Runtime overrides)

Translation Key Ownership

PackageKeysExample
@fyit/croutonCrouton component stringscrouton.table.search, crouton.form.save
@fyit/crouton-authAuth stringsauth.signIn, auth.register, teams.create
@fyit/crouton-adminAdmin stringssuperAdmin.dashboard, superAdmin.users
@fyit/crouton-i18ni18n admin UIi18n.admin.addKey, i18n.admin.override
layers/domainDomain-specificbookings.form.location, bookings.status.confirmed

Resolution Order (Runtime)

When $t('common.save') is called:

1. DB: Team override for current team?
   ↓ (not found)
2. DB: System default?
   ↓ (not found)
3. Locale files (merged, last wins):
   a. @fyit/crouton/i18n/locales/en.json
   b. @fyit/crouton-auth/i18n/locales/en.json
   c. layers/bookings/i18n/locales/en.json
   d. app/i18n/locales/en.json (if exists)
   ↓ (found in core)
4. Return value: "Save"

Seeding Translations to Database

The @fyit/crouton-i18n package provides a CLI command to populate your database with translations from locale files.

Seed Command

crouton-generate seed-translations

What It Does

  1. Reads locale files from all configured sources
  2. Inserts translations into the translationsUi table
  3. Sets teamId: null (system default)
  4. Sets isOverrideable: true (allows team overrides)
  5. Skips existing keys (no duplicates on re-run)

When to Run

  • Initial setup: After installing the i18n package
  • After updates: When locale files are updated
  • After adding layers: When new domain translations are added

Options

# Seed all locales (via API, requires dev server running)
crouton-generate seed-translations

# Preview translations without seeding
crouton-generate seed-translations --dry-run

# Output SQL statements instead of using API
crouton-generate seed-translations --sql

# Seed from a specific layer only
crouton-generate seed-translations --layer=shop

Default Locale Configuration

The package comes pre-configured with three locales:

// Built-in configuration
{
  locales: [
    { code: 'en', name: 'English', file: 'en.json' },
    { code: 'nl', name: 'Nederlands', file: 'nl.json' },
    { code: 'fr', name: 'Français', file: 'fr.json' }
  ],
  defaultLocale: 'en',
  strategy: 'no_prefix'
}

Customizing Locales

The recommended way to configure locales is via crouton.config.js. This flows through to the i18n layer, the CLI generator, and the language switcher UI automatically:

// crouton.config.js
export default {
  locales: ['en', 'de', 'es'],  // ISO 639-1 codes — names auto-resolved
  defaultLocale: 'en',

  // Or with explicit names:
  // locales: [
  //   { code: 'en', name: 'English' },
  //   { code: 'de', name: 'Deutsch' },
  //   { code: 'es', name: 'Español' }
  // ],

  features: { /* ... */ },
  collections: [ /* ... */ ],
  // ...
}

When no locales key is present, only ['en'] is configured by default. Add your desired locales explicitly.

You can also override locales directly in nuxt.config.ts (this takes precedence over crouton.config.js for the i18n module):

export default defineNuxtConfig({
  i18n: {
    locales: [
      { code: 'en', name: 'English', file: 'en.json' },
      { code: 'es', name: 'Español', file: 'es.json' },
      { code: 'de', name: 'Deutsch', file: 'de.json' }
    ]
  }
})

Adding Translations to Nuxt Layers

Automatic Setup: When you generate collections with translations enabled, Crouton automatically:
  • Creates the i18n/locales/ folder structure
  • Generates locale files for your configured locales
  • Adds the correct i18n config to your layer's nuxt.config.ts

If you need to manually configure i18n in a layer, use relative paths for langDir:

// layers/my-layer/nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    locales: [
      { code: 'en', file: 'en.json' },
      { code: 'nl', file: 'nl.json' },
      { code: 'fr', file: 'fr.json' }
    ],
    langDir: './locales'  // ✅ Relative to restructureDir 'i18n'
  }
})

Your locale files should be placed at:

layers/my-layer/
├── i18n/
│   └── locales/
│       ├── en.json
│       ├── nl.json
│       └── fr.json
└── nuxt.config.ts
Important: The langDir path is relative to the restructureDir (default: i18n). Use ./locales, NOT ./i18n/locales - the latter will result in a doubled path error.
Critical: Never use absolute paths for langDir in layers. Absolute paths will work in development but fail in production.
// ❌ WRONG - Will break in production
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'

const currentDir = fileURLToPath(new URL('.', import.meta.url))

export default defineNuxtConfig({
  i18n: {
    langDir: join(currentDir, 'i18n/locales')  // ❌ Absolute path
  }
})

Components Reference

The package provides components with the CroutonI18n prefix for managing translations.

CroutonI18nDisplay

Display translated content with automatic fallback to English.

Props

interface DisplayProps {
  translations: Record<string, string>  // Translation values by locale
  languages?: string[]                   // Override available languages
}

Features

  • Badge Display: Shows translation status for each language
  • Popover Preview: Displays translations up to 200 characters
  • Modal View: For longer translations
  • Copy to Clipboard: Quick copy functionality
  • Character/Word Count: Metadata display
  • Fallback Indication: Shows when using English fallback

Usage

<script setup lang="ts">
const product = {
  name: 'Product',
  translations: {
    en: 'English Product Name',
    nl: 'Nederlandse Productnaam',
    fr: 'Nom du produit français'
  }
}
</script>

<template>
  <!-- Basic usage -->
  <CroutonI18nDisplay :translations="product.translations" />

  <!-- Custom languages -->
  <CroutonI18nDisplay
    :translations="product.translations"
    :languages="['en', 'nl']"
  />
</template>

Badge Colors

  • Primary: Translation exists for this locale
  • Neutral: No translation available
  • Error: Translation validation failed

CroutonI18nInput

Multi-language input component for forms supporting both single and multi-field translations.

Props

interface InputProps {
  modelValue: SingleFieldValue | MultiFieldValue | null
  fields: string[]                          // Fields to translate
  label?: string                            // Form label
  error?: string | boolean                  // Validation error
  defaultValues?: Record<string, string>    // Default values for fallback
  fieldComponents?: Record<string, string>  // Custom components per field
  showAiTranslate?: boolean                 // Enable AI translation suggestions
  fieldType?: string                        // Field type context for AI (e.g., 'product_name', 'description')
  collab?: CollabConnection                 // Collab connection for real-time block editing via Yjs
  layout?: 'tabs' | 'side-by-side'          // Layout mode (default: 'tabs')
  primaryLocale?: string                    // Primary locale for side-by-side layout (default: 'en')
  secondaryLocale?: string                  // Secondary locale for side-by-side layout
  fieldOptions?: Record<string, FieldOptions> // Field-specific options like transforms
  fieldGroups?: Record<string, string>      // Group fields into collapsible sections (field name → group label)
  defaultOpenGroups?: string[]              // Which groups start open (default: all)
}

type SingleFieldValue = Record<string, string>
type MultiFieldValue = Record<string, Record<string, string>>

Features

  • Language Tabs: Switch between locales with visual indicators
  • Completion Status: Shows which locales are complete
  • Required Fields: English marked as required
  • Fallback Hints: Shows English text when editing other languages
  • Custom Components: Support for UInput, UTextarea, CroutonEditorSimple
  • Validation: Highlights errors for required fields

Single Field Mode

<script setup lang="ts">
const state = ref({
  name: '',
  translations: {} // { en: "English", nl: "Nederlands", fr: "Français" }
})
</script>

<template>
  <CroutonI18nInput
    v-model="state.translations"
    :fields="[]"
  />
</template>

Multi-Field Mode

<script setup lang="ts">
const state = ref({
  name: 'Product',
  description: 'Description',
  translations: {} // { en: { name: "...", description: "..." }, nl: { ... } }
})
</script>

<template>
  <CroutonI18nInput
    v-model="state.translations"
    :fields="['name', 'description']"
    :default-values="{
      name: state.name,
      description: state.description
    }"
  />
</template>

Custom Field Components

<script setup lang="ts">
const state = ref({
  title: '',
  content: '<p></p>',
  translations: {}
})
</script>

<template>
  <CroutonI18nInput
    v-model="state.translations"
    :fields="['title', 'content']"
    :default-values="{
      title: state.title,
      content: state.content
    }"
    :field-components="{
      content: 'CroutonEditorSimple',  // Rich text editor
      title: 'UInput'                   // Default input
    }"
  />
</template>

Supported Field Components:

  • UInput - Single-line text (default)
  • UTextarea - Multi-line text
  • CroutonEditorSimple - Rich text editor

CroutonI18nInputWithEditor

Specialized input component with built-in rich text editor support.

Props

interface InputWithEditorProps {
  modelValue: Record<string, string>  // Translation values
  fields: string[]                     // For backwards compatibility
  label?: string
  error?: string | boolean
  useRichText?: boolean                // Enable rich text editor
}

Features

  • Rich Text Toggle: Switch between plain text and rich editor
  • English Reference: Shows original when editing translations
  • Height Control: Fixed 256px height for editor
  • Visual Indicators: Border colors for validation states

Usage

<script setup lang="ts">
const state = ref({
  content: '<p>English content</p>',
  translations: {}
})
</script>

<template>
  <!-- With rich text editor -->
  <CroutonI18nInputWithEditor
    v-model="state.translations"
    :fields="['content']"
    :use-rich-text="true"
  />

  <!-- Plain text input -->
  <CroutonI18nInputWithEditor
    v-model="state.translations"
    :fields="['content']"
    :use-rich-text="false"
  />
</template>

CroutonI18nLanguageSwitcher

Dropdown selector for switching between available languages.

Features

  • Locale Selection: Uses Nuxt UI's USelect with ghost variant
  • Uppercase Labels: Locale codes displayed in uppercase (e.g., EN, NL, FR)
  • Locale Persistence: Saves locale preference to database for authenticated users

Usage

<template>
  <!-- Basic usage -->
  <CroutonI18nLanguageSwitcher />
</template>

The component automatically:

  1. Reads available locales from i18n config
  2. Updates the current locale on selection
  3. Persists the locale to the database if the user is authenticated
  4. Triggers reactive updates across the app

CroutonI18nLanguageSwitcherIsland

Floating language switcher for overlay/island usage.

Features

  • Fixed Position: Top-right corner (z-index 50)
  • Dropdown Menu: UDropdownMenu with custom styling
  • Dark Theme: Black background with hover effects
  • Uppercase Labels: Language codes in uppercase
  • Check Indicator: Shows current selection

Usage

<template>
  <!-- Floating switcher in top-right -->
  <CroutonI18nLanguageSwitcherIsland />
</template>

CroutonI18nUiForm

Form component for managing UI translation overrides.

Props

interface UiFormProps {
  action: 'create' | 'update' | 'delete'
  activeItem?: any                       // Item being edited
  loading?: string                       // Loading state
  collection: string                     // Collection name
}

Features

  • Delete Confirmation: Modal with warning icon
  • Translation Preview: Shows translation being deleted
  • Form Validation: Zod schema validation
  • Key Path Input: Auto-disabled when editing
  • Category Field: Organizes translations

Usage

<script setup lang="ts">
const action = ref<'create' | 'update' | 'delete'>('create')
const activeItem = ref(null)
</script>

<template>
  <CroutonI18nUiForm
    :action="action"
    :active-item="activeItem"
    collection="translationsUi"
  />
</template>

Form Fields

  1. Key Path (required): Dot-notation key (e.g., table.search)
  2. Category (required): Grouping category (e.g., table)
  3. Translations (required): Multi-language values
  4. Description (optional): Documentation for the translation

CroutonI18nUiList

List view for managing UI translations with Crouton table integration.

Features

  • CroutonTable Integration: Uses standard table component
  • Custom Cell Rendering: Special handling for translation values
  • Create Button: Built-in creation workflow
  • Auto-Refresh: Reactive updates

Default Columns

[
  { key: 'keyPath', label: 'Key Path' },
  { key: 'category', label: 'Category' },
  { key: 'values', label: 'Translations' },
  { key: 'description', label: 'Description' },
  { key: 'actions', label: 'Actions' }
]

Usage

<template>
  <CroutonI18nUiList />
</template>

The component automatically:

  • Fetches translations using useCollectionQuery('translationsUi')
  • Displays translations with CroutonI18nDisplay component
  • Provides CRUD actions via CroutonTable

CroutonI18nCardsMini

Badge component showing single locale translation status.

Props

interface CardsMiniProps {
  locale: string        // Locale code (en, nl, fr)
  hasTranslation: boolean  // Whether translation exists
}

Features

  • Color Coding: Primary for exists, neutral for missing
  • Uppercase Labels: Locale codes in uppercase
  • Small Size: Compact badge display

Usage

<template>
  <CroutonI18nCardsMini locale="en" :has-translation="true" />
  <CroutonI18nCardsMini locale="nl" :has-translation="false" />
</template>

CroutonI18nListCards

Container component showing translation status for all locales.

Props

interface ListCardsProps {
  item: any           // Entity with translations
  fields: string[]    // Fields to check for translations
}

Features

  • Multi-Locale Display: Shows all configured locales
  • Field-Based Validation: Checks if any field has translation
  • Auto-Layout: Flex gap layout

Usage

<script setup lang="ts">
const product = {
  translations: {
    en: { name: 'Product', description: 'Description' },
    nl: { name: 'Product' }  // description missing
  }
}
</script>

<template>
  <CroutonI18nListCards
    :item="product"
    :fields="['name', 'description']"
  />
  <!-- Shows: EN (complete), NL (incomplete), FR (missing) -->
</template>

CroutonI18nDevModeToggle

Development tool for managing missing translations with inline editing.

Features

  • Dev-Only: Only visible in development mode
  • Translation Scanner: Finds [key] patterns in DOM
  • Click-to-Edit: Modal for adding missing translations
  • Visual Highlighting: Red background for missing translations
  • Auto-Refresh: Updates UI after saving

Usage

<template>
  <!-- Add to your layout for dev mode -->
  <CroutonI18nDevModeToggle />
</template>

Workflow

  1. Click "Enable translation dev mode"
  2. Scanner finds all [keyPath] patterns
  3. Click any highlighted translation
  4. Modal opens for that key
  5. Enter translation
  6. Save to database
  7. Page refreshes with new translation

Composables Reference

useT()

Enhanced translation composable with team override support and development mode.

Type Signature

interface TranslationOptions {
  params?: Record<string, any>
  fallback?: string
  category?: string
  mode?: 'system' | 'team'
  placeholder?: string
}

function useT(): {
  t: (key: string, options?: TranslationOptions) => string
  tString: (key: string, options?: TranslationOptions) => string
  tContent: (entity: any, field: string, preferredLocale?: string) => string
  tInfo: (key: string, options?: TranslationOptions) => TranslationInfo
  hasTranslation: (key: string) => boolean
  getAvailableLocales: () => string[]
  getTranslationMeta: (key: string) => TranslationMeta
  refreshTranslations: () => Promise<void>
  locale: Ref<string>
  isDev: boolean
  devModeEnabled: Ref<boolean>
}

Features

  • Team Overrides: Checks team translations before system
  • Auto-Caching: Caches team translations per team/locale
  • Parameter Substitution: {key} replacement in strings
  • Fallback Support: Custom fallback values
  • Missing Indicators: Returns [key] when not found
  • Content Translation: Direct entity field translation
  • Metadata Access: Get translation info for admin UIs

Basic Usage

<script setup lang="ts">
const { t, tString, tContent } = useT()

// Basic translation
const saveLabel = t('common.save')

// With parameters
const greeting = t('messages.hello', {
  params: { name: 'John' }
})

// String-only (for non-template contexts)
const title = tString('page.title')

// Entity field translation
const productName = tContent(product, 'name')
</script>

Team Override Example

<script setup lang="ts">
const { t } = useT()
const { currentTeam } = useTeam()

// 1. Checks team translations for current team
// 2. Falls back to system translation
// 3. Returns [key] if not found
const label = t('dashboard.welcome')
</script>

<template>
  <h1>{{ label }}</h1>
</template>

Content Translation

<script setup lang="ts">
const { tContent } = useT()
const { locale } = useI18n()

const product = {
  name: 'Product',
  translations: {
    en: { name: 'English Name', description: 'English Description' },
    nl: { name: 'Nederlandse Naam' }
  }
}

// In English: "English Name"
// In Dutch: "Nederlandse Naam"
// In French: "English Name" (fallback)
const name = tContent(product, 'name')

// With explicit locale
const dutchName = tContent(product, 'name', 'nl')
</script>

useEntityTranslations()

Simple composable for translating entity fields based on current locale.

Type Signature

function useEntityTranslations(): {
  t: (entity: any, field: string) => string
}

Usage

<script setup lang="ts">
const { t } = useEntityTranslations()
const { locale } = useI18n()

const product = {
  name: 'Product',
  description: 'English description',
  translations: {
    nl: {
      name: 'Product',
      description: 'Nederlandse beschrijving'
    }
  }
}

// Automatically uses current locale
const name = t(product, 'name')
const description = t(product, 'description')
</script>

<template>
  <div>
    <h2>{{ name }}</h2>
    <p>{{ description }}</p>
  </div>
</template>

Fallback Behavior

// Priority:
// 1. translations[currentLocale][field]
// 2. entity[field] (root field value)
// 3. Empty string

const product = {
  name: 'Default Name',
  translations: {
    nl: { name: 'Nederlandse Naam' }
  }
}

// In NL locale: "Nederlandse Naam"
// In EN locale: "Default Name" (fallback to root)
// In FR locale: "Default Name" (fallback to root)
t(product, 'name')

translationsUi Exports

The useTranslationsUi file exports named constants for the translationsUi collection configuration. These are not a composable function — import the individual exports directly.

Named Exports

import {
  translationsUiSchema,
  TRANSLATIONS_UI_COLUMNS,
  TRANSLATIONS_UI_DEFAULTS,
  TRANSLATIONS_UI_PAGINATION,
  translationsUiConfig,
  TRANSLATIONS_UI_COLLECTION
} from '#imports'
  • translationsUiSchema: Zod validation schema for translation records
  • TRANSLATIONS_UI_COLUMNS: Table column definitions for CroutonTable
  • TRANSLATIONS_UI_DEFAULTS: Default form values for new translations
  • TRANSLATIONS_UI_PAGINATION: Default pagination settings
  • translationsUiConfig: Full collection configuration object (includes schema as non-enumerable property)
  • TRANSLATIONS_UI_COLLECTION: Collection name constant ('translationsUi')

Usage

<script setup lang="ts">
const state = ref({ ...TRANSLATIONS_UI_DEFAULTS })

const handleSubmit = async () => {
  const valid = translationsUiSchema.parse(state.value)
  await $fetch('/api/translations-ui', {
    method: 'POST',
    body: valid
  })
}
</script>

<template>
  <UForm :schema="translationsUiSchema" :state="state" @submit="handleSubmit">
    <UFormField label="Key Path" name="keyPath">
      <UInput v-model="state.keyPath" />
    </UFormField>
    <!-- ... -->
  </UForm>
</template>

Database Schema

translationsUi Table

The package adds a translations_ui table for storing system and team-specific translations.

{
  id: string (primary key)
  userId: string (not null)
  teamId: string (nullable)  // null = system translation
  namespace: string (default: 'ui')
  keyPath: string (not null)
  category: string (not null)
  values: Record<string, string> (JSON)
  description: string (nullable)
  isOverrideable: boolean (default: true)
  createdAt: timestamp
  updatedAt: timestamp
}

Unique Constraint

unique(teamId, namespace, keyPath)

This ensures one translation per team per key path.

Translation Types

  1. System Translations: teamId = null, isOverrideable = true
  2. Team Overrides: teamId = <team-id>

Integration Examples

Complete Product Management

Basic form setup:

<script setup lang="ts">
const props = defineProps<{ action: 'create' | 'update', item?: Product }>()
const { create, update } = useCollectionMutation('products')

const state = ref({
  sku: props.item?.sku || '',
  name: props.item?.name || '',
  price: props.item?.price || 0,
  translations: props.item?.translations || {}
})

const handleSubmit = async () => {
  if (props.action === 'create') {
    await create(state.value)
  } else {
    await update(props.item!.id, state.value)
  }
}
</script>

<template>
  <UForm :state="state" @submit="handleSubmit">
    <UFormField label="SKU" name="sku" required>
      <UInput v-model="state.sku" />
    </UFormField>

    <UFormField label="Name (English)" name="name" required>
      <UInput v-model="state.name" />
    </UFormField>

    <CroutonButton :action="action" collection="products" />
  </UForm>
</template>

With translation inputs:

<template>
  <UForm :state="state" @submit="handleSubmit">
    <!-- Default language fields -->
    <UFormField label="Name (English)" name="name" required>
      <UInput v-model="state.name" />
    </UFormField>

    <UFormField label="Description (English)" name="description">
      <UTextarea v-model="state.description" rows="3" />
    </UFormField>

    <!-- Translations component -->
    <UFormField label="Translations">
      <CroutonI18nInput
        v-model="state.translations"
        :fields="['name', 'description']"
        :default-values="{ name: state.name, description: state.description }"
        :field-components="{ description: 'UTextarea' }"
      />
    </UFormField>

    <CroutonButton :action="action" collection="products" />
  </UForm>
</template>

Product Display with Translations

<script setup lang="ts">
const { locale } = useI18n()
const { items } = await useCollectionQuery('products', {
  query: computed(() => ({
    locale: locale.value
  }))
})

const { t } = useEntityTranslations()
</script>

<template>
  <div class="grid gap-4">
    <div v-for="product in items" :key="product.id" class="card">
      <!-- Translated name -->
      <h3>{{ t(product, 'name') }}</h3>

      <!-- Translated description -->
      <p>{{ t(product, 'description') }}</p>

      <!-- Translation status badges -->
      <CroutonI18nListCards
        :item="product"
        :fields="['name', 'description']"
      />

      <!-- Non-translated fields -->
      <div class="text-sm text-gray-500">
        SKU: {{ product.sku }} | Price: {{ product.price }}
      </div>
    </div>
  </div>
</template>

Admin Translation Management

<script setup lang="ts">
// Full admin page for UI translations
definePageMeta({
  middleware: 'admin'
})
</script>

<template>
  <div class="space-y-6">
    <div class="flex justify-between items-center">
      <h1>UI Translations</h1>
      <CroutonI18nLanguageSwitcher />
    </div>

    <!-- Translation management table -->
    <CroutonI18nUiList />

    <!-- Dev mode toggle for testing -->
    <CroutonI18nDevModeToggle />
  </div>
</template>

Best Practices

1. Always Provide English

English is the fallback language - always provide English translations.

<!-- ✅ Good -->
<CroutonI18nInput
  v-model="state.translations"
  :fields="['name']"
  :default-values="{ name: state.name }"
/>

<!-- ❌ Bad - No English default -->
<CroutonI18nInput
  v-model="state.translations"
  :fields="['name']"
/>

2. Use Entity Translations for Content

For content fields, store translations in the entity:

// ✅ Good - Entity field translations
{
  id: '123',
  name: 'Product',
  translations: {
    nl: { name: 'Product' },
    fr: { name: 'Produit' }
  }
}

// ❌ Bad - Don't use UI translations for content
// UI translations are for interface labels only

3. Organize Translation Keys

Use dot notation and categories:

// ✅ Good
'common.save'
'common.cancel'
'table.search'
'table.filter'
'dashboard.title'

// ❌ Bad
'save'
'cancelButton'
'SearchTable'

4. Leverage Fallback Behavior

<script setup lang="ts">
// ✅ Good - Relies on automatic fallback
const { t } = useEntityTranslations()
const name = t(product, 'name')

// ❌ Bad - Manual fallback logic
const name = product.translations?.[locale.value]?.name || product.name || ''
</script>

5. Use Rich Text for Long Content

<!-- ✅ Good - Rich text for long content -->
<CroutonI18nInput
  v-model="state.translations"
  :fields="['content']"
  :field-components="{ content: 'CroutonEditorSimple' }"
/>

<!-- ❌ Bad - Plain text for HTML content -->
<CroutonI18nInput
  v-model="state.translations"
  :fields="['content']"
/>

6. Enable Dev Mode During Development

<template>
  <!-- ✅ Good - Always include in dev layout -->
  <CroutonI18nDevModeToggle />

  <!-- Quickly identify and fix missing translations -->
</template>

Troubleshooting

Translations Not Updating

Problem: Changes to translations don't appear

Solution: Clear cache and refresh

const { refreshTranslations } = useT()
await refreshTranslations()

Missing Translation Indicator

Problem: Seeing [key.path] instead of translation

Solutions:

  1. Check if translation exists in locale file
  2. Verify key path is correct
  3. Check team override isn't blocking system translation
  4. Use dev mode to identify missing translations
<template>
  <CroutonI18nDevModeToggle />
  <!-- Click highlighted translations to add them -->
</template>

Wrong Language Displayed

Problem: Content shows in wrong language

Solutions:

  1. Check current locale: const { locale } = useI18n()
  2. Verify query includes locale parameter
  3. Check entity has translation for that locale
<script setup lang="ts">
const { locale } = useI18n()
console.log('Current locale:', locale.value)

// Ensure query includes locale
const { items } = await useCollectionQuery('products', {
  query: computed(() => ({ locale: locale.value }))
})
</script>