Rethinking localization in Flutter

I wonder how you typcially deal with this. The standard way for localization is for my understanding to one of the many packages that allow to access symbolic constants for identifiers via the current context something like

context.l10n.cacheCleared,

which will assure that you always get a localized version of that identifier.
The only annoying thing is that I need the context for it which forces me to always put the localization inside the UI when the error was detected in the business layer. This makes it especially difficult to centralize the display of Toasts in one place of the app.

The only real reason for using the context for my understanding is that it will assure that the UI will rebuild in case the user changes his language in the system settings.
But the same could be achieved if in such a case Flutter would allow to trigger a rebuild of all Widgets and widgets could simply use any localization provider they want which could even be a simple global function.

It seems to me that localization via context has gotten a so familiar pattern that it isn’t even questioned anymore.

The other advantage that you can get via InheritedWidgets and the context to scope data only to certain parts of the widget tree seems not really a needed requirement for localization.

If someone else has similar thoughts or has chosen to use different approaches to localize their app.

6 Likes

I’ve run into this in the past too.

As a more general point, I try to keep user visible error text out of the business layer and only in the UI layer as I’ve seen people tempted in the past to pass on error messages such as network or API errors out of the biz layer and display them to the user, which is almost always not something the end users want to or should see.

My approach has usually been to instead have error encoded as consts, enums, etc and then have the UI layer decide how to turn those into displayed UI elements and of course then theres no issue with accessing localised strings because the UI does have access to a context.

5 Likes

