Flutter go_router Localization: Complete Guide to Multilingual Navigation
go_router is the recommended routing solution for Flutter, and it integrates seamlessly with localization. This guide covers everything from localized route paths to language-based redirects, helping you build navigation that feels native to users in any language.
Why Localize Routes?
Consider these benefits:
- SEO for Flutter Web: Localized URLs like
/es/productosrank better in Spanish searches - User Experience: Bookmarks and shared links in users' language
- Professional Appearance:
/de/einstellungenlooks more professional than/de/settingsfor German users - Deep Linking: Support localized deep links from marketing campaigns
Project Setup
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
go_router: ^13.2.0
shared_preferences: ^2.2.2
provider: ^6.1.1
flutter:
generate: true
Basic Setup: Locale-Aware Router
Router Configuration
// lib/router/app_router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../providers/locale_provider.dart';
class AppRouter {
static GoRouter router(LocaleProvider localeProvider) {
return GoRouter(
refreshListenable: localeProvider,
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/products',
builder: (context, state) => const ProductsPage(),
),
GoRoute(
path: '/products/:id',
builder: (context, state) => ProductDetailPage(
id: state.pathParameters['id']!,
),
),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsPage(),
),
GoRoute(
path: '/about',
builder: (context, state) => const AboutPage(),
),
],
errorBuilder: (context, state) => NotFoundPage(
path: state.uri.toString(),
),
);
}
}
Locale Provider
// lib/providers/locale_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LocaleProvider extends ChangeNotifier {
static const String _localeKey = 'app_locale';
Locale _locale = const Locale('en');
bool _isInitialized = false;
Locale get locale => _locale;
bool get isInitialized => _isInitialized;
/// Initialize from saved preference
Future<void> initialize() async {
final prefs = await SharedPreferences.getInstance();
final savedLocale = prefs.getString(_localeKey);
if (savedLocale != null) {
_locale = Locale(savedLocale);
}
_isInitialized = true;
notifyListeners();
}
/// Change locale
Future<void> setLocale(Locale locale) async {
if (_locale == locale) return;
_locale = locale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_localeKey, locale.languageCode);
notifyListeners();
}
/// Get supported locales
static const supportedLocales = [
Locale('en'),
Locale('es'),
Locale('de'),
Locale('fr'),
Locale('ja'),
];
}
Main App Setup
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'providers/locale_provider.dart';
import 'router/app_router.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final localeProvider = LocaleProvider();
await localeProvider.initialize();
runApp(
ChangeNotifierProvider.value(
value: localeProvider,
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final localeProvider = context.watch<LocaleProvider>();
return MaterialApp.router(
routerConfig: AppRouter.router(localeProvider),
locale: localeProvider.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'Localized App',
);
}
}
Advanced: Localized URL Paths
For Flutter Web, you might want URLs in users' language:
- English:
/products - Spanish:
/productos - German:
/produkte
Localized Route Definitions
// lib/router/localized_routes.dart
/// Route path translations
class LocalizedRoutes {
static const Map<String, Map<String, String>> _routes = {
'home': {
'en': '/',
'es': '/',
'de': '/',
'fr': '/',
'ja': '/',
},
'products': {
'en': '/products',
'es': '/productos',
'de': '/produkte',
'fr': '/produits',
'ja': '/shouhin',
},
'settings': {
'en': '/settings',
'es': '/configuracion',
'de': '/einstellungen',
'fr': '/parametres',
'ja': '/settei',
},
'about': {
'en': '/about',
'es': '/acerca',
'de': '/ueber-uns',
'fr': '/a-propos',
'ja': '/gaiyou',
},
'cart': {
'en': '/cart',
'es': '/carrito',
'de': '/warenkorb',
'fr': '/panier',
'ja': '/kago',
},
'profile': {
'en': '/profile',
'es': '/perfil',
'de': '/profil',
'fr': '/profil',
'ja': '/purofairu',
},
};
/// Get localized path for a route key
static String getPath(String routeKey, String locale) {
return _routes[routeKey]?[locale] ?? _routes[routeKey]?['en'] ?? '/';
}
/// Get route key from any localized path
static String? getRouteKey(String path) {
for (final entry in _routes.entries) {
for (final localePath in entry.value.values) {
if (localePath == path || path.startsWith('$localePath/')) {
return entry.key;
}
}
}
return null;
}
/// Check if path matches any locale version of a route
static bool matchesRoute(String path, String routeKey) {
final routes = _routes[routeKey];
if (routes == null) return false;
return routes.values.any((p) => path == p || path.startsWith('$p/'));
}
/// Get all path variants for a route
static List<String> getAllPaths(String routeKey) {
return _routes[routeKey]?.values.toList() ?? [];
}
}
Router with Localized Paths
// lib/router/localized_router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'localized_routes.dart';
class LocalizedRouter {
final String locale;
LocalizedRouter(this.locale);
GoRouter createRouter(Listenable refreshListenable) {
return GoRouter(
refreshListenable: refreshListenable,
initialLocation: '/',
routes: _buildRoutes(),
redirect: _handleRedirect,
errorBuilder: (context, state) => NotFoundPage(
path: state.uri.toString(),
),
);
}
List<RouteBase> _buildRoutes() {
return [
// Home - same for all locales
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
// Products route with all locale variants
...LocalizedRoutes.getAllPaths('products').map(
(path) => GoRoute(
path: path,
builder: (context, state) => const ProductsPage(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) => ProductDetailPage(
id: state.pathParameters['id']!,
),
),
],
),
),
// Settings
...LocalizedRoutes.getAllPaths('settings').map(
(path) => GoRoute(
path: path,
builder: (context, state) => const SettingsPage(),
),
),
// About
...LocalizedRoutes.getAllPaths('about').map(
(path) => GoRoute(
path: path,
builder: (context, state) => const AboutPage(),
),
),
// Cart
...LocalizedRoutes.getAllPaths('cart').map(
(path) => GoRoute(
path: path,
builder: (context, state) => const CartPage(),
),
),
];
}
/// Redirect to localized path when locale changes
String? _handleRedirect(BuildContext context, GoRouterState state) {
final currentPath = state.uri.path;
// Find the route key for current path
final routeKey = LocalizedRoutes.getRouteKey(currentPath);
if (routeKey == null) return null;
// Get the correct path for current locale
final correctPath = LocalizedRoutes.getPath(routeKey, locale);
// If already on correct path, no redirect needed
if (currentPath == correctPath) return null;
// Handle paths with parameters (e.g., /products/123)
if (currentPath.contains('/')) {
final segments = currentPath.split('/');
final correctSegments = correctPath.split('/');
// Replace base path, keep parameters
if (segments.length > correctSegments.length) {
final params = segments.sublist(correctSegments.length);
return '$correctPath/${params.join('/')}';
}
}
return correctPath;
}
}
Language-Prefixed URLs (Web SEO Pattern)
For better SEO, prefix all URLs with the language code:
/en/products/es/productos/de/produkte
Shell Route with Language Prefix
// lib/router/language_prefixed_router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class LanguagePrefixedRouter {
static final supportedLocales = ['en', 'es', 'de', 'fr', 'ja'];
static GoRouter createRouter({
required Listenable refreshListenable,
required String Function() getCurrentLocale,
}) {
return GoRouter(
refreshListenable: refreshListenable,
initialLocation: '/${getCurrentLocale()}',
redirect: (context, state) => _handleRedirect(state, getCurrentLocale()),
routes: [
// Root redirect to current locale
GoRoute(
path: '/',
redirect: (context, state) => '/${getCurrentLocale()}',
),
// Language-prefixed shell route
ShellRoute(
builder: (context, state, child) {
// Extract locale from path
final pathLocale = _extractLocale(state.uri.path);
return LocaleShell(
locale: pathLocale,
child: child,
);
},
routes: [
// For each supported locale
for (final locale in supportedLocales)
GoRoute(
path: '/$locale',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: _getLocalizedPath('products', locale),
builder: (context, state) => const ProductsPage(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) => ProductDetailPage(
id: state.pathParameters['id']!,
),
),
],
),
GoRoute(
path: _getLocalizedPath('settings', locale),
builder: (context, state) => const SettingsPage(),
),
GoRoute(
path: _getLocalizedPath('about', locale),
builder: (context, state) => const AboutPage(),
),
],
),
],
),
],
);
}
static String _getLocalizedPath(String routeKey, String locale) {
const paths = {
'products': {'en': 'products', 'es': 'productos', 'de': 'produkte', 'fr': 'produits', 'ja': 'shouhin'},
'settings': {'en': 'settings', 'es': 'configuracion', 'de': 'einstellungen', 'fr': 'parametres', 'ja': 'settei'},
'about': {'en': 'about', 'es': 'acerca', 'de': 'ueber-uns', 'fr': 'a-propos', 'ja': 'gaiyou'},
};
return paths[routeKey]?[locale] ?? paths[routeKey]?['en'] ?? routeKey;
}
static String? _extractLocale(String path) {
final segments = path.split('/').where((s) => s.isNotEmpty).toList();
if (segments.isEmpty) return null;
final firstSegment = segments.first;
if (supportedLocales.contains(firstSegment)) {
return firstSegment;
}
return null;
}
static String? _handleRedirect(GoRouterState state, String currentLocale) {
final path = state.uri.path;
final pathLocale = _extractLocale(path);
// If no locale prefix, add current locale
if (pathLocale == null && path != '/') {
return '/$currentLocale$path';
}
return null;
}
}
/// Shell widget that sets locale based on URL
class LocaleShell extends StatelessWidget {
final String? locale;
final Widget child;
const LocaleShell({
super.key,
this.locale,
required this.child,
});
@override
Widget build(BuildContext context) {
// Sync URL locale with app locale
if (locale != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = context.read<LocaleProvider>();
if (provider.locale.languageCode != locale) {
provider.setLocale(Locale(locale!));
}
});
}
return child;
}
}
Navigation Helpers
Type-Safe Localized Navigation
// lib/router/navigation_helper.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../providers/locale_provider.dart';
import 'localized_routes.dart';
extension LocalizedNavigation on BuildContext {
/// Navigate to a route using its key (auto-localizes)
void goLocalized(String routeKey, {Map<String, String>? params}) {
final locale = read<LocaleProvider>().locale.languageCode;
var path = LocalizedRoutes.getPath(routeKey, locale);
if (params != null) {
params.forEach((key, value) {
path = path.replaceAll(':$key', value);
});
}
go(path);
}
/// Push a route using its key (auto-localizes)
void pushLocalized(String routeKey, {Map<String, String>? params}) {
final locale = read<LocaleProvider>().locale.languageCode;
var path = LocalizedRoutes.getPath(routeKey, locale);
if (params != null) {
params.forEach((key, value) {
path = path.replaceAll(':$key', value);
});
}
push(path);
}
}
// Usage in widgets:
class ProductCard extends StatelessWidget {
final String productId;
const ProductCard({super.key, required this.productId});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.pushLocalized(
'products',
params: {'id': productId},
),
child: // ... card content
);
}
}
Localized Link Widget
// lib/widgets/localized_link.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../providers/locale_provider.dart';
import '../router/localized_routes.dart';
class LocalizedLink extends StatelessWidget {
final String routeKey;
final Map<String, String>? params;
final Widget child;
const LocalizedLink({
super.key,
required this.routeKey,
this.params,
required this.child,
});
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => _navigate(context),
child: child,
),
);
}
void _navigate(BuildContext context) {
final locale = context.read<LocaleProvider>().locale.languageCode;
var path = LocalizedRoutes.getPath(routeKey, locale);
if (params != null) {
params!.forEach((key, value) {
path = path.replaceAll(':$key', value);
});
}
context.go(path);
}
}
// Usage:
LocalizedLink(
routeKey: 'products',
child: Text(AppLocalizations.of(context)!.viewAllProducts),
)
Localized Navigation UI
Localized Breadcrumbs
// lib/widgets/localized_breadcrumbs.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedBreadcrumbs extends StatelessWidget {
const LocalizedBreadcrumbs({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final location = GoRouterState.of(context).uri.path;
final crumbs = _buildCrumbs(location, l10n);
return Row(
children: [
for (int i = 0; i < crumbs.length; i++) ...[
if (i > 0)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(Icons.chevron_right, size: 16),
),
if (i < crumbs.length - 1)
TextButton(
onPressed: () => context.go(crumbs[i].path),
child: Text(crumbs[i].label),
)
else
Text(
crumbs[i].label,
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
],
);
}
List<_Crumb> _buildCrumbs(String location, AppLocalizations l10n) {
final crumbs = <_Crumb>[
_Crumb(path: '/', label: l10n.navHome),
];
final segments = location.split('/').where((s) => s.isNotEmpty).toList();
var currentPath = '';
for (final segment in segments) {
currentPath += '/$segment';
final label = _getLocalizedLabel(segment, l10n);
if (label != null) {
crumbs.add(_Crumb(path: currentPath, label: label));
}
}
return crumbs;
}
String? _getLocalizedLabel(String segment, AppLocalizations l10n) {
// Map URL segments to localized labels
final labels = {
'products': l10n.navProducts,
'productos': l10n.navProducts,
'produkte': l10n.navProducts,
'settings': l10n.navSettings,
'configuracion': l10n.navSettings,
'einstellungen': l10n.navSettings,
'about': l10n.navAbout,
'acerca': l10n.navAbout,
'ueber-uns': l10n.navAbout,
};
return labels[segment];
}
}
class _Crumb {
final String path;
final String label;
_Crumb({required this.path, required this.label});
}
Localized Navigation Drawer
// lib/widgets/localized_drawer.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'localized_link.dart';
class LocalizedDrawer extends StatelessWidget {
const LocalizedDrawer({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
child: Text(
l10n.appName,
style: Theme.of(context).textTheme.headlineSmall,
),
),
_DrawerItem(
routeKey: 'home',
icon: Icons.home,
label: l10n.navHome,
),
_DrawerItem(
routeKey: 'products',
icon: Icons.shopping_bag,
label: l10n.navProducts,
),
_DrawerItem(
routeKey: 'cart',
icon: Icons.shopping_cart,
label: l10n.navCart,
),
const Divider(),
_DrawerItem(
routeKey: 'settings',
icon: Icons.settings,
label: l10n.navSettings,
),
_DrawerItem(
routeKey: 'about',
icon: Icons.info,
label: l10n.navAbout,
),
],
),
);
}
}
class _DrawerItem extends StatelessWidget {
final String routeKey;
final IconData icon;
final String label;
const _DrawerItem({
required this.routeKey,
required this.icon,
required this.label,
});
@override
Widget build(BuildContext context) {
return LocalizedLink(
routeKey: routeKey,
child: ListTile(
leading: Icon(icon),
title: Text(label),
onTap: () {
Navigator.pop(context); // Close drawer
// Navigation handled by LocalizedLink
},
),
);
}
}
Language Switcher with Route Preservation
When users change language, keep them on the same page:
// lib/widgets/language_switcher.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../providers/locale_provider.dart';
import '../router/localized_routes.dart';
class LanguageSwitcher extends StatelessWidget {
const LanguageSwitcher({super.key});
@override
Widget build(BuildContext context) {
final currentLocale = context.watch<LocaleProvider>().locale;
return PopupMenuButton<Locale>(
initialValue: currentLocale,
onSelected: (locale) => _changeLocale(context, locale),
itemBuilder: (context) => LocaleProvider.supportedLocales.map((locale) {
return PopupMenuItem(
value: locale,
child: Row(
children: [
Text(_getFlag(locale.languageCode)),
const SizedBox(width: 8),
Text(_getLanguageName(locale.languageCode)),
if (locale == currentLocale) ...[
const Spacer(),
const Icon(Icons.check, size: 16),
],
],
),
);
}).toList(),
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(_getFlag(currentLocale.languageCode)),
const Icon(Icons.arrow_drop_down),
],
),
),
);
}
void _changeLocale(BuildContext context, Locale newLocale) {
final provider = context.read<LocaleProvider>();
final currentPath = GoRouterState.of(context).uri.path;
final oldLocale = provider.locale.languageCode;
final newLocaleCode = newLocale.languageCode;
// Update provider
provider.setLocale(newLocale);
// Find corresponding path in new locale
final routeKey = LocalizedRoutes.getRouteKey(currentPath);
if (routeKey != null) {
final newPath = LocalizedRoutes.getPath(routeKey, newLocaleCode);
// Preserve any path parameters
final oldBase = LocalizedRoutes.getPath(routeKey, oldLocale);
if (currentPath.length > oldBase.length) {
final params = currentPath.substring(oldBase.length);
context.go('$newPath$params');
} else {
context.go(newPath);
}
}
}
String _getFlag(String languageCode) {
const flags = {
'en': '🇬🇧',
'es': '🇪🇸',
'de': '🇩🇪',
'fr': '🇫🇷',
'ja': '🇯🇵',
};
return flags[languageCode] ?? '🌐';
}
String _getLanguageName(String languageCode) {
const names = {
'en': 'English',
'es': 'Espanol',
'de': 'Deutsch',
'fr': 'Francais',
'ja': '日本語',
};
return names[languageCode] ?? languageCode;
}
}
Deep Linking with Localization
Handle localized deep links from external sources:
// lib/router/deep_link_handler.dart
import 'package:go_router/go_router.dart';
class DeepLinkHandler {
/// Parse incoming deep link and normalize to current locale
static String normalizeDeepLink(String deepLink, String currentLocale) {
final uri = Uri.parse(deepLink);
final path = uri.path;
// Extract locale from path if present (e.g., /es/productos/123)
final segments = path.split('/').where((s) => s.isNotEmpty).toList();
if (segments.isEmpty) return '/';
// Check if first segment is a locale
final possibleLocale = segments.first;
if (LocaleProvider.supportedLocales
.any((l) => l.languageCode == possibleLocale)) {
// Remove locale prefix
segments.removeAt(0);
}
// Find route key from remaining path
if (segments.isEmpty) return '/';
final routeKey = _findRouteKey(segments.first);
if (routeKey == null) return '/';
// Build path in current locale
var normalizedPath = LocalizedRoutes.getPath(routeKey, currentLocale);
// Add any remaining segments (IDs, etc.)
if (segments.length > 1) {
normalizedPath += '/${segments.skip(1).join('/')}';
}
// Preserve query parameters
if (uri.queryParameters.isNotEmpty) {
normalizedPath += '?${uri.query}';
}
return normalizedPath;
}
static String? _findRouteKey(String segment) {
// Check against all localized paths
const routeSegments = {
'products': ['products', 'productos', 'produkte', 'produits', 'shouhin'],
'settings': ['settings', 'configuracion', 'einstellungen', 'parametres', 'settei'],
'about': ['about', 'acerca', 'ueber-uns', 'a-propos', 'gaiyou'],
};
for (final entry in routeSegments.entries) {
if (entry.value.contains(segment)) {
return entry.key;
}
}
return null;
}
}
Testing Localized Routes
// test/router_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
group('LocalizedRoutes', () {
test('returns correct path for locale', () {
expect(LocalizedRoutes.getPath('products', 'en'), '/products');
expect(LocalizedRoutes.getPath('products', 'es'), '/productos');
expect(LocalizedRoutes.getPath('products', 'de'), '/produkte');
});
test('finds route key from any locale path', () {
expect(LocalizedRoutes.getRouteKey('/products'), 'products');
expect(LocalizedRoutes.getRouteKey('/productos'), 'products');
expect(LocalizedRoutes.getRouteKey('/produkte'), 'products');
});
test('handles paths with parameters', () {
expect(LocalizedRoutes.getRouteKey('/products/123'), 'products');
expect(LocalizedRoutes.getRouteKey('/productos/abc'), 'products');
});
});
group('DeepLinkHandler', () {
test('normalizes deep link to current locale', () {
expect(
DeepLinkHandler.normalizeDeepLink('/es/productos/123', 'en'),
'/products/123',
);
expect(
DeepLinkHandler.normalizeDeepLink('/products/123', 'de'),
'/produkte/123',
);
});
test('preserves query parameters', () {
expect(
DeepLinkHandler.normalizeDeepLink('/products?sort=price', 'es'),
'/productos?sort=price',
);
});
});
}
Widget Testing
// test/navigation_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
void main() {
testWidgets('navigates to localized product page', (tester) async {
final localeProvider = LocaleProvider();
await localeProvider.setLocale(const Locale('es'));
await tester.pumpWidget(
ChangeNotifierProvider.value(
value: localeProvider,
child: MaterialApp.router(
routerConfig: AppRouter.router(localeProvider),
locale: const Locale('es'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
),
),
);
// Navigate to products
final context = tester.element(find.byType(HomePage));
context.goLocalized('products');
await tester.pumpAndSettle();
// Verify we're on the Spanish products page
expect(find.byType(ProductsPage), findsOneWidget);
// URL should be /productos
});
}
Best Practices
1. Keep Route Keys Consistent
Use semantic, language-agnostic keys:
// Good
LocalizedRoutes.getPath('products', locale);
LocalizedRoutes.getPath('user_profile', locale);
// Bad - using translated paths as keys
LocalizedRoutes.getPath('productos', locale);
2. Handle Locale Changes Gracefully
When locale changes, redirect to equivalent localized route:
redirect: (context, state) {
final routeKey = LocalizedRoutes.getRouteKey(state.uri.path);
if (routeKey == null) return null;
final correctPath = LocalizedRoutes.getPath(routeKey, currentLocale);
if (state.uri.path != correctPath) {
return correctPath;
}
return null;
}
3. SEO for Flutter Web
Add hreflang tags for localized URLs:
// In your HTML or server configuration
<link rel="alternate" hreflang="en" href="https://example.com/en/products" />
<link rel="alternate" hreflang="es" href="https://example.com/es/productos" />
<link rel="alternate" hreflang="de" href="https://example.com/de/produkte" />
4. Fallback for Unknown Locales
Always have a fallback:
String getPath(String routeKey, String locale) {
return _routes[routeKey]?[locale] ??
_routes[routeKey]?['en'] ??
'/';
}
Summary
Effective go_router localization includes:
- Locale-aware routing that refreshes when locale changes
- Localized URL paths for better UX and SEO
- Language-prefixed URLs for Flutter Web
- Type-safe navigation helpers that auto-localize
- Route preservation when switching languages
- Deep link normalization for external links
With these patterns, your app's navigation will feel native to users in any language.