Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dc3e633
feat: Implement route-based culture support in localization.
maliming Mar 29, 2026
6f15b2f
feat: Enhance URL-based localization support for Blazor components an…
maliming Mar 30, 2026
7d2686d
refactor: Remove unnecessary Blazor web app configuration from locali…
maliming Mar 30, 2026
0292b2b
fix: Update fallback culture handling in AbpCultureMenuItemUrlProvide…
maliming Mar 30, 2026
cd47809
feat: Add culture parameter to account, identity, user, setting, and …
maliming Mar 30, 2026
0880b23
feat: Implement URL-based localization support for Blazor components …
maliming Mar 30, 2026
0d239d4
feat: Refactor LoginDisplay and BasicThemeToolbarContributor for impr…
maliming Mar 30, 2026
c36823f
feat: Enhance route-based culture handling by inserting RouteDataRequ…
maliming Mar 30, 2026
bb78038
feat: Refactor culture prefix handling by moving logic to MenuItemCul…
maliming Mar 31, 2026
5559596
feat: Implement route-based culture navigation helper for improved lo…
maliming Mar 31, 2026
5525449
feat: Add route-based culture support with custom route constraint an…
maliming Apr 1, 2026
3a16c04
Optimised images with calibre/image-actions
github-actions[bot] Apr 1, 2026
cb41228
feat: Enhance documentation for URL-based localization with additiona…
maliming Apr 1, 2026
209c907
Optimised images with calibre/image-actions
github-actions[bot] Apr 1, 2026
299b428
fix: Adjust route constraint to handle URL generation without HttpCon…
maliming Apr 1, 2026
e2e6f04
Merge branch 'feature/url-based-localization' of github.com:abpframew…
maliming Apr 1, 2026
ceede87
Optimised images with calibre/image-actions
github-actions[bot] Apr 1, 2026
420c860
feat: Implement URL-based culture handling in Blazor and MVC requests
maliming Apr 1, 2026
6895d24
feat: Enhance URL-based culture handling with new interfaces and asyn…
maliming Apr 1, 2026
2bbf8d6
feat: Add MenuItemCulturePrefixHelper to AbpCultureMenuItemUrlProvide…
maliming Apr 1, 2026
c98a08b
feat: Implement route-based culture handling with new helpers and tests
maliming Apr 2, 2026
bd86923
update: dynamic layout for url matching
sumeyyeKurtulus Apr 3, 2026
074bf9d
update: permission guard to find the matching url
sumeyyeKurtulus Apr 3, 2026
a257b74
add: a pipe to meet the language changes
sumeyyeKurtulus Apr 3, 2026
337e649
add: services, utils and tests for the url based localization configu…
sumeyyeKurtulus Apr 3, 2026
9d0e040
update: routes to implement related pipe for url based localization
sumeyyeKurtulus Apr 3, 2026
34d5bc1
update: breadcrumbs for url based localization
sumeyyeKurtulus Apr 3, 2026
a887fa9
update: angular documentation for url-based localization
sumeyyeKurtulus Apr 3, 2026
5e3ba18
Optimised images with calibre/image-actions
github-actions[bot] Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions docs/en/Community-Articles/2026-03-29-Url-Based-Localization/POST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# SEO-Friendly Localized URLs in ABP with a Single Line of Configuration

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.

Consider a book-store app where users browse in their language:

- A Spanish user shares a product link. The recipient opens it in English because the cookie on *their* machine says `en`.
- Search engines crawl the same URL in every language, making it impossible to create separate sitemaps per locale.
- 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.

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.

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.

## Enabling URL-Based Localization

In your ABP module class, add:

```csharp
Configure<AbpRequestLocalizationOptions>(options =>
{
options.UseRouteBasedCulture = true;
});
```

That is the only change you need to make.

## MVC / Razor Pages

MVC and Razor Pages have the most complete support — everything works automatically. No code changes needed in your pages or controllers.

![MVC sample — English](images/mvc-home-en.png)

![MVC sample — Turkish](images/mvc-home-tr.png)

## What Happens Automatically

When you set `UseRouteBasedCulture = true`, ABP automatically:

- 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.
- 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.
- Adds `{culture}/...` route selectors to all Razor Pages at startup.
- Injects the current culture into all `Url.Page()` and `Url.Action()` calls, so generated URLs automatically include the culture prefix.
- Prepends the culture prefix to navigation menu item URLs.

You do not need to configure these individually.

## URL Generation Just Works

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:

```cshtml
@Url.Page("/Books/Detail", new { id = book.Id })
@* Generates: /zh-Hans/Books/Detail?id=42 *@

@Url.Action("About", "Home")
@* Generates: /zh-Hans/Home/About *@
```

