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
Indonesian id - Bahasa Indonesia
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
Backend (NestJS)
Frontend (Next.js)
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
next-intl framework for UI text
API client automatically adds language parameter
Locale-aware routing: /[locale]/...
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
# Get all staff in Japanese
GET /staff?lang=ja
# Get staff details in Vietnamese
GET /staff/staff_001?lang=vi
# Get all settings in Korean
GET /settings?lang=ko
# Get specific setting in French
GET /settings/businessName?lang=fr
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
getAvailableLanguages ( field : unknown ): SupportedLanguage []
// Returns array of languages with translations
normalizeLanguageCode ( lang : string ): SupportedLanguage
// Converts language code variations to standard codes
// Examples: 'en-US' -> 'en', 'tl' -> 'fil'
Services Endpoint
name and description are localized strings, not JSONB objects.
Staff Endpoint
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
Service Table
Staff Table
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 ()
);
CREATE TABLE " Staff " (
id UUID PRIMARY KEY ,
userId UUID NOT NULL ,
code VARCHAR UNIQUE ,
department JSONB, -- { "en": "...", "vi": "..." }
position JSONB, -- { "en": "...", "vi": "..." }
specialization JSONB, -- { "en": "...", "vi": "..." }
status VARCHAR ,
"isAvailable" BOOLEAN DEFAULT true,
"createdAt" TIMESTAMP DEFAULT now (),
"updatedAt" TIMESTAMP DEFAULT now (),
FOREIGN KEY (userId) REFERENCES "User" (id)
);
Best Practices
Always Include English
Every translatable field must have English (en) as fallback
Use Type Safety
Use SupportedLanguage type for language parameters
Normalize Input
Use normalizeLanguageCode() before validation
Validate Languages
Check with isSupportedLanguage() before using
Localize on Response
Apply localization in service methods, not controllers
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