42

I am trying to localize my app in flutter. I created the needed string.arb files for the supported languages.

Why does AppLocalizations.of(context) need a context?

I simply want to access the named strings in the files/locales files/classes. At some point in the app I build a List and fill it later via overriding some fields with a separate class.

However, this class has no context but I want to use localized strings in it. Can I write a method which gets me the Localization of whatever String I put in?

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Salatgurke
  • 1,554
  • 1
  • 13
  • 35
  • In another StackOverflow thread I'm proposing a solution using Flutter Extensions: https://stackoverflow.com/a/68754936/7052599 – Fleximex Aug 24 '21 at 07:31
  • This is my solution: https://stackoverflow.com/a/69037938/1034225 – mabg Sep 02 '21 at 23:20

10 Answers10

14

If you would prefer to not use a package then here is a solution that worked for me. Now the most common implementation of AppLocalizations I've seen usually has these two lines:

//.........
static const LocalizationsDelegate<AppLocalizations> delegate =
      _AppLocalizationsDelegate();

static AppLocalizations of(BuildContext context) {
  return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
//.........

The implementation of the delegate would look something like this:

class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
  const _AppLocalizationsDelegate();

  @override
  Future<AppLocalizations> load(Locale locale) async {
    AppLocalizations localizations = new AppLocalizations(locale);
    await localizations.load();

    return localizations;
  }

  //... the rest omitted for brevity
}

Notice the load method on the delegate returns a Future<AppLocalizations>. The load method is usually called once from main and never again so you can take advantage of that by adding a static instance of AppLocalizations to the delegate. So now your delegate would look like this:

class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
  const _AppLocalizationsDelegate();

  static AppLocalizations instance;

  @override
  Future<AppLocalizations> load(Locale locale) async {
    AppLocalizations localizations = new AppLocalizations(locale);
    await localizations.load();

    instance = localizations; // set the static instance here

    return localizations;
  }

  //... the rest omitted for brevity
}

Then on your AppLocalizations class you would now have:

//.........
static const LocalizationsDelegate<AppLocalizations> delegate =
      _AppLocalizationsDelegate();

static AppLocalizations of(BuildContext context) {
  return Localizations.of<AppLocalizations>(context, AppLocalizations);
}

static AppLocalizations get instance => _AppLocalizationsDelegate.instance; // add this
//.........

Now in your translate helper method you could have:

String tr(String key) {
    return AppLocalizations.instance.translate(key);
}

No context needed.

chinloyal
  • 1,023
  • 14
  • 42
12

We can resolve this by using get_it easily, we can use the string anywhere after this setup.

  1. Install this to your vscode Flutter Intl VSCode Extension

  2. setup pubspec.yaml

    dependencies:
    flutter:
      sdk: flutter
    flutter_localizations:                          # Add this line
      sdk: flutter                                  # Add this line
    intl: ^0.17.0                                   # Add this line
    get_it: ^7.2.0                                  # Add this line
    
    
    flutter:
      uses-material-design: true
      generate: true                                # Add this line
    
    
    flutter_intl:                                   # Add this line
      enabled: true                                 # Add this line
      class_name: I10n                              # Add this line
      main_locale: en                               # Add this line
      arb_dir: lib/core/localization/l10n           # Add this line
      output_dir: lib/core/localization/generated   # Add this line
    
  3. Setup main.dart

    import 'package:component_gallery/core/localization/generated/l10n.dart';
    import 'package:component_gallery/locator.dart';
    import 'package:component_gallery/ui/pages/home.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_localizations/flutter_localizations.dart';
    
    void main() {
      setupLocator();
      runApp(App());
    }
    
    class App extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          localizationsDelegates: [
            I10n.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          supportedLocales: I10n.delegate.supportedLocales,
          localeResolutionCallback: (deviceLocale, supportedLocales) {
            if (supportedLocales
                .map((e) => e.languageCode)
                .contains(deviceLocale?.languageCode)) {
              return deviceLocale;
            } else {
              return const Locale('en', '');
            }
          },
          home: HomePage(),
        );
      }
    }
    
  4. setup locator.dart

    import 'package:component_gallery/core/services/navigation_service.dart';
    import 'package:get_it/get_it.dart';
    
    GetIt locator = GetIt.instance;
    
    void setupLocator() {
      locator.registerLazySingleton(() => I10n());
    }
    
    
  5. Use it with Get_it without context as

    final I10n _i10n = locator<I10n>();
    class MessageComponent extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Text(
          _i10n.sample,
          textAlign: TextAlign.center,
        );
      }
    }
    
    
zap
  • 191
  • 3
  • 8
8

