Internationalization (i18n)
A fully type-safe internationalization utility with compile-time validation of translation keys and parameters.
Import
import { I18n } from "semola/i18n";
Class: I18n
Constructor
new I18n<
const TLocales extends Record<string, Record<string, unknown>>,
TDefaultLocale extends keyof TLocales
>(config: { defaultLocale: TDefaultLocale; locales: TLocales })
i18n configurationDefault locale key (used as fallback and type inference source)
Locale translations object. Must use as const for proper type inference.
Example:
const i18n = new I18n({
defaultLocale: "en",
locales: {
en: {
common: {
hello: "Hello, world",
sayHi: "Hi, {name:string}",
age: "I am {age:number} years old",
},
},
es: {
common: {
hello: "Hola, mundo",
sayHi: "Hola, {name:string}",
age: "Tengo {age:number} años",
},
},
} as const, // Required for type inference
});
Methods
translate
Translates a key with optional parameters. Fully type-safe with compile-time validation.
translate<TKey extends NestedKeyOf<TLocales[TDefaultLocale]>>(
key: TKey,
...params: [BuildParamObject<...>] | []
): string
Nested translation key (e.g., "common.hello", "auth.welcome"). Only valid keys are accepted.
params
BuildParamObject<...> | []
Parameters object if the translation contains placeholders. TypeScript enforces correct parameter types.
Translated string with substituted parameters
Example:
// Basic translation
i18n.translate("common.hello");
// "Hello, world"
// With parameters
i18n.translate("common.sayHi", { name: "Leonardo" });
// "Hi, Leonardo"
i18n.translate("common.age", { age: 25 });
// "I am 25 years old"
// Type errors (won't compile)
i18n.translate("invalid.key"); // ✗ Invalid key
i18n.translate("common.sayHi"); // ✗ Missing required params
i18n.translate("common.sayHi", { name: 123 }); // ✗ Wrong param type
i18n.translate("common.hello", { name: "x" }); // ✗ Unnecessary params
setLocale
Switches to a different locale.
setLocale(locale: keyof TLocales): void
Locale key to switch to. Only valid locale keys are accepted.
Example:
i18n.setLocale("es");
i18n.translate("common.hello");
// "Hola, mundo"
i18n.setLocale("invalid"); // ✗ Type error
getLocale
Returns the current locale.
getLocale(): keyof TLocales
Example:
const currentLocale = i18n.getLocale();
// "es"
Parameter Syntax
Translation strings support typed parameters with the syntax {paramName:type}:
{name:string} - String parameter
{age:number} - Number parameter
{active:boolean} - Boolean parameter
The type system extracts these at compile time and enforces them in the translate() method.
Example:
const translations = {
en: {
user: {
welcome: "Welcome, {name:string}!",
age: "Age: {age:number}",
status: "Active: {active:boolean}",
},
},
} as const;
const i18n = new I18n({ defaultLocale: "en", locales: translations });
// TypeScript enforces parameter types
i18n.translate("user.welcome", { name: "Alice" }); // ✓
i18n.translate("user.age", { age: 30 }); // ✓
i18n.translate("user.status", { active: true }); // ✓
i18n.translate("user.welcome", { name: 123 }); // ✗ Type error
Type Definitions
NestedKeyOf
Extracts all valid nested keys from a translation object.
type NestedKeyOf<T> = ...; // Internal type
Generates keys like "common.hello", "auth.login.success", etc.
BuildParamObject
Extracts parameter types from a template string.
type BuildParamObject<T extends string> = ...;
Converts "Hi, {name:string}" to { name: string }, etc.
GetNestedValue
Retrieves the value at a nested key path.
type GetNestedValue<T, K extends string> = ...;
Usage Examples
Multi-language Application
import { I18n } from "semola/i18n";
const translations = {
en: {
auth: {
welcome: "Welcome back, {name:string}!",
loginSuccess: "Successfully logged in",
loginFailed: "Login failed",
},
profile: {
age: "Age: {age:number}",
verified: "Verified: {status:boolean}",
},
},
es: {
auth: {
welcome: "Bienvenido, {name:string}!",
loginSuccess: "Inicio de sesión exitoso",
loginFailed: "Inicio de sesión fallido",
},
profile: {
age: "Edad: {age:number}",
verified: "Verificado: {status:boolean}",
},
},
} as const;
const i18n = new I18n({
defaultLocale: "en",
locales: translations,
});
// Use in your app
function greetUser(name: string) {
return i18n.translate("auth.welcome", { name });
}
function showProfile(age: number, verified: boolean) {
console.log(i18n.translate("profile.age", { age }));
console.log(i18n.translate("profile.verified", { status: verified }));
}
// Switch language
i18n.setLocale("es");
greetUser("Maria"); // "Bienvenido, Maria!"
API Error Messages
import { I18n } from "semola/i18n";
const errors = {
en: {
validation: {
required: "{field:string} is required",
minLength: "{field:string} must be at least {min:number} characters",
email: "Invalid email address",
},
auth: {
unauthorized: "Unauthorized access",
tokenExpired: "Your session has expired",
},
},
fr: {
validation: {
required: "{field:string} est requis",
minLength: "{field:string} doit contenir au moins {min:number} caractères",
email: "Adresse e-mail invalide",
},
auth: {
unauthorized: "Accès non autorisé",
tokenExpired: "Votre session a expiré",
},
},
} as const;
const i18n = new I18n({ defaultLocale: "en", locales: errors });
// Use in validation
function validateField(field: string, value: string, minLength: number) {
if (!value) {
throw new Error(i18n.translate("validation.required", { field }));
}
if (value.length < minLength) {
throw new Error(
i18n.translate("validation.minLength", { field, min: minLength })
);
}
}
With User Preferences
import { I18n } from "semola/i18n";
class UserSession {
private i18n: I18n<typeof translations, "en">;
constructor(userLocale: "en" | "es" | "fr") {
this.i18n = new I18n({
defaultLocale: "en",
locales: translations,
});
this.i18n.setLocale(userLocale);
}
t(key: string, params?: any) {
return this.i18n.translate(key as any, params);
}
changeLanguage(locale: "en" | "es" | "fr") {
this.i18n.setLocale(locale);
}
}
// Usage
const session = new UserSession("es");
console.log(session.t("common.hello")); // "Hola, mundo"
session.changeLanguage("fr");
console.log(session.t("common.hello")); // "Bonjour, monde"
Nested Translations
const translations = {
en: {
app: {
nav: {
home: "Home",
profile: "Profile",
settings: "Settings",
},
footer: {
copyright: "© {year:number} Company Name",
privacy: "Privacy Policy",
},
},
},
} as const;
const i18n = new I18n({ defaultLocale: "en", locales: translations });
// Deeply nested keys are fully typed
i18n.translate("app.nav.home"); // ✓ "Home"
i18n.translate("app.footer.copyright", { year: 2024 }); // ✓ "© 2024 Company Name"
i18n.translate("app.invalid.key"); // ✗ Type error
Features
- Type-safe keys: Only valid nested keys accepted (e.g.,
"common.hello")
- Type-safe parameters: Parameter types validated at compile time
- Type-safe locales: Only defined locale keys can be set
- Nested translations: Support for deeply nested translation objects
- Locale fallback: Falls back to default locale if translation missing
- Zero runtime overhead: No runtime type checking - pure TypeScript validation
- Const assertion required: Use
as const on locale objects for proper type inference
Important Notes
Const Assertion Required
You must use as const on the locales object for proper type inference:
// ✓ Correct
const i18n = new I18n({
defaultLocale: "en",
locales: {
en: { hello: "Hello" },
es: { hello: "Hola" },
} as const, // Required!
});
// ✗ Incorrect (no type safety)
const i18n = new I18n({
defaultLocale: "en",
locales: {
en: { hello: "Hello" },
es: { hello: "Hola" },
}, // Missing as const
});
Locale Fallback
If a translation is missing in the current locale, the default locale is used as fallback:
const i18n = new I18n({
defaultLocale: "en",
locales: {
en: { greeting: "Hello" },
es: {}, // Missing translation
} as const,
});
i18n.setLocale("es");
i18n.translate("greeting"); // Falls back to "Hello"