Skip to content

Commit 7b7b07e

Browse files
authored
Merge pull request #25174 from abpframework/feature/url-based-localization
feat: Implement route-based culture support in localization.
2 parents 7a40be2 + 5e3ba18 commit 7b7b07e

90 files changed

Lines changed: 3185 additions & 64 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# SEO-Friendly Localized URLs in ABP with a Single Line of Configuration
2+
3+
ABP has always supported language switching via the `?culture=en` query string and the culture cookie. That works fine for most applications — but it has a limitation that shows up quickly once SEO or link-sharing matters.
4+
5+
Consider a book-store app where users browse in their language:
6+
7+
- A Spanish user shares a product link. The recipient opens it in English because the cookie on *their* machine says `en`.
8+
- Search engines crawl the same URL in every language, making it impossible to create separate sitemaps per locale.
9+
- A user shares a link like `/Books/Detail?id=42&culture=es`. When the server processes the request, it sets the culture cookie and then redirects to `/Books/Detail?id=42` — stripping the `?culture=` parameter. The shared link no longer carries the intended language.
10+
11+
Embedding the culture in the URL path — `/es/books`, `/zh-Hans/about` — solves all three. Each language has its own stable URL, readable by humans and index-friendly for search engines.
12+
13+
ABP supports this out of the box. You opt in with a single configuration property, and the framework takes care of routing, URL generation, menu links, and language switching automatically.
14+
15+
## Enabling URL-Based Localization
16+
17+
In your ABP module class, add:
18+
19+
```csharp
20+
Configure<AbpRequestLocalizationOptions>(options =>
21+
{
22+
options.UseRouteBasedCulture = true;
23+
});
24+
```
25+
26+
That is the only change you need to make.
27+
28+
## MVC / Razor Pages
29+
30+
MVC and Razor Pages have the most complete support — everything works automatically. No code changes needed in your pages or controllers.
31+
32+
![MVC sample — English](images/mvc-home-en.png)
33+
34+
![MVC sample — Turkish](images/mvc-home-tr.png)
35+
36+
## What Happens Automatically
37+
38+
When you set `UseRouteBasedCulture = true`, ABP automatically:
39+
40+
- Registers ASP.NET Core's built-in [`RouteDataRequestCultureProvider`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.localization.routing.routedatarequestcultureprovider) to detect culture from the URL path.
41+
- Adds a `{culture}/{controller}/{action}` conventional route for MVC controllers, with a route constraint to prevent non-culture URL segments (like `/enterprise/products`) from matching.
42+
- Adds `{culture}/...` route selectors to all Razor Pages at startup.
43+
- Injects the current culture into all `Url.Page()` and `Url.Action()` calls, so generated URLs automatically include the culture prefix.
44+
- Prepends the culture prefix to navigation menu item URLs.
45+
46+
You do not need to configure these individually.
47+
48+
## URL Generation Just Works
49+
50+
In a Razor Page or view running under a culture-prefixed URL (say, `/zh-Hans/Books`), you do not need to pass a `culture` parameter anywhere:
51+
52+
```cshtml
53+
@Url.Page("/Books/Detail", new { id = book.Id })
54+
@* Generates: /zh-Hans/Books/Detail?id=42 *@
55+
56+
@Url.Action("About", "Home")
57+
@* Generates: /zh-Hans/Home/About *@
58+
```
59+
60+
If you explicitly pass a different `culture` value, that takes precedence — so cross-language links are also straightforward:
61+
62+
```cshtml
63+
@Url.Page("/Books/Index", new { culture = "tr" })
64+
@* Generates: /tr/Books *@
65+
```
66+
67+
## Language Switching
68+
69+
The built-in ABP language switcher already works with route-based culture. When a user switches language, the culture segment in the URL is automatically replaced:
70+
71+
| Current URL | Switch to | Redirect to |
72+
|---|---|---|
73+
| `/tr/books` | `en` | `/en/books` |
74+
| `/zh-Hans/about` | `en` | `/en/about` |
75+
| `/tenant-a/zh-Hans/about` | `en` | `/tenant-a/en/about` |
76+
77+
No theme changes, no language switcher changes — the existing UI component just works.
78+
79+
## Blazor Support
80+
81+
Blazor Server and Blazor WebAssembly (WebApp) both support URL-based localization. Culture detection and cookie persistence work automatically on the initial page load (SSR). Menu URLs and language switching also work automatically.
82+
83+
![Blazor Server sample](images/blazor-server-zh-hans.png)
84+
85+
![Blazor WebApp sample](images/blazor-webapp-tr.png)
86+
87+
ABP's built-in module pages (Identity, Settings, etc.) also work with URL-based localization out of the box:
88+
89+
![Identity module — User Management](images/module-identity-users.png)
90+
91+
### Manual step: Blazor component routes
92+
93+
The only manual step for Blazor is adding `@page "/{culture}/..."` routes to your own pages. ASP.NET Core does not support automatically adding route selectors to Blazor components (unlike Razor Pages), so you must add them explicitly:
94+
95+
```razor
96+
@page "/"
97+
@page "/{culture}"
98+
99+
@code {
100+
[Parameter]
101+
public string? Culture { get; set; }
102+
}
103+
```
104+
105+
```razor
106+
@page "/Products"
107+
@page "/{culture}/Products"
108+
109+
@code {
110+
[Parameter]
111+
public string? Culture { get; set; }
112+
}
113+
```
114+
115+
> **ABP's built-in module pages** (Identity, Tenant Management, Settings, Account, etc.) already ship with `@page "/{culture}/..."` route variants. You only need to add these routes to your own application pages.
116+
117+
### Blazor WebApp (WASM) configuration
118+
119+
The WASM client project does not need any `UseRouteBasedCulture` configuration. It reads the setting from the server automatically.
120+
121+
```csharp
122+
// Server project — the only place you need to configure
123+
Configure<AbpRequestLocalizationOptions>(options =>
124+
{
125+
options.UseRouteBasedCulture = true;
126+
});
127+
```
128+
129+
## Multi-Tenancy
130+
131+
URL-based localization is fully compatible with ABP's multi-tenant routing. Language switching supports tenant-prefixed URLs, so `/tenant-a/zh-Hans/About` correctly switches to `/tenant-a/en/About` without any additional configuration.
132+
133+
## UI Framework Support Overview
134+
135+
| UI Framework | Route Registration | URL Generation | Menu URLs | Language Switch | Manual Work |
136+
|---|---|---|---|---|---|
137+
| **MVC / Razor Pages** | Automatic | Automatic | Automatic | Automatic | None |
138+
| **Blazor Server** | Manual `@page` routes | N/A | Automatic | Automatic | Add `{culture}` route to pages |
139+
| **Blazor WebApp (WASM)** | Manual `@page` routes | N/A | Automatic | Automatic | Add `{culture}` route to pages |
140+
141+
## Running the Sample
142+
143+
A runnable sample is available at [abp-samples/UrlBasedLocalization](https://github.com/abpframework/abp-samples/tree/master/UrlBasedLocalization), with three projects:
144+
145+
| Project | UI Type | URL | Command |
146+
|---|---|---|---|
147+
| `BookStore.Mvc` | MVC / Razor Pages | `https://localhost:44335` | `dotnet run --project src/BookStore.Mvc` |
148+
| `BookStore.Blazor.Server` | Blazor Server | `https://localhost:44336` | `dotnet run --project src/BookStore.Blazor.Server` |
149+
| `BookStore.Blazor.WebApp` | Blazor WebApp (InteractiveAuto) | `https://localhost:44337` | `dotnet run --project src/BookStore.Blazor.WebApp` |
150+
151+
Supported languages: English, Türkçe, Français, 简体中文.
152+
153+
## Summary
154+
155+
To add SEO-friendly localized URL paths to your ABP application:
156+
157+
1. Set `options.UseRouteBasedCulture = true` in your module.
158+
2. For **Blazor** projects, add `@page "/{culture}/..."` routes to your own pages.
159+
160+
Everything else — route registration, URL generation, menu links, and language switching — is handled automatically.
161+
162+
## References
163+
164+
- [URL-Based Localization — ABP Documentation](https://abp.io/docs/latest/framework/fundamentals/url-based-localization)
165+
- [Localization — ABP Documentation](https://abp.io/docs/latest/framework/fundamentals/localization)
166+
- [abp-samples/UrlBasedLocalization — GitHub](https://github.com/abpframework/abp-samples/tree/master/UrlBasedLocalization)
167+
- [Request Localization in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/select-language-culture)
154 KB
Loading
83.5 KB
Loading
82.2 KB
Loading
34.3 KB
Loading
92.4 KB
Loading
93 KB
Loading

docs/en/framework/fundamentals/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The following documents explains the fundamental building blocks to create ABP s
1717
* [Dependency Injection](./dependency-injection.md)
1818
* [Exception Handling](./exception-handling.md)
1919
* [Localization](./localization.md)
20+
* [URL-Based Localization](./url-based-localization.md)
2021
* [Logging](./logging.md)
2122
* [Object Extensions](./object-extensions.md)
2223
* [Options](./options.md)

docs/en/framework/fundamentals/localization.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ Configure<AbpLocalizationOptions>(options =>
294294
});
295295
```
296296

297+
## URL-Based Localization
298+
299+
ABP supports embedding the culture code directly in the URL path (e.g. `/en/products`, `/zh-Hans/about`), which is useful for SEO-friendly and shareable localized URLs. See the [URL-Based Localization](./url-based-localization.md) document for details.
300+
297301
## The Client Side
298302

299303
See the following documents to learn how to reuse the same localization texts in the JavaScript side;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
````json
2+
//[doc-seo]
3+
{
4+
"Description": "Learn how to use ABP's URL-based localization to embed culture in the URL path, enabling SEO-friendly and shareable localized URLs."
5+
}
6+
````
7+
8+
# URL-Based Localization
9+
10+
ABP supports embedding the current culture directly in the URL path, for example `/tr/products` or `/en/about`. This approach is widely used by documentation sites, e-commerce platforms, and any site that needs SEO-friendly, shareable localized URLs.
11+
12+
By default, ABP detects language from QueryString (`?culture=tr`), Cookie, and `Accept-Language` header. URL path detection is **opt-in** and fully backward-compatible.
13+
14+
## Enabling URL-Based Localization
15+
16+
Configure the `AbpRequestLocalizationOptions` in your [module class](../architecture/modularity/basics.md):
17+
18+
````csharp
19+
Configure<AbpRequestLocalizationOptions>(options =>
20+
{
21+
options.UseRouteBasedCulture = true;
22+
});
23+
````
24+
25+
That's all you need. The framework automatically handles the rest.
26+
27+
## What Happens Automatically
28+
29+
When you set `UseRouteBasedCulture` to `true`, ABP automatically registers the following:
30+
31+
* **`RouteDataRequestCultureProvider`** — A built-in ASP.NET Core provider that reads `{culture}` from route data. ABP inserts it after `QueryStringRequestCultureProvider` and before `CookieRequestCultureProvider`.
32+
* **`{culture}/{controller}/{action}` route** — A conventional route for MVC controllers. The `{culture}` parameter uses a custom route constraint (`AbpCultureRouteConstraint`) that only matches culture values configured in `AbpLocalizationOptions.Languages`, so URLs like `/enterprise/products` are not mistaken for culture-prefixed routes.
33+
* **`AbpCultureRoutePagesConvention`** — An `IPageRouteModelConvention` that adds `{culture}/...` route selectors to all Razor Pages.
34+
* **`AbpCultureRouteUrlHelperFactory`** — Replaces the default `IUrlHelperFactory` to auto-inject culture into `Url.Page()` and `Url.Action()` calls.
35+
* **`AbpCultureMenuItemUrlProvider`** — Prepends the culture prefix to navigation menu item URLs (MVC / Blazor Server).
36+
* **`AbpWasmCultureMenuItemUrlProvider`** — Prepends the culture prefix to menu item URLs in Blazor WebAssembly (reads the `UseRouteBasedCulture` flag from `/api/abp/application-configuration`).
37+
38+
You do not need to configure these individually.
39+
40+
## URL Generation
41+
42+
When a request has a `{culture}` route value, all URL generation methods automatically include the culture prefix:
43+
44+
````csharp
45+
// In a Razor Page — culture is auto-injected, no manual parameter needed
46+
@Url.Page("/About") // Generates: /zh-Hans/About
47+
@Url.Action("About", "Home") // Generates: /zh-Hans/Home/About
48+
````
49+
50+
Menu items registered via `IMenuContributor` also automatically get the culture prefix. No changes are needed in your menu contributors or theme.
51+
52+
## Language Switching
53+
54+
ABP's built-in language switcher (the `/Abp/Languages/Switch` action) automatically replaces the culture segment in the `returnUrl`. The controller reads the culture from the request cookie to identify the current page culture and replaces it with the new one:
55+
56+
| Before switching | After switching to English |
57+
|---|---|
58+
| `/tr/products` | `/en/products` |
59+
| `/tenant-a/zh-Hans/about` | `/tenant-a/en/about` |
60+
| `/home?culture=tr&ui-culture=tr` | `/home?culture=en&ui-culture=en` |
61+
| `/about` (no prefix) | `/about` (unchanged) |
62+
63+
No changes are needed in any theme or language switcher component.
64+
65+
## MVC / Razor Pages
66+
67+
MVC and Razor Pages have the most complete support. Everything works automatically when `UseRouteBasedCulture = true` — route registration, URL generation, menu links, and language switching. **No code changes are needed in your pages or controllers.**
68+
69+
## Blazor Server
70+
71+
Blazor Server uses SignalR (WebSocket) for the interactive circuit. The HTTP middleware pipeline only runs on the **initial page load** — subsequent interactions happen over the WebSocket connection. ABP handles this by persisting the detected URL culture to a **Cookie** on the first request, so the entire Blazor circuit uses the correct language.
72+
73+
Culture detection, cookie persistence, menu URLs, and language switching all work automatically. No additional configuration is needed beyond the `UseRouteBasedCulture` option.
74+
75+
### What requires manual changes
76+
77+
**Blazor component routes**: ASP.NET Core does not provide an `IPageRouteModelConvention` equivalent for Blazor components. You must manually add the `{culture}` route to each page:
78+
79+
````razor
80+
@page "/"
81+
@page "/{culture}"
82+
83+
@code {
84+
[Parameter]
85+
public string? Culture { get; set; }
86+
}
87+
````
88+
89+
````razor
90+
@page "/About"
91+
@page "/{culture}/About"
92+
93+
@code {
94+
[Parameter]
95+
public string? Culture { get; set; }
96+
}
97+
````
98+
99+
> This applies to your own application pages. ABP built-in module pages (Identity, Tenant Management, Settings, Account, etc.) already include `@page "/{culture}/..."` routes out of the box — you do not need to add them manually.
100+
101+
## Blazor WebAssembly (WebApp)
102+
103+
Blazor WebAssembly (WASM) runs in the browser. On the **first page load**, the server renders the page via SSR, and the culture is detected from the URL. After WASM downloads, subsequent renders run in the browser. The WASM app fetches `/api/abp/application-configuration` from the server to get the current culture, so the culture stays consistent.
104+
105+
Culture detection, cookie persistence, menu URLs, and language switching all work automatically. The WASM client reads the `UseRouteBasedCulture` flag from the server via `/api/abp/application-configuration`, so no client-side configuration is needed.
106+
107+
### What requires manual changes
108+
109+
Same as Blazor Server — you must manually add `@page "/{culture}/..."` routes to your Blazor pages.
110+
111+
## Angular
112+
113+
The [ABP Angular UI](../ui/angular/quick-start.md) runs in the browser. The server still applies `UseRouteBasedCulture`; the client reads **`localization.useRouteBasedCulture`** from `/api/abp/application-configuration` (same payload as other UI types). There is no separate Angular setting.
114+
115+
### Routing
116+
117+
Angular does not add a culture segment to your route config automatically. Use **`withOptionalRouteCulturePrefix`** from **`@abp/ng.core`** so one route tree matches both **`/identity/users`** and **`/en/identity/users`** (the first path segment is matched only when it looks like a culture code, e.g. `en`, `tr`, `zh-Hans`).
118+
119+
````typescript
120+
import { Routes } from '@angular/router';
121+
import { withOptionalRouteCulturePrefix } from '@abp/ng.core';
122+
123+
const appRoutesCore: Routes = [
124+
// ... your routes (path: '', 'account', 'identity', lazy children, etc.)
125+
];
126+
127+
export const appRoutes = withOptionalRouteCulturePrefix(appRoutesCore);
128+
````
129+
130+
![Angular: routes wrapped with optional culture prefix](../../images/url-based-localization-angular-routes.png)
131+
132+
### URL → session language
133+
134+
When **`useRouteBasedCulture`** is **true**, **`RouteBasedCultureService`** (from `@abp/ng.core`) keeps the session language aligned with the first URL segment after navigation. This runs during application bootstrap and on each **`NavigationEnd`**.
135+
136+
### Menu links, breadcrumbs, and `routerLink`
137+
138+
Menu paths from **`RoutesService`** are usually **without** a culture prefix (`/identity/users`). Use the **`abpRouteCultureUrl`** pipe on **`routerLink`** (or **`RouteBasedCultureUrlService.prefixPathWithCulture`**) so links navigate to **`/en/identity/users`** when route-based culture is enabled. The **Basic** theme navigation and **Theme Shared** breadcrumb links follow this pattern.
139+
140+
![Angular: culture-prefixed menu or URL bar](../../images/url-based-localization-angular-menu-url.png)
141+
142+
### Language switcher (toolbar)
143+
144+
If the user selects a language in the UI, call **`RouteBasedCultureUrlService.applyLanguageSelection(cultureName)`** (or **`navigateToUrlWithCulture`**) instead of only updating the session language. That rewrites the current URL’s culture segment (or prepends it) so the address bar and session stay consistent; **`RouteBasedCultureService`** then picks up the culture from the URL after navigation.
145+
146+
### Active menu, breadcrumbs, and route matching
147+
148+
The browser URL may be **`/en/identity/users`** while menu items and **`RoutesService`** paths stay **`/identity/users`**. For comparisons (active state, **`findRoute`**, permission guard, dynamic layout), normalize the current URL with **`RouteBasedCultureUrlService.normalizeForMenuMatch`** (or **`stripCulturePrefixIfEnabled`**) or use **`getRoutePathForMatching`** where **`getRoutePath`** was used.
149+
150+
### Configuration refresh
151+
152+
**`RouteBasedCultureUrlService`** refreshes its cached **`useRouteBasedCulture`** and **languages** when application configuration is updated (for example after **`refreshAppState`**), so hot paths do not query configuration on every change detection cycle.
153+
154+
## Multi-Tenancy Compatibility
155+
156+
URL-based localization is fully compatible with [multi-tenancy URL routing](../architecture/multi-tenancy/index.md). The culture route is registered as a conventional route `{culture}/{controller}/{action}`. If your application uses tenant routing (e.g., `/{tenant}/...`), the tenant middleware strips the tenant segment before routing, and the culture segment is handled separately.
157+
158+
Language switching also supports tenant-prefixed URLs. For example, `/tenant-a/zh-Hans/About` correctly switches to `/tenant-a/en/About`.
159+
160+
## API Routes
161+
162+
Routes like `/api/products` have no `{culture}` segment, so `RouteDataRequestCultureProvider` returns `null` and falls through to the next provider (Cookie → `Accept-Language` → default). API routes are completely unaffected.
163+
164+
## Culture Detection Priority
165+
166+
ASP.NET Core has a built-in [`RouteDataRequestCultureProvider`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.localization.routing.routedatarequestcultureprovider) (in `Microsoft.AspNetCore.Localization.Routing`) that reads culture from route data, but it is not included in the default provider list. When `UseRouteBasedCulture` is enabled, ABP inserts it after `QueryStringRequestCultureProvider` and before `CookieRequestCultureProvider`. The resulting provider order is:
167+
168+
1. `QueryStringRequestCultureProvider` (ASP.NET Core default — useful for debugging and testing)
169+
2. `RouteDataRequestCultureProvider` (URL path — inserted by ABP when enabled)
170+
3. `CookieRequestCultureProvider` (ASP.NET Core default)
171+
4. `AcceptLanguageHeaderRequestCultureProvider` (ASP.NET Core default)
172+
173+
If a URL contains an invalid culture code (e.g. `/xyz1234/page`), `RequestLocalizationMiddleware` ignores it and falls through to the next provider. No error is thrown.

0 commit comments

Comments
 (0)