Flutter localization without context
Asked Answered
C

15

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?

Chthonian answered 2/5, 2020 at 16:52 Comment(2)
In another StackOverflow thread I'm proposing a solution using Flutter Extensions: https://mcmap.net/q/354171/-applocalizations-without-calling-of-context-every-timeCyclops
This is my solution: https://mcmap.net/q/354172/-getting-buildcontext-in-flutter-for-localizationShiite
B
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,
        );
      }
    }
    
    
Beeler answered 15/7, 2021 at 8:18 Comment(8)
According from get_it pub.dev/packages/get_it#why-getit , is Extremely fast (O(1)) when testingBeeler
Do you have any example project using this method?Foreknow
If you are already using bloc, it is not recommended to use get_it with it.Malvasia
@Malvasia Why is it not recommended? Do you know where I can find info about why?Oneil
What is the component_gallery package? From which package you get the I10n object and what it's purpose?Youngstown
@Malvasia Please recommend another package or another way to implement dependency injection? or should I Use another state management Like riverpod?Ahab
@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.Guillory
@Youngstown component_gallery is the name of the project of the exampleGaribull
C
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.

Counterrevolution answered 28/9, 2020 at 0:39 Comment(5)
how is that supposed to work, when the app_localizations.dart is autogenerated and overwritten in its entirety, any time arb files are changed?Back
Really wish you'd have put a full example up. This is impossible for a new dart developer to follow.Doctor
cool approach, thanks!Exhilaration
In this way instance will be null when called from background service (when app closed)Caddish
As @CeeMcSharpface says, I think it is not a solution to modify generated filesBess
H
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.

Honky answered 20/3, 2022 at 8:33 Comment(5)
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.Brianna
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.Brianna
@Gábor What's the use-case for changing language in background?Dental
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.Brianna
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"Dourine
T
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();
Tacita answered 10/5, 2022 at 21:48 Comment(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.Rehearing
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.Brianna
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.Rehearing
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.Brianna
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.Vachil
If you mean packages inside your main app, with their own pubspec.yaml and stuff, that's an interesting mix. But did you modify it to call the Flutter Intl localization or do you use the old one?Brianna
K
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'),),
Konikow answered 2/5, 2020 at 17:3 Comment(7)
It seems there is no documentation. I am unsure how to use this exactly.Chthonian
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")Konikow
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 allChthonian
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.Konikow
The package you suggested is awesome!Chifley
It is still tied to the context.Sylvester
It needs context: github.com/aissat/easy_localization/issues/210Andrade
T
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);
...
Townie answered 7/7, 2022 at 10:17 Comment(3)
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!;Phenylalanine
@JackSiro you need to call it in the builder method of the MaterialApp. Otherwise it wont workPerrine
this is a genius approach man, whatever with some good state management you can apply the runtime switch as well.Assignation
F
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
Fungal answered 11/8, 2022 at 7:39 Comment(0)
C
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

Carley answered 4/12, 2022 at 12:41 Comment(0)
U
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.

Underdone answered 27/8, 2023 at 13:24 Comment(0)
E
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;
  }
Emerald answered 27/1, 2021 at 16:0 Comment(0)
E
1

I use the following code:

final appLocalizations = await AppLocalizations.delegate
      .load(Locale(Platform.localeName.substring(0, 2)));
print(appLocalizations.yourCode)
Edric answered 8/11, 2023 at 6:12 Comment(0)
H
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!;
  },
)
Hagen answered 2/1 at 8:32 Comment(0)
B
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.

Bemuse answered 5/4 at 14:15 Comment(0)
M
0

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

Maurey answered 26/5 at 6:17 Comment(0)
P
0

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

Text(t.loginPage.title)
Plater answered 22/6 at 21:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.