i18n Concepts
Understanding internal vs external internationalization
i18n Concepts
Collections provides native internationalization support, but it's important to understand the distinction between internal i18n and external i18n.
Overview
Prop
Type
Internal i18n
Internal i18n refers to translating the structural elements of your collections - things that define how your data layer works.
What Gets Translated
How Internal i18n Works
- Define supported locales in your configuration
- Provide translations for labels, descriptions, messages
- Set current locale when performing operations
- Collections returns translated values based on locale
// Configuration
const { collections } = defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr', 'es'],
fallbackLocale: 'en'
}
})
// Set locale for operation
const label = collections.posts.fields.title.getLabel('fr')
// Returns: 'Titre'External i18n
External i18n refers to translating the actual content that users create and store in your database.
What Gets Translated
Approaches to External i18n
1. Separate Records per Locale
Store each translation as a separate record with a locale field:
export const posts = collection({
slug: 'posts',
fields: {
title: field({ type: text() }),
content: field({ type: text() }),
locale: field({ type: enumField(['en', 'fr', 'es']) })
}
})
// Usage
const englishPost = await collections.posts.create({
data: {
title: 'Hello World',
content: '...',
locale: 'en'
}
})
const frenchPost = await collections.posts.create({
data: {
title: 'Bonjour le monde',
content: '...',
locale: 'fr'
}
})Pros:
- Simple to implement
- Easy to query by locale
- Works well with Collections
Cons:
- Duplicate data structure
- Need to manually link translations
2. Translation Relations
Use relations to link translations:
export const posts = collection({
slug: 'posts',
fields: {
title: field({ type: text() }),
content: field({ type: text() }),
locale: field({ type: enumField(['en', 'fr', 'es']) }),
translationGroup: field({ type: text() }) // ID to group translations
}
})
// Query all translations
const translations = await collections.posts.findMany({
where: {
translationGroup: { equals: 'group-123' }
}
})Pros:
- Links related translations
- Can query all translations at once
Cons:
- Requires manual management of translation groups
- More complex queries
3. JSON Fields for Translations
Store all translations in a single JSON field:
export const posts = collection({
slug: 'posts',
fields: {
title: field({
type: json(z.object({
en: z.string(),
fr: z.string().optional(),
es: z.string().optional()
}))
}),
content: field({
type: json(z.object({
en: z.string(),
fr: z.string().optional(),
es: z.string().optional()
}))
})
}
})
// Usage
await collections.posts.create({
data: {
title: {
en: 'Hello World',
fr: 'Bonjour le monde',
es: 'Hola mundo'
},
content: {
en: 'Content in English...',
fr: 'Contenu en français...'
}
}
})Pros:
- Single record per content piece
- Easy to access all translations
Cons:
- Harder to query individual translations
- Schema is more rigid
- Can't easily query missing translations
4. Separate Translation Collection
Create a dedicated collection for translations:
export const posts = collection({
slug: 'posts',
fields: {
slug: field({ type: text(), unique: true })
}
})
export const postTranslations = collection({
slug: 'postTranslations',
fields: {
postId: field({ type: relation('posts') }),
locale: field({ type: enumField(['en', 'fr', 'es']) }),
title: field({ type: text() }),
content: field({ type: text() })
}
})
// Query with translation
const postWithTranslation = await collections.posts.findUnique({
where: { id: 1 },
include: {
postTranslations: {
where: { locale: { equals: 'fr' } }
}
}
})Pros:
- Clean separation of content
- Flexible schema
- Easy to query
Cons:
- More complex queries
- Requires joins
Choosing an Approach
Prop
Type
Best Practices
Internal i18n
- Always provide translations for all supported locales
- Use fallback locale for missing translations
- Keep translations consistent across your schema
- Test all locales during development
// ✅ Good - complete translations
label: {
en: 'Title',
fr: 'Titre',
es: 'Título'
}
// ❌ Bad - incomplete translations
label: {
en: 'Title',
fr: 'Titre'
// Missing 'es'
}External i18n
- Choose approach based on use case
- Handle missing translations gracefully
- Consider content management workflow
- Plan for content synchronization
// ✅ Good - handle missing translations
const getPost = async (slug: string, locale: string) => {
let post = await collections.posts.findFirst({
where: {
slug: { equals: slug },
locale: { equals: locale }
}
})
// Fallback to default locale
if (!post && locale !== 'en') {
post = await collections.posts.findFirst({
where: {
slug: { equals: slug },
locale: { equals: 'en' }
}
})
}
return post
}Example: Complete i18n Setup
Here's a complete example showing both internal and external i18n:
// config/database.ts
import { defineConfig } from '@deessejs/collections'
export const { collections } = defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr', 'es'],
fallbackLocale: 'en'
},
collections: [posts, users]
})
// collections/posts.ts
import { collection, field } from '@deessejs/collections'
import { text, enumField } from '@deessejs/collections/fields'
export const posts = collection({
slug: 'posts',
// Internal i18n - collection labels
label: {
en: 'Blog Post',
fr: 'Article de Blog',
es: 'Artículo de Blog'
},
fields: {
// Internal i18n - field labels
title: field({
type: text(),
label: {
en: 'Title',
fr: 'Titre',
es: 'Título'
}
}),
// Internal i18n - field descriptions
content: field({
type: text(),
description: {
en: 'The main content of the post',
fr: 'Le contenu principal de l\'article'
}
}),
// External i18n - locale field for content
locale: field({
type: enumField(['en', 'fr', 'es']),
label: {
en: 'Language',
fr: 'Langue'
}
})
}
})
// Usage
const englishPost = await collections.posts.create({
data: {
title: 'Hello World', // External: English content
content: 'English content...',
locale: 'en'
},
locale: 'en' // Internal: get error messages in English
})
const frenchPost = await collections.posts.create({
data: {
title: 'Bonjour le monde', // External: French content
content: 'Contenu français...',
locale: 'fr'
},
locale: 'fr' // Internal: get error messages in French
})Summary
- Internal i18n = Translations of UI elements managed by Collections
- External i18n = Translation of user content managed by your application
- Use the approach that fits your use case
- Plan your i18n strategy before building
Next Steps
- i18n - Internal i18n configuration and usage
- Fields - Field-level i18n options
- Core Concepts - Configuration and setup