There is a library called easy_localization that does localization without context, you can simply use that one. Library also provides more convenient approach of writing less code and still localizing all the segments of the app. An example main class:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
  ]).then((_) {
    runApp(EasyLocalization(
      child: MyApp(),
      useOnlyLangCode: true,
      startLocale: Locale('nl'),
      fallbackLocale: Locale('nl'),
      supportedLocales: [
        Locale('nl'),
        Locale('en'),
      ],
      path: 'lang',
    ));
  });
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SplashScreen(),
      supportedLocales: EasyLocalization.of(context).supportedLocales,
      locale: EasyLocalization.of(context).locale,
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        DefaultCupertinoLocalizations.delegate,
        EasyLocalization.of(context).delegate,
      ],
      localeResolutionCallback: (locale, supportedLocales) {
        if (locale == null) {
          EasyLocalization.of(context).locale = supportedLocales.first;
          Intl.defaultLocale = '${supportedLocales.first}';
          return supportedLocales.first;
        }

        for (Locale supportedLocale in supportedLocales) {
          if (supportedLocale.languageCode == locale.languageCode) {
            EasyLocalization.of(context).locale = supportedLocale;
            Intl.defaultLocale = '$supportedLocale';
            return supportedLocale;
          }
        }

        EasyLocalization.of(context).locale = supportedLocales.first;
        Intl.defaultLocale = '${supportedLocales.first}';
        return supportedLocales.first;
      },
    );
  }
}

Also don't forget to put localization path to your pubspec.yamal file!

After all of this is done, you can simply just use it in a Text widget like this:

Text(tr('someJsonKey'),),
Aleksandar
  • 1,457
  • 13
  • 30
  • It seems there is no documentation. I am unsure how to use this exactly. – Salatgurke May 02 '20 at 18:09
  • What do you mean there is no documentation? You need to open the link (open readme page of the plugin), there is a tutorial on how to integrate it and use it throughout the app. Main part is integrating it in main function, and than using it just by calling tr("keyFromJSONFile") – Aleksandar May 02 '20 at 18:49
  • When I integrate it into the App, I get an error message on launch saying that the localization file could not be found. Changing it does not help at all – Salatgurke May 02 '20 at 19:40
  • That is due to some error in main.dart file, and some piece of the puzzle not integrated by the docs of the plugin. I will update my answer with main file example so please check what you are doing wrong. – Aleksandar May 02 '20 at 19:56
  • The package you suggested is awesome! – Kreedz Zhen May 16 '21 at 07:30
  • 5
    It is still tied to the context. – SalahAdDin Sep 07 '21 at 14:06
  • 3
    It needs context: https://github.com/aissat/easy_localization/issues/210 – Jani Jan 11 '22 at 22:19
8

If you know the desired Locale then you could use:

final locale = Locale('en');
AppLocalizations t = await AppLocalizations.delegate.load(locale);
println(t.someTranslationKey);

Instead of hardcoding Locale('en') you could implement some kind of resolver to find out what the desired locale is. The supported languages are AppLocalizations.supportedLocales.

Stuck
  • 11,225
  • 11
  • 59
  • 104
  • 1
    If you're somewhere with an UI displayed (as opposed to eg. a background service) then you can get the current locale from the system, making it this one-liner: `final t = await AppLocalizations.delegate.load(WidgetsBinding.instance!.window.locale)`. But this will return the `und` undefined locale if there's no UI present. – Gábor May 10 '22 at 21:01
  • 1
    Considering that we use the `intl` package, anyway, the following works in some cases even without an UI: `final t = await AppLocalizations.delegate.load(Locale(Intl.getCurrentLocale()));` but without fallback. So, for instance, on an `en_US` device it won't automatically find the `en` localization. – Gábor May 10 '22 at 21:11
  • @Gábor What's the use-case for changing language in background? – Usama Karim May 18 '22 at 15:30
  • I didn't intend to _change_ it, just to _use_ it. But as I wrote in an answer below, I now use Flutter Intl which I found to be superior to the "standard" gen_l10n solution, in three important areas. This was one of them: Flutter Intl provides non-context localization out-of-the-box. – Gábor May 18 '22 at 21:57
6

Latest: the current Flutter Intl plugin makes this approach obsolete, at least if you use a supported IDE. You have a context-free alternative there:

S.current.translationKey

Previous: Starting from the suggestions of Stuck, this is the final solution I found. It isn't as cheap as the simple lookup with the context, so only use it if really necessary and make sure you call it once and use as many times as possible. But this approach works even if you have no context at all, for instance, you are in a background service or any other program part without UI.

Future<AppLocalizations> loadLocalization() async {
  final parts = Intl.getCurrentLocale().split('_');
  final locale = Locale(parts.first, parts.last);
  return await AppLocalizations.delegate.load(locale);
}

Using it is just the same as usual:

final t = await loadLocalization();
print(t.translationKey);

Update: the singleton I suggested in the comments could look like:

class Localization {
  static final Localization _instance = Localization._internal();
  AppLocalizations? _current;

  Localization._internal();

  factory Localization() => _instance;

