Search code examples
flutterinternationalizationlocaleflutter-web

How to force flutter web preserve current language when enter the URL directly into the browser


I have an application that supports two languages; English and Arabic. The URL path starts with /en/home for English and /ar/home for Arabic. Language switching works fine. The issues that I am facing are:

  1. When user switches language (clicking the button), the browser URL path does not get updated to reflect the selected language.

  2. If the user enters the URL manually in the browser to access the Arabic version /ar/home, the page language keeps showing in English.

Below is a test code that can simulate the problem. Translation files are added as comments at the end of the code.

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:url_strategy/url_strategy.dart';
import 'package:provider/provider.dart';

var urlLang = '';

class L10n {
  static final all = [
    const Locale('en', ''),
    const Locale('ar', ''),
  ];
}

Locale getSwitchToLanguage(Locale currentLocale) {
  if (currentLocale == const Locale('ar', '')) {
    return const Locale('en', '');
  }
  return const Locale('ar', '');
}

class LocaleProvider extends ChangeNotifier {
  Locale _locale = const Locale('en', '');

  Locale get locale => _locale;

  void setLocale(Locale locale) {
    if (!L10n.all.contains(locale)) return;
    _locale = locale;
    notifyListeners();
  }

  void clearLocale() {
    _locale = const Locale('en', '');
    notifyListeners();
  }
}

void switchLanguage(BuildContext context) {
  final provider = Provider.of<LocaleProvider>(context, listen: false);
  final siwtchToLocale = getSwitchToLanguage(provider.locale);
  provider.setLocale(siwtchToLocale);
}

void main() {
  setPathUrlStrategy();
  runApp(
    const MyApp(),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) => ChangeNotifierProvider(
        create: (context) => LocaleProvider(),
        builder: (context, child) {
          final provider = Provider.of<LocaleProvider>(context);
          urlLang = provider.locale.languageCode;
          return MaterialApp(
            localizationsDelegates: const [
              AppLocalizations.delegate,
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
              GlobalCupertinoLocalizations.delegate,
            ],
            supportedLocales: L10n.all,
            locale: provider.locale,
            debugShowCheckedModeBanner: false,
            initialRoute: '/$urlLang/home',
            // routing
            onGenerateRoute: (settings) {
              if (settings.name == '/ar/home' || settings.name == '/en/home') {
                return MaterialPageRoute(
                    settings: settings, builder: (context) => const HomePage());
              }
              return MaterialPageRoute<void>(
                settings: settings,
                builder: (BuildContext context) => const UnknownScreen(),
              );
            },
            onUnknownRoute: (settings) {
              return MaterialPageRoute<void>(
                settings: settings,
                builder: (BuildContext context) => const UnknownScreen(),
              );
            },
          );
        },
      );
}

class UnknownScreen extends StatelessWidget {
  const UnknownScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("404 page"),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(AppLocalizations.of(context)!.appTitle),
      ),
      body: Center(
        child: Column(children: [
          const SizedBox(height: 50),
          urlLang == 'ar'
              ? Text("This is the Arabic version : $urlLang")
              : Text("This is the English version : $urlLang"),
          const SizedBox(height: 100),
          ElevatedButton(
            child: Text(
              urlLang == 'en' ? "Switch to Arabic" : "Switch to English",
            ),
            onPressed: () {
              switchLanguage(context);
            },
          )
        ]),
      ),
    );
  }
}

/*
app_en.arb file content

{
    "appTitle": "Home Page",
    "not_used" : "not_used"

}

app_ar.arb file content:

{
    "appTitle": "الصفحة الرئيسية - Home Page",
  "not_used" : "not_used"
   
}
*/

/*
pubspec.yaml

name: langissue
description: A new Flutter project.
publish_to: 'none' 
version: 1.0.0+1

environment:
  sdk: ">=2.17.3 <3.0.0"
dependencies:
  flutter:
    sdk: flutter
  url_strategy: ^0.2.0
  intl: ^0.17.0 
  flutter_web_plugins:
    sdk: flutter
  provider: ^6.0.3
  async: ^2.8.2   
  flutter_localizations:
    sdk: flutter

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
flutter:
  uses-material-design: true
  generate: true 

*/

