Type-Safe Translations in Next.js - A Modern Approach
A comprehensive guide to implementing type-safe translations in Next.js with nested keys and template parameters
November 26, 2024
When building multilingual applications in Next.js, we often face several challenges:
- Type safety across translation keys and paths
 
- Template parameter support
 
- Prefix support for better organization
 
- Easy locale switching
 
- Seamless integration with Next.js routing
 
Our implementation solves these challenges while providing a delightful developer experience.
Loading...
Let's break down the implementation step by step:
Translation management in Next.js applications can be error-prone. Without proper type safety, developers can easily reference non-existent translation keys, miss required template parameters, or break existing translations during refactoring. Our type system addresses these challenges by:
- Catching missing translations at compile-time rather than runtime
 
- Providing autocomplete for all available translation paths
 
- Ensuring template parameters match the expected format
 
- Making refactoring safer by tracking all translation key dependencies
 
- Preventing typos in deeply nested translation keys
 
Loading...
This type system provides:
- Full Type Safety: All translation paths are derived from the actual translation object structure
 
- Intelligent Path Completion: TypeScript can suggest valid paths based on your translation structure
 
- Prefix Support: Allows for scoped translation functions that maintain type safety
 
- Template Type Safety: Ensures template parameters are properly typed
 
Example of the type inference in action:
Loading...
Let's see how these types resolve with our dictionary structure:
Loading...
Loading...
Here's how this component looks with our translation files:
Translation Files
Loading...
Loading...
 
- 
Type Safety:
- Compile-time validation of translation keys
 
- Autocomplete support
 
- Refactoring safety
 
- Template parameter validation
 
 
- 
Developer Experience:
- Intuitive API
 
- Prefix support for better organization
 
- Comprehensive TypeScript support
 
- Detailed error messages
 
 
- 
Flexibility:
- Support for nested keys
 
- Template parameters
 
- Multiple translation scopes
 
- String interpolation
 
 
- 
Locale Management:
- Easy locale switching
 
- Persistent locale preference
 
- Integration with Next.js routing
 
 
Loading...
Loading...
Loading...
- 
Productivity:
- Faster development with autocomplete
 
- Fewer runtime errors
 
- Easy debugging
 
 
- 
Maintainability:
- Organized translations with prefixes
 
- Type-safe refactoring
 
