52

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?

2

15 Answers 15

15

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,
        );
      }
    }
    
    
8
  • According from get_it pub.dev/packages/get_it#why-getit , is Extremely fast (O(1)) when testing
    – zap
    Commented Jul 15, 2021 at 10:06
  • 1
    Do you have any example project using this method?
    – Harish
    Commented Feb 28, 2022 at 6:05
  • 1
    If you are already using bloc, it is not recommended to use get_it with it.
    – Ataberk
    Commented Apr 6, 2022 at 14:20
  • 3
    @FadyFouad, what Ataberk is saying does not make any sense. Your state management choice doesn't have anything to do with your DI choice. You can perfectly use GetIt with bloc. Commented Jan 19, 2023 at 10:51
  • 1
    @genericUser component_gallery is the name of the project of the example Commented Feb 19, 2023 at 17:35
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.

5
  • 9
    how is that supposed to work, when the app_localizations.dart is autogenerated and overwritten in its entirety, any time arb files are changed? Commented Sep 16, 2021 at 15:20
  • 2
    Really wish you'd have put a full example up. This is impossible for a new dart developer to follow.
    – Chris
    Commented Sep 26, 2021 at 22:14
  • cool approach, thanks!
    – Olga
    Commented Sep 29, 2021 at 8:50
  • In this way instance will be null when called from background service (when app closed)
    – AnasSafi
    Commented Apr 18, 2022 at 1:51
  • As @CeeMcSharpface says, I think it is not a solution to modify generated files
    – Laurent
    Commented Mar 28 at 16:34
14

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.

5
  • 3
    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
    Commented May 10, 2022 at 21:01
  • 2
    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
    Commented May 10, 2022 at 21:11
  • @Gábor What's the use-case for changing language in background? Commented May 18, 2022 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
    Commented May 18, 2022 at 21:57
  • Thank you, all. I had no idea the reason my flutter app's background thread was getting "und" out of Platform.localeName because it wasn't running in the UI thread, until seeing this discussion. Starting an Isolate early enough in main() seems to be the reason, for me at least, that localeName == "und" Commented Jul 5 at 1:12
10

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();
6
  • 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. Commented May 11, 2022 at 6: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
    Commented May 11, 2022 at 6: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. Commented May 11, 2022 at 7: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
    Commented May 11, 2022 at 7:06
  • Flutter Intl doesn't have support for localizing multiple submodules. So the previous solution with the Localization() class still is the best for this usecase (by far, not obsolete). I adapt it to make sure it loads just one time, and that way it doesn't to always load async. Thanks for the solution.
    – luizv
    Commented Apr 21 at 23:41
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'),),
7
  • It seems there is no documentation. I am unsure how to use this exactly.
    – Salatgurke
    Commented May 2, 2020 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
    Commented May 2, 2020 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
    Commented May 2, 2020 at 19:40
  • 6
    It is still tied to the context.
    – SalahAdDin
    Commented Sep 7, 2021 at 14:06
  • 4
    It needs context: github.com/aissat/easy_localization/issues/210
    – Jani
    Commented Jan 11, 2022 at 22:19
7

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);
...
3
  • 4
    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
    Commented Feb 14, 2023 at 21:45
  • @JackSiro you need to call it in the builder method of the MaterialApp. Otherwise it wont work Commented May 17 at 19:16
  • this is a genius approach man, whatever with some good state management you can apply the runtime switch as well. Commented Jun 10 at 11:46
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
0
3

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

2

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.

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;
  }
1

I use the following code:

final appLocalizations = await AppLocalizations.delegate
      .load(Locale(Platform.localeName.substring(0, 2)));
print(appLocalizations.yourCode)
0

You can make a singleton class like this:

class LocalizationManager {
  static final LocalizationManager instance = LocalizationManager._();
  LocalizationManager._();

  late AppLocalizations _localization;

  AppLocalizations get appLocalization => _localization;

  void setLocalization(BuildContext context) {
    _localization = AppLocalizations.of(context)!;
  }
}

then in the material app builder, set the value of localization:

return MaterialApp(
  builder: (context, child) {
    LocalizationManager.instance.setLocalization(context);
    return child!;
  },
)
0

Hey guys in my case I needed this feature (Contextless translation) for my background service and I have done this way:

Future<void> loadTranslations(Locale locale) async {
  final languageCode = locale.languageCode;
  final countyCode = locale.countryCode;
  // Read the JSON file based on the locale
  String filePath = countyCode != null
      ? 'assets/flutter_i18n/${languageCode}_$countyCode.json'
      : 'assets/flutter_i18n/$languageCode.json';
  late String jsonString;

  try {
    jsonString = await rootBundle.loadString(filePath);
  } catch (e) {
    filePath = 'assets/flutter_i18n/$languageCode.json';
    jsonString = await rootBundle.loadString(filePath);
  }

  _currentTranslations = jsonDecode(jsonString);
}

String translate(String key) {
  if (_currentTranslations == null) {
    throw "Translations not loaded";
  }

  // Access translation for the given key
  String? translation = _currentTranslations![key];
  if (translation != null) {
    return translation;
  } else {
    // If translation not found, return the key itself
    return key;
  }
}

Note that I had translations as json files.

0

I did not test this solution, but in theory you can wrap your code in ValueListenableBuilder, it has context in "builder" section

0

The slang package supports context-free translations out of the box:

Text(t.loginPage.title)

Not the answer you're looking for? Browse other questions tagged or ask your own question.