Internationalization (i18n)
Add comprehensive multi-language support to your Nuxt Crouton applications with the @friendlyinternet/nuxt-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 @friendlyinternet/nuxt-crouton-i18n
Then add it to your Nuxt config:
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'@friendlyinternet/nuxt-crouton',
'@friendlyinternet/nuxt-crouton-i18n'
]
})
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: @friendlyinternet/nuxt-crouton-i18nVersion: 1.3.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. nuxt-crouton/i18n/locales/ (Crouton component strings)
2. nuxt-crouton-supersaas/i18n/locales/ (App-level strings from SuperSaaS package)
3. layers/[domain]/i18n/locales/ (Domain-specific strings)
4. app/i18n/locales/ (App-level overrides)
5. Database (translationsUi table) (Runtime overrides)
Translation Key Ownership
| Package | Keys | Example |
|---|---|---|
| nuxt-crouton | Crouton component strings | crouton.table.search, crouton.form.save |
| nuxt-crouton-supersaas | App-level strings | common.save, auth.signIn, navigation.dashboard |
| nuxt-crouton-i18n | Admin UI strings | i18n.admin.addKey, i18n.admin.override |
| layers/domain | Domain-specific | bookings.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. nuxt-crouton/i18n/locales/en.json
b. nuxt-crouton-supersaas/i18n/locales/en.json
c. layers/bookings/i18n/locales/en.json
d. app/i18n/locales/en.json (if exists)
↓ (found in supersaas)
4. Return value: "Save"
Seeding Translations to Database
The @friendlyinternet/nuxt-crouton-i18n package provides a CLI command to populate your database with translations from locale files.
Seed Command
pnpm crouton i18n seed
What It Does
- Reads locale files from all configured sources
- Inserts translations into the
translationsUitable - Sets
teamId: null(system default) - Sets
isOverrideable: true(allows team overrides) - 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
pnpm crouton i18n seed
# Seed specific locale
pnpm crouton i18n seed --locale=en
# Force overwrite existing
pnpm crouton i18n seed --force
# Dry run (show what would be inserted)
pnpm crouton i18n seed --dry-run
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
Override the default locales in your nuxt.config.ts:
export default defineNuxtConfig({
extends: [
'@friendlyinternet/nuxt-crouton',
'@friendlyinternet/nuxt-crouton-i18n'
],
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
- Creates the
i18n/locales/folder structure - Generates starter locale files (en.json, nl.json, fr.json)
- 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
langDir path is relative to the restructureDir (default: i18n). Use ./locales, NOT ./i18n/locales - the latter will result in a doubled path error.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 11 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
}
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 textCroutonEditorSimple- 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 ULocaleSelect
- Flag Icons: Custom emoji flags per locale
- Selection Indicator: Check mark for current language
- Ghost Variant: Minimal UI styling
Default Flags
{
en: '🌹', // English
nl: '🦁', // Dutch
fr: '🐔' // French
}
Usage
<template>
<!-- Basic usage -->
<CroutonI18nLanguageSwitcher />
</template>
The component automatically:
- Reads available locales from i18n config
- Updates the current locale on selection
- 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
- Key Path (required): Dot-notation key (e.g.,
table.search) - Category (required): Grouping category (e.g.,
table) - Translations (required): Multi-language values
- 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
CroutonI18nDisplaycomponent - 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
- Click "Enable translation dev mode"
- Scanner finds all
[keyPath]patterns - Click any highlighted translation
- Modal opens for that key
- Enter translation
- Save to database
- Page refreshes with new translation
CroutonI18nDevWrapper
Wrapper component for inline translation editing in development.
Props
interface DevWrapperProps {
translationKey: string // Translation key path
value?: string // Current value
locale?: string // Current locale (default: 'en')
level?: 'system' | 'team' | 'missing' // Translation source
teamId?: string // Team ID for team translations
}
Features
- Inline Editing: Click to edit, Enter to save, Escape to cancel
- Visual Indicators: Color-coded by source level
- Pulse Animation: Missing translations pulse red
- Quick Actions: Check and X buttons for save/cancel
Level Indicators
- System: Blue background - from system translations
- Team: Green background - team override
- Missing: Red background with pulse - no translation found
Usage
<script setup lang="ts">
const { currentTeam } = useTeam()
</script>
<template>
<!-- Wrap translatable content -->
<CroutonI18nDevWrapper
translation-key="common.save"
:value="$t('common.save')"
level="system"
>
{{ $t('common.save') }}
</CroutonI18nDevWrapper>
<!-- Team-specific translation -->
<CroutonI18nDevWrapper
translation-key="dashboard.title"
:value="title"
level="team"
:team-id="currentTeam?.id"
>
{{ title }}
</CroutonI18nDevWrapper>
</template>
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: (key: string) => 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')
useTranslationsUi()
Configuration composable for the translationsUi collection.
Type Signature
function useTranslationsUi(): {
schema: ZodSchema
columns: ColumnDefinition[]
defaultValue: TranslationDefaults
defaultPagination: PaginationConfig
config: CollectionConfig
collection: string
}
Returns
- schema: Zod validation schema
- columns: Table column definitions
- defaultValue: Default form values
- defaultPagination: Pagination settings
- config: Full collection configuration
- collection: Collection name (
'translationsUi')
Usage
<script setup lang="ts">
const { schema, columns, defaultValue } = useTranslationsUi()
const state = ref({ ...defaultValue })
const handleSubmit = async () => {
const valid = schema.parse(state.value)
await $fetch('/api/translations-ui', {
method: 'POST',
body: valid
})
}
</script>
<template>
<UForm :schema="schema" :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
- System Translations:
teamId = null,isOverrideable = true - 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:
- Check if translation exists in locale file
- Verify key path is correct
- Check team override isn't blocking system translation
- 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:
- Check current locale:
const { locale } = useI18n() - Verify query includes locale parameter
- 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>
Related Documentation
- Collections & Layers - Collection basics
- Data Operations - Working with data
- Querying Data - Query patterns
- Package Architecture - Understanding addons