Skip to main content

Multi-Language (i18n) Implementation

JSONB-based multi-language system supporting 14 languages with dynamic language switching.
Implementation: Phase 3-5 complete. Backend API localization with query parameter-based language selection.

Supported Languages

English

en - English (Required fallback)

Vietnamese

vi - Tiếng Việt

Japanese

ja - 日本語

Chinese

zh - 中文

Korean

ko - 한국어

Thai

th - ภาษาไทย

Indonesian

id - Bahasa Indonesia

French

fr - Français

German

de - Deutsch

Spanish

es - Español

Lao

lo - ພາສາລາວ

Khmer

km - ខ្មែរ

Burmese

my - မြန်မာ

Filipino

fil - Wikang Tagalog

Architecture

Data Storage (Database Layer)

Translatable fields stored as JSONB in PostgreSQL with English as required fallback:
{
  "en": "Pet Grooming",
  "vi": "Dịch vụ chăm sóc lông",
  "ja": "ペットグルーミング",
  "ko": "애완동물 미용",
  "fr": "Toilettage des animaux",
  "de": "Haustierpflege"
}

Implementation Stack

Files:
  • backend/src/common/utils/i18n.util.ts - Translation utilities
  • backend/src/common/decorators/language.decorator.ts - Language decorator
  • backend/src/common/interceptors/language.interceptor.ts - Language interceptor
API Query Parameter:
GET /services?lang=vi
GET /staff?lang=ko
GET /settings?lang=de

Service & Staff Localization

Services Module

export class CreateServiceDto {
  name: TranslatedField;           // { en: "...", vi: "...", ja: "..." }
  description: TranslatedField;    // Multi-language descriptions
  category: ServiceCategory;
  basePrice: number;
  priceUnit: string;
  currency?: string;
  duration?: number;
  imageUrl?: string;
}

Endpoint Examples

# Get all services in Vietnamese
GET /services?lang=vi

# Get service details in Korean
GET /services/svc_001?lang=ko

# Get in English (default)
GET /services
GET /services?lang=en

Utility Functions

Core Functions

Creates and validates translated field. Requires English (en) translation.
const serviceData = {
  name: createTranslatedField({
    en: "Pet Grooming",
    vi: "Dịch vụ chăm sóc lông",
    ja: "ペットグルーミング"
  }),
  description: createTranslatedField({
    en: "Professional pet grooming services",
    vi: "Dịch vụ chăm sóc lông chuyên nghiệp"
  })
};
Extracts localized string from translated field. Falls back to English if language not available.
const service = { name: { en: "Grooming", vi: "Chăm sóc" } };
getLocalizedField(service.name, 'vi')   // "Chăm sóc"
getLocalizedField(service.name, 'ja')   // "Grooming" (fallback to en)
Localizes specific fields in a single object. Returns new object with localized values.
const service = {
  id: 'svc_001',
  name: { en: "Grooming", vi: "Chăm sóc" },
  description: { en: "Grooming service", vi: "Dịch vụ chăm sóc" }
};

const localized = localizeObject(service, 'vi', ['name', 'description']);
// Returns: {
//   id: 'svc_001',
//   name: "Chăm sóc",
//   description: "Dịch vụ chăm sóc"
// }
Localizes specific fields in array of objects. Used for list endpoints.
const services = [
  { id: 'svc_001', name: { en: "Grooming", vi: "..." } },
  { id: 'svc_002', name: { en: "Boarding", vi: "..." } }
];

const localized = localizeArray(services, 'vi', ['name']);
// Returns array with all names in Vietnamese

Additional Utilities

hasEnglishTranslation(field: unknown): boolean
// Checks if field has English translation

isSupportedLanguage(lang: string): lang is SupportedLanguage
// Type guard to validate language code

API Response Format

Services Endpoint

GET /services?lang=vi
name and description are localized strings, not JSONB objects.

Staff Endpoint

GET /staff?lang=ko

Implementation Patterns

Service Method Pattern

All GET endpoints follow this pattern:
async findAll(language: SupportedLanguage = 'en'): Promise<Entity[]> {
  // 1. Fetch from database
  const items = await this.prisma.entity.findMany();

  // 2. Localize translatable fields
  return localizeArray(items, language, [
    'field1',  // Field names that have TranslatedField type
    'field2'
  ]);
}

Controller Pattern

@Get()
findAll(@Language() lang: SupportedLanguage) {
  return this.entityService.findAll(lang);
}

@Get(':id')
findOne(
  @Param('id') id: string,
  @Language() lang: SupportedLanguage
) {
  return this.entityService.findOne(id, lang);
}

Frontend Integration

API Client Usage

// Fetch in Vietnamese
const services = await fetch('/api/services?lang=vi');

// Fetch in Japanese
const staff = await fetch('/api/staff?lang=ja');

// Default to user's locale in next-intl
const locale = useLocale();
const response = await fetch(`/api/services?lang=${locale}`);

Response Handling

Frontend receives localized strings directly (no translation needed):
// Response from API
const service = {
  id: 'svc_001',
  name: "Dịch vụ chăm sóc lông",      // Already localized!
  description: "Chuyên nghiệp...",    // No further translation needed
  category: "GROOMING"
};

// Direct use in components
<h1>{service.name}</h1>              // Display localized name
<p>{service.description}</p>         // Display localized description

Database Schema

CREATE TABLE "Service" (
  id UUID PRIMARY KEY,
  name JSONB NOT NULL,              -- { "en": "...", "vi": "..." }
  description JSONB,                 -- { "en": "...", "vi": "..." }
  category VARCHAR NOT NULL,
  basePrice DECIMAL(10,2),
  priceUnit VARCHAR,
  currency VARCHAR,
  duration INTEGER,
  imageUrl TEXT,
  images TEXT[],
  "isActive" BOOLEAN DEFAULT true,
  "createdAt" TIMESTAMP DEFAULT now(),
  "updatedAt" TIMESTAMP DEFAULT now()
);

Best Practices

1

Always Include English

Every translatable field must have English (en) as fallback
2

Use Type Safety

Use SupportedLanguage type for language parameters
3

Normalize Input

Use normalizeLanguageCode() before validation
4

Validate Languages

Check with isSupportedLanguage() before using
5

Localize on Response

Apply localization in service methods, not controllers
6

Test Fallbacks

Verify English fallback works for missing translations

Error Handling

The system handles edge cases gracefully:
// Missing language - defaults to English
GET /services           // Uses 'en'

// Invalid language - falls back to English
GET /services?lang=xxx  // Normalizes to 'en'

// Missing translation - falls back to English
getLocalizedField({ en: "Text" }, 'ja')  // Returns "Text"

// Missing English translation - rejected
createTranslatedField({ vi: "Text" })    // Throws error