Solution

  • To get changes from the URL by the user Navigator 2.0 should be used. More specifically the method RouterDelegate.setNewRoutePath should be implemented. So, instead of MaterialApp.onGenerateRoute and MaterialApp.initialRoute properties, the MaterialApp.routeInformationParser and MaterialApp.routerDelegate properties should be used.

    Consequently, there is a need to implement 2 classes:

    1. The RouteInformationParser is responsible to parse the URL to a custom configuration and the other way around;
    2. The RouterDelegate is responsible to receive the new URL input by the user and notify the URL changes on page changes;

    Here's the result for Flutter Web:

    enter image description here

    From the provided code, the changes are the following:

    import 'package:path/path.dart' as path;
    import 'package:flutter/material.dart';
    import 'package:flutter_localizations/flutter_localizations.dart';
    import 'package:flutter_gen/gen_l10n/app_localizations.dart';
    import 'package:url_strategy/url_strategy.dart';
    import 'package:provider/provider.dart';
    
    const englishLocale = Locale('en', '');
    const arabicLocale = Locale('ar', '');
    
    class L10n {
      static final all = [
        englishLocale,
        arabicLocale,
      ];
    }
    
    enum Language {
      english,
      arabic,
    }
    
    const languageByLanguageCode = {
      'en': Language.english,
      'ar': Language.arabic,
    };
    
    final languageCodeByLanguage = {
      for (final entry in languageByLanguageCode.entries) entry.value: entry.key
    };
    
    Locale getSwitchToLanguage(Locale currentLocale) {
      if (currentLocale == arabicLocale) {
        return englishLocale;
      }
      return arabicLocale;
    }
    
    class LocaleProvider extends ChangeNotifier {
      Locale _locale = englishLocale;
      Locale get locale => _locale;
      Language get language => languageByLanguageCode[_locale.languageCode]!;
      String get languageCode => _locale.languageCode;
    
      void setLocale(Locale locale) {
        if (!L10n.all.contains(locale) || _locale == locale) return;
        _locale = locale;
        notifyListeners();
      }
    
      void clearLocale() {
        _locale = englishLocale;
        notifyListeners();
      }
    }
    
    void switchLanguage(BuildContext context) {
      final provider = Provider.of<LocaleProvider>(context, listen: false);
      final siwtchToLocale = getSwitchToLanguage(provider.locale);
      provider.setLocale(siwtchToLocale);
    }
    
    void main() {
      setPathUrlStrategy();
      final appRouterDelegate = AppRouterDelegate();
      runApp(
        MultiProvider(
          providers: [
            ChangeNotifierProvider(create: (_) => LocaleProvider()),
            ChangeNotifierProxyProvider<LocaleProvider, AppRouterDelegate>(
              create: (_) => appRouterDelegate,
              update: (_, localeProvider, appRouterDelegate) =>
                  appRouterDelegate!..updateLocaleProvider(localeProvider),
            ),
          ],
          child: const MyApp(),
        ),
      );
    }
    
    class MyApp extends StatefulWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      State<MyApp> createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      final _routeLanguageParser = AppRouteInformationParser();
    
      @override
      Widget build(BuildContext context) {
        final localeProvider = context.watch<LocaleProvider>();
        final appRouterDelegate = context.read<AppRouterDelegate>();
        return MaterialApp.router(
          routeInformationParser: _routeLanguageParser,
          routerDelegate: appRouterDelegate,
          localizationsDelegates: const [
            AppLocalizations.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          supportedLocales: L10n.all,
          locale: localeProvider.locale,
          debugShowCheckedModeBanner: false,
        );
      }
    }
    
    class UnknownScreen extends StatelessWidget {
      const UnknownScreen({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("404 page"),
          ),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      const HomePage({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(AppLocalizations.of(context)!.appTitle),
          ),
          body: Center(
            child: Consumer2<LocaleProvider, AppRouterDelegate>(
              builder: (_, localeProvider, appRouterDelegate, __) {
                return Column(children: [
                  const Spacer(),
                  localeProvider.languageCode == 'ar'
                      ? Text(
                          "This is the Arabic version : ${localeProvider.languageCode}")
                      : Text(
                          "This is the English version : ${localeProvider.languageCode}"),
                  const Spacer(),
                  ElevatedButton(
                    child: Text(
                      localeProvider.languageCode == 'en'
                          ? "Switch to Arabic"
                          : "Switch to English",
                    ),
                    onPressed: () {
                      switchLanguage(context);
                    },
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton(
                    child: const Text('Go to services'),
                    onPressed: () => appRouterDelegate.setPage(Page.services),
                  ),
                  const Spacer()
                ]);
              },
            ),
          ),
        );
      }
    }
    
    enum Page {
      home,
      services,
      serviceA,
      serviceB,
      serviceC,
      unknown,
    }
    
    const pathByPage = {
      Page.home: '/',
      Page.services: '/services',
      Page.serviceA: '/services/a',
      Page.serviceB: '/services/b',
      Page.serviceC: '/services/c',
      Page.unknown: '/404',
    };
    
    final pageByPath = {
      for (final entry in pathByPage.entries) entry.value: entry.key
    };
    
    final pagesByPage = {
      for (final page in Page.values)
        page: (pathByPage[page]!.endsWith('/')
                ? pathByPage[page]!.substring(0, pathByPage[page]!.length - 1)
                : pathByPage[page]!)
            .split('/')
            .fold<List<Page>>(
          [],
          (pages, pathSegment) {
            final pagePath = path.join('/',
                path.joinAll(pages.map((page) => pathByPage[page]!)), pathSegment);
            final subpage = pageByPath[pagePath];
            assert(subpage != null,
                'path segment "$pathSegment" of subpage "$page" not defined');
            pages.add(subpage!);
            return pages;
          },
        ),
    };
    
    class AppRouteConfiguration {
      final Language language;
      final List<Page> pages;
      Page get page => pages.last;
      AppRouteConfiguration(this.language, this.pages);
    }
    
    class AppRouteInformationParser
        extends RouteInformationParser<AppRouteConfiguration> {
      @override
      Future<AppRouteConfiguration> parseRouteInformation(
          RouteInformation routeInformation) {
        final uri = Uri.parse(routeInformation.location ?? '/');
        if (uri.pathSegments.isEmpty) {
          return Future.value(AppRouteConfiguration(Language.english, [Page.home]));
        }
    
        final language = languageByLanguageCode[uri.pathSegments[0]];
    
        final path = '/${uri.pathSegments.skip(1).join('/')}';
        final pages = pagesByPage[pageByPath[path]] ?? [Page.home, Page.unknown];
        return Future.value(
            AppRouteConfiguration(language ?? Language.english, pages));
      }
    
      @override
      RouteInformation? restoreRouteInformation(
          AppRouteConfiguration configuration) {
        return RouteInformation(location: getPath(configuration));
      }
    }
    
    String getPath(AppRouteConfiguration configuration) =>
        '/${languageCodeByLanguage[configuration.language]}${pathByPage[configuration.page]}';
    
    class AppRouterDelegate extends RouterDelegate<AppRouteConfiguration>
        with ChangeNotifier, PopNavigatorRouterDelegateMixin {
      LocaleProvider? _localeProvider;
      var _configuration = AppRouteConfiguration(Language.english, [Page.home]);
      Page get page => _configuration.page;
      bool get canPop => _configuration.pages.length > 1;
    
      void setPage(Page page) {
        _configuration =
            AppRouteConfiguration(_configuration.language, pagesByPage[page]!);
        notifyListeners();
      }
    
      void popPage() {
        if (canPop) {
          final pages =
              _configuration.pages.sublist(0, _configuration.pages.length - 1);
          _configuration = AppRouteConfiguration(_configuration.language, pages);
          notifyListeners();
        }
      }
    
      void updateLocaleProvider(LocaleProvider localeProvider) {
        _localeProvider = localeProvider;
        _configuration =
            AppRouteConfiguration(localeProvider.language, _configuration.pages);
        notifyListeners();
      }
    
      @override
      final navigatorKey = GlobalKey<NavigatorState>();
    
      @override
      AppRouteConfiguration? get currentConfiguration => _configuration;
    
      @override
      Widget build(BuildContext context) {
        return Navigator(
          key: navigatorKey,
          pages: [
            for (final page in _configuration.pages) ...[
              if (_configuration.page == Page.home)
                MaterialPage(
                  name: getPath(_configuration),
                  child: const HomePage(),
                ),
              if (_configuration.page == Page.services)
                MaterialPage(
                  name: getPath(_configuration),
                  child: ServicesScreen(),
                ),
              if (_configuration.page == Page.serviceA)
                MaterialPage(
                  name: getPath(_configuration),
                  child: ServiceScreen(page: _configuration.page),
                ),
              if (_configuration.page == Page.serviceB)
                MaterialPage(
                  name: getPath(_configuration),
                  child: ServiceScreen(page: _configuration.page),
                ),
              if (_configuration.page == Page.serviceC)
                MaterialPage(
                  name: getPath(_configuration),
                  child: ServiceScreen(page: _configuration.page),
                ),
              if (_configuration.page == Page.unknown)
                MaterialPage(
                  name: getPath(_configuration),
                  child: const UnknownScreen(),
                ),
            ]
          ],
          onPopPage: (route, result) {
            var didPop = route.didPop(result);
            if (!didPop) {
              return false;
            }
            didPop = canPop;
            if (canPop) popPage();
            return didPop;
          },
        );
      }
    
      @override
      Future<void> setNewRoutePath(AppRouteConfiguration configuration) async {
        _configuration = configuration;
        final String language = languageCodeByLanguage[configuration.language]!;
        _localeProvider?.setLocale(Locale(language, ''));
        notifyListeners();
      }
    }
    
    class ServiceInfo {
      final Page page;
      final String name;
      const ServiceInfo(this.page, this.name);
    }
    
    final serviceNameByPage = {
      Page.serviceA: 'Service A',
      Page.serviceB: 'Service B',
      Page.serviceC: 'Service C',
    };
    
    class ServicesScreen extends StatelessWidget {
      ServicesScreen({Key? key}) : super(key: key);
    
      final _services = [
        for (final entry in serviceNameByPage.entries)
          ServiceInfo(entry.key, entry.value)
      ];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Services'),
          ),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: [
                Expanded(
                  child: ListView.separated(
                    shrinkWrap: true,
                    separatorBuilder: (_, __) => const Divider(),
                    itemBuilder: (_, index) => ElevatedButton(
                        onPressed: () {
                          final appRouterDelegate =
                              context.read<AppRouterDelegate>();
                          appRouterDelegate.setPage(_services[index].page);
                        },
                        child: Text(_services[index].name)),
                    itemCount: _services.length,
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class ServiceScreen extends StatelessWidget {
      final Page page;
      const ServiceScreen({Key? key, required this.page}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(serviceNameByPage[page]!),
          ),
        );
      }
    }