← Back to Blog

Flutter go_router Localization: Complete Guide to Multilingual Navigation

fluttergo_routernavigationlocalizationroutingdeep-linking

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/productos rank better in Spanish searches
  • User Experience: Bookmarks and shared links in users' language
  • Professional Appearance: /de/einstellungen looks more professional than /de/settings for 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:

  1. Locale-aware routing that refreshes when locale changes
  2. Localized URL paths for better UX and SEO
  3. Language-prefixed URLs for Flutter Web
  4. Type-safe navigation helpers that auto-localize
  5. Route preservation when switching languages
  6. Deep link normalization for external links

With these patterns, your app's navigation will feel native to users in any language.

Related Resources