Yes that’s the way probably most of us do today. The biggest limitations IMHO is that we have code that will display a toast all over your UI instead of having it at just one place and errors routed there.
It probably is more a problem how the current packages work that only give access to generated constants that map to arb files via the context. It would be better to have access to these constants with intellisense outside of context extension methods too. (I guess I too fell victim not looking deeper into this because I was so used to only see them connected to the context.

I think you maybe misunderstood what I meant. I wasn’t suggesting to use the localisation constants in your business logic layer, I meant having separate constants or enums or whatever to represent the error as part of the contract between your UI layer and business layer and then the UI layer maps that constant to a localisation token that it then uses to look up the per locale string to display in the UI. Its an extra layer of indirection, but it means that your business layer doesn’t even know that localisation happens in the UI layer.

What would be the benefit of another indirection where I have to ensure I don’t miss a mapping if I already have unique constants in the arb file?

The benefit is not coupling your UI layer to your business layer. I don’t think the business layer should concern itself with the specific text that you display to the user do you? What if tomorrow the requirements change and instead of an error text, the product owner wants you to show an animated crying elf GIF?

We ran into the same challenges when building our app.

To make the localization messages available everywhere (even without the context) we use a global navigator key, and I wrote a short blog post explaining how we force all widgets to be redrawn.

This clearly isn’t the best way to solve it but the solution has worked well for us.

1 Like

That’s s a fair point, but at least after so many years in development I stopped to design for every future change and prefer to do a refactor if that arrises.
I probeblably would not just pass the message constant but also an addtional data field to the central error handler

We also use a global navigator key but because of the localization we typically call push the toasts from UI code.
I would probably just use some Listenable to trigger a full rebuild on the base of the app.
How do you do manage to map a certain error to its localized text?

1 Like

Most errors come from the server which are localized using the user’s language. For client errors we just use a local localized string.

2 Likes

Thats fair enough, likely not every case would call for that much engineering.

Note that I’m still not sure we are on the same page here, because I was advocating passing constants from the lower layers to the UI layer that represent the error state, not a error message, eg. a resource unavailable error coming from an API and then letting the UI layer then decide on how to present that state to the user.

(Unfortunately) we are also facing the same dilemma. As @maks has mentioned, we also try to encode errors as enums or classes so that they carry meaningful information that then can be used by UI to display corresponding error/validation message.

sealed class SleepValidationError extends Equatable {
  @override
  List<Object?> get props => [];

  final isBlocking = true;
}

class InvalidDuration extends SleepValidationError {}

class OverlappingActivities extends SleepValidationError {
  OverlappingActivities(this.activities);
  final List<Activity> activities;

  @override
  List<Object?> get props => [activities];
}

And then in UI layer:

final l10n = context.l10n;
final message = switch (error) {
  InvalidDuration() => l10n.sleepValidationInvalidDuration,
  OverlappingActivities() => l10n.sleepValidationActivitiesOverlapping,
  WakeUpAfterBedTime() => l10n.sleepValidationBedTimeMustBeBefore,
  SleepTimeInTheFuture() => l10n.sleepValidationCannotFuture,
  WakeUpShownOnPreviousDay() => l10n.sleepWarningWakeUpDisplayedOnPreviousDay,
  BedtimeShownOnNextDay() => l10n.sleepWarningBedtimeDisplayedOnNextDay,
};

final details = switch (error) {
  final OverlappingActivities e => e.activities.map((e) {
      final line = '${e.name}: ${e.startTime.toDynamicTime(context)} - ${e.endTime.toDynamicTime(context)}';
      return line;
    }).join('\n'),
  _ => null,
};

// continue to build the error widget

Obviously it adds a bit of boilerplate and synchronization code. Showing a toast would mean we would have to introduce some “triggerable” code whether it’s a BlocListener, or just a basic callback in the initState, or useEffect hook. Tracing back where the error comes from requires understanding of the given widget tree and is not instantly visible if the “state” class is located elsewhere.

Even though I’m still hesitant to put localization into the non-UI code.

3 Likes

When you use flutter_command you have a sophisticated error handling and routing in the business layer.
At least you show here an example that is rarely found in any examples for localisation and it will allow you to have all your toast created at one place

1 Like

Good experience sharing! Use the same approach to handle error handling and showing operations. Keep UI-related behavior in the UI layer to locate any doubts in the future and increase maintainability. The development flow will be fixed and clean.

@escamoteur Hi Thomas. One of the first packages I published is EXACTLY what you describe. It gets the translations directly, and rebuilds all widgets only if and when the language is changed. This allows you to define const identifiers and use them directly, by appending .i18n to them:

const greetings = Object();

Text(greetings.i18n); // Will display "Hello" in English, "Hola" in Spanish, etc.

You don’t need context, which means you get your translations working on any type of code, not only UI code.

Also, there is the option of using the original Strings as identifiers (which means you don’t need to come up with identifiers, and the code gets more readable). For example:

Text("Hello".i18n); // Will display "Hello" in English, "Hola" in Spanish, etc.

This is the Flutter package: i18n_extension | Flutter package

And the i18n_extension_core package is Dart only, and can be used in backend code.

And, I’m also creating a AI agent that will allow you to translate any app to hundreds of languages with the click of a button: https://mytext.ai and it will work with all the main localization packages, not only i18n_extension.

4 Likes

I’ve just started deep diving on localization in Flutter. While I initially was thinking the flutter_localization package would get me where I needed to go, the lack of hierarchy support in the arb format had me start looking elsewhere. I started experimenting with Slang which has the hierarchical (and YAML) support for the translations among other things. Related to your topic, it builds a globally accessibly property (default name t) so anywhere you can just do something like t.userNameField or something. Slang has the bonus of not being tied into the widget tree/build context stuff so it can be used in non-Flutter Dart programs too. I’m continuing with my experimentation before settling on a solution.

2 Likes

@hankg very interested in hearing your experiences. How do you plan to react if the user changes the language?

I have a settings panel that lets the user select the system language or override with one of the supported languages. Slang provides a list of supported languages when it generates the code. Slang has a means of updating the singleton so that the change is reflected the next time something uses the translation lookup. At the top level I’m using the same Riverpod provider I used for the flutter_localizations but in there it does the other step of updating the Slang singleton. Interestingly it is possible to subscribe to a stream that tracks all changes to the singleton as well.

After playing around with it all of last night I’d say it is a pretty good swap in replacement. The one thing that gives me pause is that it doesn’t seem to do plurals within the string definition the way some of the other JSON/YAML formats do. So when I look at how various translation management websites work with it the plurals mechanism wouldn’t be supported out of the box easily. It’s a not very edge edge case I guess. I originally wanted to use YAML not JSON, many of the sites I looked at not supporting plurals YAML at all, but it was pretty insistent on not wrapping values in quotes unlike how I’ve seen language files in other projects. So I’m back to JSON files albeit hierarchical.

3 Likes