Next.js Localization Best Practices for Enterprise Apps
Table of contents Table of contents Overview Guideline for adding new key Folder acrhitecture Configuration Provider and client support Combination with failure handling and params validation Requirements This article based on Next.js boilerplate repository. To explore all concepts in depth and see a production-ready boilerplate and following these best practices, visit: behnamrhp / Next-clean-boilerplate A Full featured nextjs boilerplate, based on clean architecture, mvvm and functional programming paradigm. Nextjs clean architecture boilerplate Table of content Overview Technologies Architecture Folder Structure Getting started Guildline Overview This project is a starting point for your medium to large scale projects with Nextjs, to make sure having a structured, maintainable and reusable base for your project based on best practices in clean architecture, DDD approach for business logics, MVVM for the frontend part, storybook and vitest for testing logics and ui part and also functional programming with error handling for business logics. Motivation Nextjs and many other new SSR tools provide a really good and new approach to handle frontend applications, with new tools to bring a new good experience for users. But as they're new and they just tried to bring new tools and features and also frontend community, didn't talk about software engineering and best practices approach for this tools. So in many cases we see many teams uses nextjs… View on GitHub Also to know more about higher level architecture in Next.js applications. please take a look at clean architecture in Next.js article. Overview A major issue with most localization libraries is their reliance on hardcoded string keys throughout the codebase. This forces developers to manually track and update every language dictionary file whenever keys change - a tedious and error-prone process with no type safety or automation. This document provides guidelines for: File organization: Structure localization files so changes to any key trigger errors across all language resources Key management: Replace hardcoded strings with typed objects to eliminate manual refactoring when keys change Possible to work with error handling Guideline for adding new key Define namespace: at first based on the domains which we talked about it in this article we make a folder. Main folder: common Add langkey and namespace name for the domain: langKey is the single source for the languages in that domain which all dictionaries will use it. As we use this source in failure and params message in domain layer we put them in feature layer but dictionaries will remain in bootstrap. /** * main language keys which will be used for translation to avoid using strings directly and be * a single source of truth in all changes between all languages dictionaries. * All languages dictionaries should have the same keys by having this object type. */ const commonLangKey = { global: { home: "global.home", dashboard: "global.dashboard", loading: "global.loading", required: "global.required", passwordMinLength: "global.passwordMinLength", }, failure: { network: "failure.network", param: "failure.param", }, dashboard: { invoice: { createButton: "dashboard.invoice.createButton", }, }, }; export const commonLangNs = "common"; export default commonLangKey; Main file: common-lang-key.ts note: Make sure the namespace name is the same as folder name in dictionaries to be able import them by this namespace name. Dictionaries: Add related dictionaries for the domain in bootstrap layer. // en.ts import commonLangKey from "@/feature/common/lang-keys/common.lang-key"; const en: typeof commonLangKey = { global: { home: "Home", loading: "Loading", required: "{{field}} is Required", dashboard: "Dashboard", passwordMinLength: "Password length should be at least 8 characters!", }, dashboard: { invoice: { createButton: "Create random Invoice", }, }, failure: { network: "Something went wrong please try again layer!", param: "Please provide correct information", }, }; export default en; Main file: en.ts note: as we use language enum to define languages make sure the langs are the same as language enum in this file to be able import the dictionaries. Add your new key to langKey.ts to proper domain. Add translation for new key in en.ts and ru.ts in the same domain as in the langKey.ts. en.ts and ru.ts objects are connected to source object, so typescript will show an error if new key added or old key was changed. Also this connections helps us to avoid code become brittle since typescript will show an error in places of usage of translation keys if they were changed. langKey.ts const langKey = { glo
Table of contents
- Table of contents
- Overview
- Guideline for adding new key
- Folder acrhitecture
- Configuration
- Provider and client support
- Combination with failure handling and params validation
Requirements
This article based on Next.js boilerplate repository.
To explore all concepts in depth and see a production-ready boilerplate and following these best practices, visit:
behnamrhp / Next-clean-boilerplate
A Full featured nextjs boilerplate, based on clean architecture, mvvm and functional programming paradigm.
Nextjs clean architecture boilerplate
Table of content
- Overview
- Technologies
- Architecture
- Folder Structure
- Getting started
- Guildline
Overview
This project is a starting point for your medium to large scale projects with Nextjs, to make sure having a structured, maintainable and reusable base for your project based on best practices in clean architecture, DDD approach for business logics, MVVM for the frontend part, storybook and vitest for testing logics and ui part and also functional programming with error handling for business logics.
Motivation
Nextjs and many other new SSR tools provide a really good and new approach to handle frontend applications, with new tools to bring a new good experience for users. But as they're new and they just tried to bring new tools and features and also frontend community, didn't talk about software engineering and best practices approach for this tools.
So in many cases we see many teams uses nextjs…
Also to know more about higher level architecture in Next.js applications. please take a look at clean architecture in Next.js article.
Overview
A major issue with most localization libraries is their reliance on hardcoded string keys throughout the codebase. This forces developers to manually track and update every language dictionary file whenever keys change - a tedious and error-prone process with no type safety or automation.
This document provides guidelines for:
File organization: Structure localization files so changes to any key trigger errors across all language resources
Key management: Replace hardcoded strings with typed objects to eliminate manual refactoring when keys change
Possible to work with error handling
Guideline for adding new key
Define namespace: at first based on the domains which we talked about it in this article we make a folder.
Main folder: commonAdd langkey and namespace name for the domain: langKey is the single source for the languages in that domain which all dictionaries will use it. As we use this source in failure and params message in domain layer we put them in feature layer but dictionaries will remain in bootstrap.
/**
* main language keys which will be used for translation to avoid using strings directly and be
* a single source of truth in all changes between all languages dictionaries.
* All languages dictionaries should have the same keys by having this object type.
*/
const commonLangKey = {
global: {
home: "global.home",
dashboard: "global.dashboard",
loading: "global.loading",
required: "global.required",
passwordMinLength: "global.passwordMinLength",
},
failure: {
network: "failure.network",
param: "failure.param",
},
dashboard: {
invoice: {
createButton: "dashboard.invoice.createButton",
},
},
};
export const commonLangNs = "common";
export default commonLangKey;
Main file: common-lang-key.ts
note: Make sure the namespace name is the same as folder name in dictionaries to be able import them by this namespace name.
- Dictionaries: Add related dictionaries for the domain in bootstrap layer.
// en.ts
import commonLangKey from "@/feature/common/lang-keys/common.lang-key";
const en: typeof commonLangKey = {
global: {
home: "Home",
loading: "Loading",
required: "{{field}} is Required",
dashboard: "Dashboard",
passwordMinLength: "Password length should be at least 8 characters!",
},
dashboard: {
invoice: {
createButton: "Create random Invoice",
},
},
failure: {
network: "Something went wrong please try again layer!",
param: "Please provide correct information",
},
};
export default en;
Main file: en.ts
note: as we use language enum to define languages make sure the langs are the same as language enum in this file to be able import the dictionaries.
- Add your new key to langKey.ts to proper domain.
- Add translation for new key in en.ts and ru.ts in the same domain as in the langKey.ts. en.ts and ru.ts objects are connected to source object, so typescript will show an error if new key added or old key was changed. Also this connections helps us to avoid code become brittle since typescript will show an error in places of usage of translation keys if they were changed.
langKey.ts
const langKey = {
global: {
keyWithoutSpecificDomain: "global.keyWithoutSpecificDomain"
},
sampleKey: "sampleDomain.sampleKey",
};
export default langKey;
ru.ts
const ru: typeof langKey = {
global: {
keyWithoutSpecificDomain: "Строка без домена"
},
sampleKey: "Строка с доменом",
}
en.ts
const en: typeof langKey = {
global: {
keyWithoutSpecificDomain: "String without domain"
},
sampleKey: "String with domain",
}
Folder acrhitecture
.
└── src/
├── bootstrap/
│ └── i18n/
│ ├── i18n.ts
│ └── dictionaries/
│ └── common/
│ ├── en.ts
│ └── ru.ts
└── feature/
└── common/
└── lang-keys/
├── common.lang-keys.ts
└── user.lang-key.ts
i18n.ts configuring i18next.
langKey.ts contains object of all possible keys for translation.
ru.ts object of russian keys.
en.ts object of english keys.
Configuration
Before usage i18next's translation hook, it must be configured with languages it should support.
example of initialization
export const i18nInstance = createInstance();
export enum LANGS {
EN = "en",
RU = "ru",
}
const nameSpaces = [commonLangNs, userLangNs];
export const getI18n = async (params: {
lng: LANGS;
resources?: Resource;
ns?: string;
}) => {
const { lng, ns, resources } = params;
if (i18nInstance.isInitialized) {
await i18nInstance.changeLanguage(lng);
return {
i18n: i18nInstance,
resources: i18nInstance.services.resourceStore.data,
ns: nameSpaces,
defaultNs: commonLangNs,
t: i18nInstance.t,
};
}
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(language: LANGS, namespace: string) =>
import(`./dictionaries/${namespace}/${language}.ts`),
),
)
.init({
...getOptions(lng, ns),
resources,
ns: nameSpaces,
defaultNS: commonLangNs,
preload: resources ? [] : languages,
});
await i18nInstance.init();
return {
i18n: i18nInstance,
resources: i18nInstance.services.resourceStore.data,
t: i18nInstance.t,
};
};
export async function getServerTranslation(
lng: LANGS,
ns?: string,
options: { keyPrefix?: string } = {},
) {
const { i18n } = await getI18n({ lng });
// console.log(i18n);
return {
t: i18n.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix),
i18n: i18nInstance,
};
}
Provider and client support
To support client side translations with once loaded data in the server we can use provider component like this file.
Then we can use this provider in one main layout of the app like this file.
Combination with failure handling and params validation
- For combination with failure hanlding, please check this article or this document in the boilerplate
- Also for combination with params please check the feature layer architecture article.
Conclusion
By implementing this structured approach to localization, we've solved three critical pain points:
- Type-safe translations - No more hardcoded string keys
- Automated synchronization - Changes propagate across all language files
- Scalable architecture - Clean separation between domains
This system integrates seamlessly with:
- Failure handling
- Param validation
- Next.js server/client components
If you found this article helpful, I’d be truly grateful if you could:
⭐ Star the repository to support its visibility