  Future<AppLocalizations> loadCurrent() async {
    if (_current == null) {
      final parts = Intl.getCurrentLocale().split('_');
      final locale = Locale(parts.first, parts.last);
      _current = await AppLocalizations.delegate.load(locale);
    }
    return Future.value(_current);
  }

  void invalidate() {
    _current = null;
  }
}

and used like:

final t = await Localization().loadCurrent();

To keep track of language changes, call this from your main build():

PlatformDispatcher.instance.onLocaleChanged = () => Localization().invalidate();
Gábor
  • 9,466
  • 3
  • 65
  • 79
  • This approach will not work if we have a class containing translated strings. If the user changes the app language on the fly, the translated strings from the class will not update to the new language. – mister_cool_beans May 11 '22 at 06:03
  • In my own experience, this is the _only_ solution that works. Yes, it's your responsibility to invalidate your cached version when needed (you may want to rely on `didChangeLocales()` or `PlatformDispatcher.onLocaleChanged` or a `Provider` of your own. But at least you can get localization. All the other alternatives I tried (eg. building `MaterialApp` with a `navigatorKey` and using that to obtain a context) failed miserably. – Gábor May 11 '22 at 06:57
  • Provider is not an option as it requires context, if we had context then we would call AppLocalizations in the standard manor. I am going to check didChangeLocales(), to see if that solves the issue. – mister_cool_beans May 11 '22 at 07:05
  • 1
    Not directly, no. But you can keep this whole lot in a singleton and use the `Provider` or any other means to invalidate from the UI-based code when necessary. The non-UI parts of your app can use that singleton to do the string lookup when needed. – Gábor May 11 '22 at 07:06
5

My 2 cents into it, just not to loose the solution :)

I totally get why Flutter localization solutions needs BuildContext - makes total sense. But, if I explicitly don't want runtime language switch, and happy with the app restart?

That's the solution I came up with that seems to work pretty well.

Assuming you've followed Flutter's official localization steps, create a global variable that will be used to accessing the AppLocalizations class.

i18n.dart:

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

AppLocalizations get tr => _tr!; // helper function to avoid typing '!' all the time
AppLocalizations? _tr; // global variable 

class AppTranslations {
  static init(BuildContext context) {
    _tr = AppLocalizations.of(context);
  }
}

Now, somewhere in your main wrapper (the one below MaterialApp) call to set the localizations for the currently selected locale:

    AppTranslations.init(context);

It could be initState() or even build() of the main widget (it's safe to call this multiple times, obviously).

Now you can simply call:

import 'package:my_app/i18n.dart'

...
  Text(tr.welcome_text)

  // or

  print(tr.welcome_text);
...
divan
  • 2,591
  • 1
  • 17
  • 19
  • 2
    I can't seem to get this working as expected, am getting the error: _CastError (Null check operator used on a null value) on the line AppLocalizations get tr => _tr!; – Jack Siro Feb 14 '23 at 21:45
5

I was already using easy_localization package, so this found me very easy.

Trick I used to get app language without context as below

en-US.json

{
   "app_locale":"en"
}

ar-SA.json

{
   "app_locale":"ar"
}

Used it like a way in utility/extension function

LocaleKeys.app_locale.tr() //will return 'en' for English, 'ar' for Arabic
MohammedYakub M.
  • 2,893
  • 5
  • 31
  • 42
2

The best approach is using Flutter Intl (Flutter i18n plugins) as it is built by Flutter developers. It has a method to use without context like the following (Code example from the Visual Studio Marketplace details page):

Widget build(BuildContext context) {
    return Column(children: [
        Text(
            S.of(context).pageHomeConfirm,
        ),
        Text(
            S.current.pageHomeConfirm,// If you don't have `context` to pass
        ),
    ]);
}

More details on official plugin page and Visual Studio Marketplace details page

Elmar
  • 2,235
  • 24
  • 22
1

In my case, I am using the Minimal internationalization version.

Adapting chinloyal's answer to the Minimal internationalization version.

Apply chinloyal's solution but with this difference:

From this:

  @override
  Future<AppLocalizations> load(Locale locale) {
    return SynchronousFuture<AppLocalizations>(AppLocalizations(locale));
  }

To this:

  @override
  Future<AppLocalizations> load(Locale locale) async {
    var localizations =
        await SynchronousFuture<AppLocalizations>(AppLocalizations(locale));

    instance = localizations; // set the static instance here

    return localizations;
  }
Dui64
  • 11
  • 2
0

If you're building a mobile app, this one liner did it for me:

import 'dart:ui' as ui;
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

AppLocalizations get l10n {
  return lookupAppLocalizations(ui.PlatformDispatcher.instance.locale);
}

If you're building an app with multiple windows, it looks like the easiest way to do this synchronously is to set the global translation when the app starts and update the language code manually using docs described on the intl package.