Why localization breaks routing and metadata
When a second locale is added to a project, it's not the translation that breaks — it's the routing.
A parallel tree of paths appears, and from there everything depends on exactly where you decide which language to show: in the request middleware, in the layout, or inside the component.
If the decision is made in three places at once, you get a route conflict — some addresses serve the wrong locale, some redirect in a loop, and internal links point to a language the user didn't choose.
The second typical breakage is duplicate canonicals. As long as a page lives at a single address, the canonical tag is assembled trivially.
With multilingual routing, the same entity is available at several paths, and if canonical and hreflang are generated in different layers, the search engine sees competing versions of the same page.
This isn't fixed by editing a template but by a single source of truth: the locale and the path must be computed from one place, and alternative addresses must be generated from the same list of locales as the routes.
The third failure is the last to become noticeable: the translation gets lost in server components.
The client side picks up the language, but the server render outputs the default locale's text — the markup ends up with a mix of languages, and this hurts both search results and perception.
The reason is that dictionaries are loaded asynchronously and aren't always available at the server rendering stage.
Next-intl setup removes these problems at the architecture level: the locale becomes part of the route segment, dictionaries are distributed through a provider and are available both on the server and on the client, and metadata and alternative addresses are assembled from a shared description of locales.
Then adding a third language is a line in the configuration, not a revision of the entire path tree.
What correct internationalization delivers
Correct next-intl configuration removes the main pain of multilingual sites — the desync between routes, dictionaries, and markup. When locales, path prefixes, and the hreflang header are generated from a single source, each language version gets its own canonical address.
The search engine sees related pages rather than duplicates, and the search results stop "eating" translations in favor of the primary language.
The speed of adding a language no longer depends on the size of the project. A new dictionary is connected as a separate file with a flat key structure, routes pick up the locale automatically, and middleware determines the language from the request header and cookie.
In practice, this turns launching a language from a multi-week project into a task of a few days — without rewriting components.
Dictionary growth is a typical failure point. With hundreds of keys, manually searching for missing translations doesn't work, so a check is built into the build: a missing key breaks the build instead of quietly rendering an empty string in production.
This kind of control keeps dictionaries in a consistent state, even when several people work on translations in parallel.
Build stability directly affects the release cycle. Validating dictionary schemas at the build stage catches structural discrepancies between locales before deployment, so updating texts doesn't turn into an emergency rollback.
We lock down the key format and add the check to the pipeline — translation edits stop being a risk to production.
Taken together, this delivers predictable search results for each language, no duplicates in the index, and manageable content growth. Describe your task — we'll come back with an estimate and a setup plan for your stack.
Comparison of approaches to multilingualism
The choice of a multilingualism tool determines how much manual work remains for the team after launching the first language.
In practice, the decision is between three options: custom dictionaries, a third-party library, and next-intl — each covers its own class of tasks and has boundaries beyond which it starts to get in the way.
| Option | When it fits | Limitations |
|---|---|---|
| Manual dictionaries | One or two languages, static content, a couple dozen strings | No pluralization, number and date formatting; keys diverge between files; as languages grow, duplicates and "forgotten" strings appear |
| Third-party libraries | Exotic formats are needed, complex declension rules, non-standard routing | Pull their own layer on top of the framework, conflict with server components, require manual synchronization with updates |
| next-intl | A Next.js project with App Router, multiple languages, server-side rendering | Tied to the Next.js ecosystem; static export and non-standard routing schemes require separate configuration |
Manual dictionaries win at the start: zero dependencies, full control. The problem shows up with the third language, when forms with number and gender appear, and translations begin to live in different branches of the code.
Third-party libraries cover complex linguistics but add a layer of abstraction between the application and the framework. Every Next.js update turns into a compatibility check, and server components require workarounds.
next-intl integrates directly into Next.js routing and rendering: a locale segment in the URL, dictionaries on the server, locale-aware date and number formatting. The limitation is one — the tool is designed specifically for this stack, and outside of it the advantages are lost.
How next-intl setup works in a project
Setting up internationalization in Next.js is not about "plugging in a library" but about designing routing, dictionaries, and language-switching behavior so that no language breaks SEO or loses content. We work in stages, each ending with a verifiable result.
- Audit of the current localization. We look at how texts are structured now: hardcoded into components, scattered across separate files, or already extracted into dictionaries. We record the languages that are actually needed and the places where content is injected dynamically — that is exactly where untranslated strings most often surface.
- Designing the route scheme. We determine whether the locale will be in the path (
/en/...,/de/...) or in a subdomain, set the default locale and redirect rules. Indexing depends on this scheme: each language must have its own address and a correcthreflang. - Connecting dictionaries. We split translations by namespaces — shared, page-level, utility — and configure loading of only the needed set for a specific route. This way the size of transmitted data does not grow with the number of languages.
- Translating the interface and content. We replace hardcoded strings with keys, and separately handle date, number, and plural formats — they behave differently in different languages and require explicit rules rather than template substitution.
- Checking across all languages. We run every route in every locale: language switching, direct links, redirects, missing keys. We separately check that when the language changes, the user stays on the same page rather than landing on the home page.
- Handing over documentation. We provide the route scheme, dictionary structure, rules for adding a new language, and a verification checklist. The client's team can then extend localization without our involvement.
The key principle: a new language is added by editing the configuration and the translation file, not by rewriting routes. If launching a locale requires touching page code, the scheme was designed incorrectly, and we fix it before handover.
Artifacts after internationalization implementation
After internationalization implementation, the client receives not a "working translation" but a complete package that the team can use to add languages and edit texts without us. Below is what exactly remains in the repository and in the documentation.
- Routing and locale configuration. A settings file with the list of languages, the default locale, and the prefix strategy:
/en/catalogversuscatalogwith language detection from the request header. The behavior for the site root and for non-existent locales is documented separately — redirect or 404 page. - Route schema. A mapping table of "public address → page file → dictionary", including nested segments and dynamic parts. With it, a new developer understands the structure in minutes rather than reconstructing it from the code.
- Message dictionaries. Organized by namespaces: common interface texts, pages, emails, system errors. The format is unified across all languages, and keys are not duplicated between files.
- Translation update procedure. The order is described: who edits the source language, how changes reach the translator, what happens to keys that are missing in the target language. The application's behavior when a translation is missing is specified — substituting the source text instead of an empty string.
- Integrity checks. Automatic dictionary verification: key mismatches between languages and unused strings are caught before the build, not in production.
- Documentation for editors. Instructions for the content manager: where the text is located, how to add a language, what must not be changed in keys so as not to break the markup.
- Procedure for connecting new languages. A step-by-step list: add the locale to the configuration, create a dictionary, verify routes and page metadata. A single language is added without edits to the components.
We hand over the package together with a walkthrough: we show what is where and how to verify the result locally. Describe your task — we will get back to you with an estimate and a plan for your stack.
Case study: 4x faster launch of a new language
The project is a service marketplace with a catalog, a user account area, and a stream of user notifications.
The interface lived in three languages, and each new market was launched manually: developers copied pages for the new locale, strings were edited right in the markup, and some texts were pulled from the database without any language binding.
The fourth language took three weeks, and localization errors started pouring in almost immediately — untranslated blocks, "broken" dates and numbers, emails in the previous user's language.
A separate pain point was duplicates in search. The same product was accessible at several addresses with different language prefixes, canonical links were not set, and search engines indexed both versions. Rankings sagged, and traffic was distributed among clone pages instead of one.
We migrated the project to next-intl and moved all text into per-locale dictionaries: strings no longer live in the markup but are connected by key.
Routing got a single language prefix, a canonical address and a language versions map are automatically generated for each page — duplicates were closed at the generation level.
Formatting of dates, numbers, and currencies was handed over to built-in locale-aware tools, so regional settings no longer depend on who opened the page and where.
What changed in numbers
| Metric | Before | After |
|---|---|---|
| Time to launch a new language | 3 weeks | 4 days |
| Localization errors in a release | regular, dozens of edits | isolated, caught during review |
| Duplicate pages in search | present across all locales | none |
| Text edits without a developer | unavailable | edited in the dictionary |
The fourfold acceleration came not from the library itself but from the order it imposes: a single source of text, a unified address scheme, automatic formatting.
Adding a language now comes down to a new dictionary and one line of configuration — the engineer doesn't rewrite pages and doesn't risk touching the logic.
How is next-intl different from manual localization?
Manual localization starts with a harmless dictionary in a separate file, but it lives only until the first discrepancy between languages.
After that come keys that exist in English and are forgotten in German, strings with interpolation like Hello, {name} assembled through concatenation, and dates formatted by hand for each market.
Errors like these don't fail the build — they surface for the user, who sees an empty space instead of text or an English phrase in the middle of a German page.
next-intl flips this model: translations become a typed resource rather than a set of strings. Keys are described once, and referencing a non-existent key breaks the build, not production — a forgotten translation in a new language branch is caught before release.
Automatic value interpolation works through explicit parameters, so word order in different languages doesn't have to be adjusted by hand: the template describes meaning, not position.
A separate pain of the manual approach is declensions and plural forms.
In Russian, "1 товар", "2 товара", "5 товаров" are three different forms, in Arabic there are six, and handling this through conditions in code quickly turns into unreadable branching logic.
The library takes the rules from standard locale data and selects the right form by number itself, while the developer writes a single template instead of a set of conditions.
At the maintenance level, the difference is even more noticeable. With manual localization, adding a language means auditing all the code for hardcoded strings, and changing text requires synchronously editing several files.
With next-intl, a new language is connected via configuration and a translation file, while the key structure stays shared: an edit in one place doesn't diverge across branches.
That is exactly why on projects with two or more languages, manual localization becomes more expensive with every release over time, while the next-intl approach does not.
Technical concerns about internationalization
Let's go through three technical concerns that most often come up when setting up next-intl: speed, caching, and working with dynamic content. Below are short answers with the mechanics, not generalities.
Will performance suffer because of internationalization?
No, if messages are loaded at build time rather than at runtime. We keep dictionaries as static modules and connect them through server components — only the needed language goes to the client, without extra dictionaries.
Locale-based routing is handled at the middleware level, so redirects don't eat into response time. In practice, the difference between a monolingual and a multilingual version falls within the measurement error.
How do you cache pages if the language depends on the URL?
The locale is part of the path, which means each language version is cached separately and independently. This allows serving static content from the cache along the entire route without mixing languages.
For pages with personalization, we separate the static shell from the dynamic data: the shell is cached per locale, while user-specific blocks are loaded separately. This way the cache stays effective and the content stays up to date.
What should be done with dynamic content and translations from the database?
Strings that change without a release are stored in the database or in the content management system and pulled in by key. Static interface texts remain in dictionaries, while editable ones stay in the data source.
At build time we check that all keys are translated: if a key is missing, the build fails rather than serving emptiness in production. This eliminates a whole class of errors where text exists in one language but is an empty block in another.
Can a new language be added without rewriting the code?
Yes. Adding a locale is a new dictionary file and one line in the route configuration. Components and logic remain untouched because they work with keys rather than with text directly.
We design the key structure in advance so that word order and cases in different languages don't break the layout.
If you still have concerns about your stack — describe the task, and we'll come back with an estimate and a plan.
Let's discuss your localization task
To come back with an estimate and a localization work plan, we need a minimum of context about your application — we'll figure out the rest ourselves.
Describe the task in free form: which languages and regions are needed, whether translations already exist, how routing is set up, and where the texts come from. That's enough to estimate the scope and propose an approach.
What helps speed up the start of the project:
- A list of target locales and the priority between them — the structure of routes and the order of proofreading depend on it.
- The current state of the texts: where they currently live — in the markup, in the database, in translation files, or in spreadsheets with translators.
- Requirements for date, number, currency, and unit formats by region — they are usually forgotten, yet they break the interface more than anything else.
- The availability of ready-made translations and a glossary of terms, so as not to re-translate what has already been agreed upon.
- Access to the repository and the staging environment: we'll look at how the build is set up and propose an integration approach without rewriting the application.
- Constraints on deadlines and releases — we'll build the order of work around them so that localization doesn't block current tasks.
We work with internationalization at the architecture level: texts are extracted from the code, routes get a language prefix, and dictionaries are connected in such a way that adding a new locale doesn't require edits in the components.
This allows launching languages one at a time rather than waiting for a full translation of all sections.
Describe the task — we'll come back with an estimate, a work plan, and a proposed timeline. If convenient, attach a link to the application or screenshots of key screens: that way the estimate will be more accurate.