If you explicitly pass a different `culture` value, that takes precedence — so cross-language links are also straightforward:

```cshtml
@Url.Page("/Books/Index", new { culture = "tr" })
@* Generates: /tr/Books *@
```

## Language Switching

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:

| Current URL | Switch to | Redirect to |
|---|---|---|
| `/tr/books` | `en` | `/en/books` |
| `/zh-Hans/about` | `en` | `/en/about` |
| `/tenant-a/zh-Hans/about` | `en` | `/tenant-a/en/about` |

No theme changes, no language switcher changes — the existing UI component just works.

## Blazor Support

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.

![Blazor Server sample](images/blazor-server-zh-hans.png)

![Blazor WebApp sample](images/blazor-webapp-tr.png)

ABP's built-in module pages (Identity, Settings, etc.) also work with URL-based localization out of the box:

![Identity module — User Management](images/module-identity-users.png)

### Manual step: Blazor component routes

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:

```razor
@page "/"
@page "/{culture}"

@code {
[Parameter]
public string? Culture { get; set; }
}
```

```razor
@page "/Products"
@page "/{culture}/Products"

@code {
[Parameter]
public string? Culture { get; set; }
}
```

> **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.

### Blazor WebApp (WASM) configuration

The WASM client project does not need any `UseRouteBasedCulture` configuration. It reads the setting from the server automatically.

```csharp
// Server project — the only place you need to configure
Configure<AbpRequestLocalizationOptions>(options =>
{
options.UseRouteBasedCulture = true;
});
```

## Multi-Tenancy

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.

## UI Framework Support Overview

| UI Framework | Route Registration | URL Generation | Menu URLs | Language Switch | Manual Work |
|---|---|---|---|---|---|
| **MVC / Razor Pages** | Automatic | Automatic | Automatic | Automatic | None |
| **Blazor Server** | Manual `@page` routes | N/A | Automatic | Automatic | Add `{culture}` route to pages |
| **Blazor WebApp (WASM)** | Manual `@page` routes | N/A | Automatic | Automatic | Add `{culture}` route to pages |

## Running the Sample