- Clear separation of concerns
 
 
The implementation provides a robust foundation for building multilingual applications in Next.js, with strong TypeScript support and a developer-friendly API.
Written with the help of WindSurf's Cascade.
1// Basic usage
2const { t } = useTranslation();
3t('common.welcome');
4
5// With prefix
6const { t } = useTranslation('common');
7t('welcome');
8
9// With template parameters
10t('
1import type translation from 'public/assets/locales/en';
2
3// Helper type that extracts all possible paths from a nested object
4// Returns an array of keys for each level of nesting
5type AllPathsProps<T, Acc extends any[] = []> = T extends string
6    ? Acc
7    : {
8          [K in Extract
1const { t } = useTranslation();
2
3// Valid - path exists in translations
4t('forms.errors.required');
5
6// Error - path doesn't exist in translations
7t('forms.errors.nonexistent');
8
9// With prefix
10const { t: formT } = useTranslation('forms
1// Given this translation structure:
2const translation = {
3  header: {
4    title: "Welcome to My App",
5    subtitle: "Start your journey",
6    nav: {
7      home: "Home",
8      about: "About",
9      contact
greeting
'
, { name
: 
'
John
'
 }); 
// "Hello, John!"
13const { setLocale } = useTranslation();
<
keyof 
T
, 
string
>]
: 
Acc 
| 
AllPathsProps
<
T
[
K
], [
...
Acc
, 
K
]>;
9      }[Extract<keyof T, string>];
11// Helper type that converts an object structure into arrays of path segments
12// e.g., { forms: { errors: { required: string } } } 
13// → ['forms', 'errors', 'required']
14type PathsToStringProps<T> = T extends string
17          [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];
18      }[Extract<keyof T, string>];
20// Utility type that joins string array with a delimiter
21// e.g., Join<['forms', 'errors', 'required'], '.'> 
22// → 'forms.errors.required'
23type Join<T extends string[], D extends string> = T extends []
27      : T extends [infer F, ...infer R]
29            ? `${F}${D}${Join<Extract<R, string[]>, D>}`
33// All possible paths in the translation object
34// This creates a union type of all possible dot-notation paths
35export type AllPaths = Join<AllPathsProps<typeof translation>, '.'>;
37// Valid translation paths derived from the translation object structure
38// This ensures type safety by only allowing paths that exist in translations
39export type TranslationPath = Join<PathsToStringProps<typeof translation>, '.'>;
41// Parameters that can be injected into translation templates
42export type TemplateParams = Record<string, string>;
44// The translation function type
45// Paths can be either TranslationPath or a more specific path type
46export type TranslationFunction<Paths extends string = TranslationPath> = (
48    templateParams?: TemplateParams
51// Extracts the remaining path after a prefix
52// Used for creating scoped translation functions
53export type PrefixedPath<
55    Prefix extends string,
56> = Paths extends `${Prefix}.${infer Rest}` ? Rest : never;
58// The return type of useTranslation hook
59export type Results<T> = {
60    locale: string;              // Current active locale
61    setLocale: (locale: string) => void;  // Function to change locale
62    t: T;                        // The translation function
'
);
11// Valid - shorter path due to prefix
12formT('errors.required');
: 
"
Contact
"
15        required: "This field is required",
16        email: "Please enter a valid email",
18          length: "Password must be at least ${length} characters",
19          uppercase: "Password must contain at least one uppercase letter",
20          number: "Password must contain at least one number"
24        save: "Successfully saved ${itemName}",
25        delete: "Successfully deleted ${itemName}"
31// PathsToStringProps resolves to:
32type PathArrays = PathsToStringProps<typeof translation>;
36  | ['header', 'subtitle']
38  | ['header', 'nav', 'home']
39  | ['header', 'nav', 'about']
40  | ['header', 'nav', 'contact']
42  | ['forms', 'validation']
43  | ['forms', 'validation', 'errors']
44  | ['forms', 'validation', 'errors', 'required']
45  | ['forms', 'validation', 'errors', 'email']
46  | ['forms', 'validation', 'errors', 'password']
47  | ['forms', 'validation', 'errors', 'password', 'length']
48  | ['forms', 'validation', 'errors', 'password', 'uppercase']
49  | ['forms', 'validation', 'errors', 'password', 'number']
50  | ['forms', 'validation', 'success']
51  | ['forms', 'validation', 'success', 'save']
52  | ['forms', 'validation', 'success', 'delete']
1'use client';
2
3// Function overloads to provide correct typing based on usage:
4// 1. No prefix - returns translation function for all paths
5// 2. With prefix - returns translation function for paths under that prefix
6export function useTranslation(): Results<TranslationFunction>;
7export function useTranslation<Partial extends Exclude<AllPaths, TranslationPath>>(
8    prefix: Partial
1{
2  "common": {
3    "welcome": "Welcome to Our App",
4    "description": "Register to get started",
5    "loading": "Loading..."
6  },
7  "languages": {
8    "english": "English"
1{
2  "common": {
3    "welcome": "ברוכים הבאים לאפליקציה שלנו",
4    "description": "הירשמו כדי להתחיל",
5    "loading": "טוען..."
6  },
7  "languages": {
8    "english": "אנגלית"
1'use client';
2
3function WelcomePage() {
4    const { t } = useTranslation();
5    return <h1>{t('common.welcome')}</h1>;
6}
7
1'use client';
2
3function AnalysisPage() {
4    const { t } = useTranslation('analysis');
5    return (
6        <div>
7            <h2>{t('title')}</h2>
8            <p>{t(
1'use client';
2
3function Greeting({ name }: { name: string }) {
4    const { t } = useTranslation();
5    return <p>{t('greeting', { name })}</p>;
6}
7
9): Results<TranslationFunction<PrefixedPath<TranslationPath, Partial>>>;
12 * The useTranslation hook provides a translation function (t) and locale management.
13 * It can be used with or without a prefix for scoped translations.
15 * @param {string} [prefix] - Optional prefix for scoped translations.
16 * @returns {Results<TranslationFunction>} - An object containing the translation function (t), 
17 * the current locale, and a function to set the locale.
19export function useTranslation(prefix?: string) {
20    // Get the current route path for locale persistence
21    const { asPath } = useRouter();
23    // Access and track the current locale from our context/state
24    const locale = useLocale();
27     * Function to update the active locale.
28     * This will persist the choice and trigger a re-render.
30     * @param {string} locale - The new locale to set.
32    const setLocale = useCallback(
34            setLocaleCookie(locale);
41     * The main translation function that:
42     * 1. Handles prefixed paths if a prefix was provided
43     * 2. Processes template parameters in the translation string
44     * 3. Falls back gracefully if translation is missing
46     * @param {string} path - The translation path.
47     * @param {TemplateParams} [templateParams] - Optional template parameters.
48     * @returns {string} - The translated string.
50    const t = useCallback(
51        (path: string, templateParams = {}) =>
52            // If prefix provided, automatically prepend it to the path
54                ? getTranslation(`${prefix}.${path}`, locale, templateParams)
55                : getTranslation(path, locale, templateParams),
59    // Return the locale state and translation function
60    // This allows components to both translate and control the locale
69const Component = () => {
70    // Basic usage - access to all translation paths
71    const { t } = useTranslation();
73    // Prefixed usage for form-related translations
74    const { t: formT } = useTranslation('forms');
76    // Prefixed usage specifically for error messages
77    const { t: errorT } = useTranslation('forms.errors');
80        <div className="registration-form">
81            {/* Main header with basic translation */}
82            <h1>{t('common.welcome')}</h1>
83            <p>{t('common.description')}</p>
85            {/* Form section using prefixed translations */}
86            <form onSubmit={handleSubmit}>
87                <div className="form-group">
88                    <label className="block text-sm font-medium mb-1">
89                        {formT('labels.email')}
93                        className="w-full px-3 py-2 border rounded-md"
94                        placeholder={formT('placeholders.email')}
95                        aria-label={formT('aria.email')}
98                        <span className="error-message">
104                {/* Example with template parameters */}
105                <p className="password-requirement">
106                    {formT('validation.password.length', { length: '8' })}
109                {/* Button with loading state */}
110                <button type="submit">
111                    {isLoading ? t('common.loading') : formT('buttons.submit')}
115            {/* Language switcher */}
116            <div className="language-switcher">
117                <button onClick={() => setLocale('en')}>
118                    {t('languages.english')}
120                <button onClick={() => setLocale('he')}>
121                    {t('languages.hebrew')}
,
13      "email": "Email Address"
16      "email": "Enter your email"
19      "email": "Email input field"
26        "length": "Password must be at least ${length} characters"
30      "required": "This field is required"
,
13      "email": "כתובת אימייל"
16      "email": "הכניסו את האימייל שלכם"
19      "email": "שדה קלט אימייל"
26        "length": "הסיסמה חייבת להכיל לפחות ${length} תווים"
30      "required": "שדה זה הוא חובה"
 Type-Safe Translations in Next.js - A Modern Approach - Itamar Sharify'
description
'
)}
</
p
>