A runnable sample is available at [abp-samples/UrlBasedLocalization](https://github.com/abpframework/abp-samples/tree/master/UrlBasedLocalization), with three projects:

| Project | UI Type | URL | Command |
|---|---|---|---|
| `BookStore.Mvc` | MVC / Razor Pages | `https://localhost:44335` | `dotnet run --project src/BookStore.Mvc` |
| `BookStore.Blazor.Server` | Blazor Server | `https://localhost:44336` | `dotnet run --project src/BookStore.Blazor.Server` |
| `BookStore.Blazor.WebApp` | Blazor WebApp (InteractiveAuto) | `https://localhost:44337` | `dotnet run --project src/BookStore.Blazor.WebApp` |

Supported languages: English, Türkçe, Français, 简体中文.

## Summary

To add SEO-friendly localized URL paths to your ABP application:

1. Set `options.UseRouteBasedCulture = true` in your module.
2. For **Blazor** projects, add `@page "/{culture}/..."` routes to your own pages.

Everything else — route registration, URL generation, menu links, and language switching — is handled automatically.

## References

- [URL-Based Localization — ABP Documentation](https://abp.io/docs/latest/framework/fundamentals/url-based-localization)
- [Localization — ABP Documentation](https://abp.io/docs/latest/framework/fundamentals/localization)
- [abp-samples/UrlBasedLocalization — GitHub](https://github.com/abpframework/abp-samples/tree/master/UrlBasedLocalization)
- [Request Localization in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/select-language-culture)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/en/framework/fundamentals/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The following documents explains the fundamental building blocks to create ABP s
* [Dependency Injection](./dependency-injection.md)
* [Exception Handling](./exception-handling.md)
* [Localization](./localization.md)
* [URL-Based Localization](./url-based-localization.md)
* [Logging](./logging.md)
* [Object Extensions](./object-extensions.md)
* [Options](./options.md)
Expand Down
4 changes: 4 additions & 0 deletions docs/en/framework/fundamentals/localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ Configure<AbpLocalizationOptions>(options =>
});
```

## URL-Based Localization

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.

## The Client Side

See the following documents to learn how to reuse the same localization texts in the JavaScript side;
Expand Down
173 changes: 173 additions & 0 deletions docs/en/framework/fundamentals/url-based-localization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
````json
//[doc-seo]
{
"Description": "Learn how to use ABP's URL-based localization to embed culture in the URL path, enabling SEO-friendly and shareable localized URLs."
}
````

# URL-Based Localization

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.

By default, ABP detects language from QueryString (`?culture=tr`), Cookie, and `Accept-Language` header. URL path detection is **opt-in** and fully backward-compatible.

## Enabling URL-Based Localization

Configure the `AbpRequestLocalizationOptions` in your [module class](../architecture/modularity/basics.md):

````csharp
Configure<AbpRequestLocalizationOptions>(options =>
{
options.UseRouteBasedCulture = true;
});
````

That's all you need. The framework automatically handles the rest.

## What Happens Automatically

When you set `UseRouteBasedCulture` to `true`, ABP automatically registers the following:

* **`RouteDataRequestCultureProvider`** — A built-in ASP.NET Core provider that reads `{culture}` from route data. ABP inserts it after `QueryStringRequestCultureProvider` and before `CookieRequestCultureProvider`.
* **`{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.
* **`AbpCultureRoutePagesConvention`** — An `IPageRouteModelConvention` that adds `{culture}/...` route selectors to all Razor Pages.
* **`AbpCultureRouteUrlHelperFactory`** — Replaces the default `IUrlHelperFactory` to auto-inject culture into `Url.Page()` and `Url.Action()` calls.
* **`AbpCultureMenuItemUrlProvider`** — Prepends the culture prefix to navigation menu item URLs (MVC / Blazor Server).
* **`AbpWasmCultureMenuItemUrlProvider`** — Prepends the culture prefix to menu item URLs in Blazor WebAssembly (reads the `UseRouteBasedCulture` flag from `/api/abp/application-configuration`).

You do not need to configure these individually.

## URL Generation

When a request has a `{culture}` route value, all URL generation methods automatically include the culture prefix:

````csharp
// In a Razor Page — culture is auto-injected, no manual parameter needed
@Url.Page("/About") // Generates: /zh-Hans/About
@Url.Action("About", "Home") // Generates: /zh-Hans/Home/About
````

Menu items registered via `IMenuContributor` also automatically get the culture prefix. No changes are needed in your menu contributors or theme.

## Language Switching

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:

| Before switching | After switching to English |
|---|---|
| `/tr/products` | `/en/products` |
| `/tenant-a/zh-Hans/about` | `/tenant-a/en/about` |
| `/home?culture=tr&ui-culture=tr` | `/home?culture=en&ui-culture=en` |
| `/about` (no prefix) | `/about` (unchanged) |

No changes are needed in any theme or language switcher component.

## MVC / Razor Pages

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.**

## Blazor Server

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.

Culture detection, cookie persistence, menu URLs, and language switching all work automatically. No additional configuration is needed beyond the `UseRouteBasedCulture` option.

### What requires manual changes

**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:

````razor
@page "/"
@page "/{culture}"

@code {
[Parameter]
public string? Culture { get; set; }
}
````

````razor
@page "/About"
@page "/{culture}/About"

@code {
[Parameter]
public string? Culture { get; set; }
}
````

> 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.

## Blazor WebAssembly (WebApp)

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.

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.

### What requires manual changes

Same as Blazor Server — you must manually add `@page "/{culture}/..."` routes to your Blazor pages.

## Angular

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.

### Routing

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`).

````typescript
import { Routes } from '@angular/router';
import { withOptionalRouteCulturePrefix } from '@abp/ng.core';

const appRoutesCore: Routes = [
// ... your routes (path: '', 'account', 'identity', lazy children, etc.)
];

export const appRoutes = withOptionalRouteCulturePrefix(appRoutesCore);
````

![Angular: routes wrapped with optional culture prefix](../../images/url-based-localization-angular-routes.png)

### URL → session language

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`**.

### Menu links, breadcrumbs, and `routerLink`

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.

![Angular: culture-prefixed menu or URL bar](../../images/url-based-localization-angular-menu-url.png)

### Language switcher (toolbar)

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.

### Active menu, breadcrumbs, and route matching

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.

### Configuration refresh

**`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.

## Multi-Tenancy Compatibility

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.

Language switching also supports tenant-prefixed URLs. For example, `/tenant-a/zh-Hans/About` correctly switches to `/tenant-a/en/About`.

## API Routes

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.

## Culture Detection Priority

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:

1. `QueryStringRequestCultureProvider` (ASP.NET Core default — useful for debugging and testing)
2. `RouteDataRequestCultureProvider` (URL path — inserted by ABP when enabled)
3. `CookieRequestCultureProvider` (ASP.NET Core default)
4. `AcceptLanguageHeaderRequestCultureProvider` (ASP.NET Core default)

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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading