Flutter package: Easy and powerful internationalization using Dart extensions.

Overview

pub package

i18n_extension

Non-boilerplate Translation and Internationalization (i18n) for Flutter

Start with a widget with some text in it:

Text("Hello, how are you?")

Translate it simply by adding .i18n to the string:

Text("Hello, how are you?".i18n)

Or you can also use identifiers, as you prefer:

Text(greetings.i18n)

If the current locale is 'pt_BR', then the text in the screen will be "Olá, como vai você?", the Portuguese translation to the above text. And so on for any other locales you want to support.

You can also provide different translations depending on modifiers, for example plural quantities:

print("There is 1 item".plural(0)); // Prints 'There are no items'
print("There is 1 item".plural(1)); // Prints 'There is 1 item'
print("There is 1 item".plural(2)); // Prints 'There are 2 items'

And you can invent your own modifiers according to any conditions. For example, some languages have different translations for different genders. So you could create gender versions for Gender modifiers:

print("There is a person".gender(Gender.male)); // Prints 'There is a man'
print("There is a person".gender(Gender.female)); // Prints 'There is a woman'
print("There is a person".gender(Gender.they)); // Prints 'There is a person'

Also, interpolating strings is easy, with the fill method:

// Prints 'Hello John, this is Mary' in English.
// Prints 'Olá John, aqui é Mary' in Portuguese.
print("Hello %s, this is %s".i18n.fill(["John", "Mary"]));

See it working

Try running the example.

Good for simple or complex apps

I'm always interested in creating packages to reduce boilerplate. For example, async_redux is about Redux without boilerplate, and align_positioned is about creating layouts using fewer widgets. This current package is also about reducing boilerplate for translations, so it doesn't do anything you can't already do with plain old Localizations.of(context).

That said, this package is meant both for the one person app developer, and the big company team. It has you covered in all stages of your translation efforts:

  1. When you create your widgets, it makes it easy for you to define which strings should be translated, by simply adding .i18n to them. These strings are called "translatable strings".

  2. When you want to start your translation efforts, it can automatically list for you all strings that need translation. If you miss any strings, or if you later add more strings or modify some of them, it will let you know what changed and how to fix it.

  3. You can then provide your translations manually, in a very easy-to-use format.

  4. Or you can easily integrate it with professional translation services, importing it from, or exporting it to any format you want.

Setup

Wrap your widget tree with the I18n widget, below the MaterialApp, together with the localizationsDelegates and the supportedLocales:

import 'package:i18n_extension/i18n_widget.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

...

@override
Widget build(BuildContext context) {
  return MaterialApp(
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en', "US"),
        const Locale('pt', "BR"),
      ],
      home: I18n(child: ...)
  );
}

Note: To be able to import flutter_localizations.dart you must add this to your pubspec.yaml:

dependencies:
  flutter_localizations:
    sdk: flutter

  i18n_extension: ^4.1.0

The code home: I18n(child: ...) shown above will translate your strings to the current system locale. Or you can override it with your own locale, like this:

I18n(
  initialLocale: Locale("pt", "BR"),
  child: ...

Note: Don't ever put translatable strings in the same widget where you declared the I18n widget, since they may not respond to future locale changes. For example, this is a mistake:

Widget build(BuildContext context) {
  return I18n(
    child: Scaffold(
      appBar: AppBar(title: Text("Hello there".i18n)),
      body: MyScreen(),
  );
}

You may put translatable strings in any widgets down the tree.

A quick recap of Dart locales

The correct way to create a Locale is to provide a language code (usually 2 or 3 lowercase letters) and a country code (usually 2 uppercase letters), as two separate Strings.

For example:

var locale = Locale("en", "US");

print(locale); // Prints `en_US`.
print(locale.languageCode); // Prints `en`.

You can, if you want, omit the country code:

var locale = Locale("en");

print(locale); // Prints `en`.
print(locale.languageCode); // Prints `en`.

But you cannot provide language code and country code as a single String. This is wrong:

// This will create a language called "en_US" and no country code.
var locale = Locale("en_US");

print(locale); // Prints `en_US`.
print(locale.languageCode); // Also prints `en_US`.

To help avoiding this mistake, the i18n_extension may throw an error if your language code contains underscores.

Translating a widget

When you create a widget that has translatable strings, add this default import to the widget's file:

import 'package:i18n_extension/default.i18n.dart';

This will allow you to add .i18n and .plural() to your strings, but won't translate anything.

When you are ready to create your translations, you must create a dart file to hold them. This file can have any name, but I suggest you give it the same name as your widget and change the termination to .i18n.dart.

For example, if your widget is in file my_widget.dart, the translations could be in file my_widget.i18n.dart

You must then remove the previous default import, and instead import your own translation file:

import 'my_widget.i18n.dart';

Your translation file itself will be something like this:

import 'package:i18n_extension/i18n_extension.dart';

extension Localization on String {

  static var _t = Translations("en_us") +
    {
      "en_us": "Hello, how are you?",
      "pt_br": "Olá, como vai você?",
      "es": "¿Hola! Cómo estás?",
      "fr": "Salut, comment ca va?",
      "de": "Hallo, wie geht es dir?",
    };

  String get i18n => localize(this, _t);
}

The above example shows a single translatable string, translated to American English, Brazilian Portuguese, and general Spanish, French and German.

You can, however, translate as many strings as you want, by simply adding more translation maps:

import 'package:i18n_extension/i18n_extension.dart';

extension Localization on String {

    static var _t = Translations("en_us") +
        {
          "en_us": "Hello, how are you?",
          "pt_br": "Olá, como vai você?",
        } +
        {
          "en_us": "Hi",
          "pt_br": "Olá",
        } +
        {
          "en_us": "Goodbye",
          "pt_br": "Adeus",
        };

  String get i18n => localize(this, _t);
}

Strings themselves are the translation keys

The locale you pass in the Translations() constructor is called the default locale. For example, in Translations("en_us") the default locale is en_us. All translatable strings in the widget file should be in the language of that locale.

The strings themselves are used as keys when searching for translations to the other locales. For example, in the Text below, "Hello, how are you?" is both the translation to English, and the key to use when searching for its other translations:

Text("Hello, how are you?".i18n)

If any translation key is missing from the translation maps, the key itself will be used, so the text will still appear in the screen, untranslated.

If the translation key is found, it will choose the language according to the following rules:

  1. It will use the translation to the exact current locale, for example en_us.

  2. If this is absent, it will use the translation to the general language of the current locale, for example en.

  3. If this is absent, it will use the translation to any other locale with the same language, for example en_uk.

  4. If this is absent, it will use the value of the key in the default language.

  5. If this is absent, it will use the key itself as the translation.

Try running the example using strings as translation keys.

Or you can, instead, use identifiers as translation keys

Instead of:

"Hello there".i18n

You can also do:

greetings.i18n

To that end, you can use the Translations.from constructor:

const appbarTitle = "appbarTitle";
const greetings = "greetings";

extension Localization on String {
    
  static final _t = Translations.from("en_us", {
    appbarTitle: {
      "en_us": "i18n Demo",
      "pt_br": "Demonstração i18n",
    },
    greetings: {
      "en_us": "Helo there",
      "pt_br": "Olá como vai",
    },    
  });

  String get i18n => localize(this, _t);    
}

Try running the example using identifiers as translation keys.

Managing keys

Other translation packages force you to define identifier keys for each translation, and use those. For example, an identifier key could be helloHowAreYou or simply greetings. And then you could access it like this: MyLocalizations.of(context).greetings.

However, having to define identifiers is not only a boring task, but it also makes it more difficult to remember the exact text of the widget.

With i18n_extension you can still use identifier keys, but you can also simply type the text you want and add .i18n to them.

If some string is already translated, and you later change it in the widget file, this will break the link between the key and the translation map. However, i18n_extension is smart enough to let you know when that happens, so it's easy to fix. You can even add this check to tests, as to make sure all translations are linked and complete.

When you run your app or tests, each key not found will be recorded to the static set Translations.missingKeys. And if the key is found but there is no translation to the current locale, it will be recorded to Translations.missingTranslations.

You can manually inspect those sets to see if they're empty, or create tests to do that automatically, for example:

expect(Translations.missingKeys, isEmpty);
expect(Translations.missingTranslations, isEmpty);

Note: You can disable the recording of missing keys and translations by doing:

Translations.recordMissingKeys = false;
Translations.recordMissingTranslations = false;

Another thing you may do, if you want, is to set up callbacks that the i18n_extension package will call whenever it detects a missing translation. You can then program these callbacks to throw errors if any translations are missing, or log the problem, or send emails to the person responsible for the translations.

To do that, simply inject your callbacks into Translations.missingKeyCallback and Translations.missingTranslationCallback.

For example:

Translations.missingTranslationCallback =
  (key, locale) =>
    throw TranslationsException("There are no translations in '$locale' for '$key'.");

Defining translations by locale instead of by key

As explained, by using the Translations() constructor you define each key and then provide all its translations at the same time. This is the easiest way when you are doing translations manually, for example, when you speak English and Spanish and are creating yourself the translations to your app.

However, in other situations, such as when you are hiring professional translation services or crowdsourcing translations, it may be easier if you can provide the translations by locale/language, instead of by key. You can do that by using the Translations.byLocale() constructor.

static var _t = Translations.byLocale("en_us") +
    {
      "en_us": {
        "Hi.": "Hi.",
        "Goodbye.": "Goodbye.",
      },
      "es_es": {
        "Hi.": "Hola.",
        "Goodbye.": "Adiós.",
      }
    };

You can also add maps using the + operator:

static var _t = Translations.byLocale("en_us") +
    {
      "en_us": {
        "Hi.": "Hi.",
        "Goodbye.": "Goodbye.",
      },
    } +
    {
      "es_es": {
        "Hi.": "Hola.",
        "Goodbye.": "Adiós.",
      }
    };

Note above, since "en_us" is the default locale you don't need to provide translations for those.

Combining translations

To combine translations you can use the * operator. For example:

var t1 = Translations("en_us") +
    {
      "en_us": "Hi.",
      "pt_br": "Olá.",
    };

var t2 = Translations("en_us") +
    {
      "en_us": "Goodbye.",
      "pt_br": "Adeus.",
    };

var translations = t1 * t2;

print(localize("Hi.", translations, locale: "pt_br");
print(localize("Goodbye.", translations, locale: "pt_br");

Translation modifiers

Sometimes you have different translations that depend on a number quantity. Instead of .i18n you can use .plural() and pass it a number. For example:

int numOfItems = 3;
return Text("You clicked the button %d times".plural(numOfItems));

This will be translated, and if the translated string contains %d it will be replaced by the number.

Then, your translations file should contain something like this:

static var _t = Translations("en_us") +
  {
    "en_us": "You clicked the button %d times"
        .zero("You haven't clicked the button")
        .one("You clicked it once")
        .two("You clicked a couple times")
        .many("You clicked %d times")
        .times(12, "You clicked a dozen times"),
    "pt_br": "Você clicou o botão %d vezes"
        .zero("Você não clicou no botão")
        .one("Você clicou uma única vez")
        .two("Você clicou um par de vezes")
        .many("Você clicou %d vezes")
        .times(12, "Você clicou uma dúzia de vezes"),
  };

String plural(value) => localizePlural(value, this, _t);

Or, if you want to define your translations by locale:

static var _t = Translations.byLocale("en_us") +
    {
      "en_us": {
        "You clicked the button %d times": 
          "You clicked the button %d times"
            .zero("You haven't clicked the button")
            .one("You clicked it once")
            .two("You clicked a couple times")
            .many("You clicked %d times")
            .times(12, "You clicked a dozen times"),
      },
      "pt_br": {
        "You clicked the button %d times": 
          "Você clicou o botão %d vezes"
            .zero("Você não clicou no botão")
            .one("Você clicou uma única vez")
            .two("Você clicou um par de vezes")
            .many("Você clicou %d vezes")
            .times(12, "Você clicou uma dúzia de vezes"),
      }
    };

The plural modifiers you can use are zero, one, two, three, four, five, six, ten, times (for any number of elements, except 0, 1 and 2), many (for any number of elements, except 1, including 0), zeroOne (for 0 or 1 elements), and oneOrMore (for 1 and more elements).

Also, according to a Czech speaker, there must be a special modifier for 2, 3 and 4. To that end you can use the twoThreeFour modifier.

Note: It will use the most specific plural modifier. For example, .two is more specific than .many. If no applicable modifier can be found, it will default to the unversioned string. For example, this: "a".zero("b").four("c:") will default to "a" for 1, 2, 3, or more than 5 elements.

Note: The .plural() method actually accepts any Object?, not only an integer number. In case it's not an integer, it will be converted into an integer. The 4 rules are: (1) If the modifier is an int, its absolute value will be used (meaning a negative value will become positive). (2) If the modifier is a double, its absolute value will be used, like so: 1.0 will be 1; Values below 1.0 will become 0; Values larger than 1.0 will be rounded up. (3) Strings will be converted to integer or if that fails to a double. Conversion is done like so: First, it will discard other chars than numbers, dot and the minus sign, by converting them to spaces; Then it will convert to int using int.tryParse; Then it will convert to double using double.tryParse; If all fails, it will be zero. (4) Other objects will be converted to a string (using the toString method), and then the above rules will apply.

Custom modifiers

You can actually create any modifiers you want. For example, some languages have different translations for different genders. So you could create .gender() that accepts Gender modifiers:

enum Gender {they, female, male}

int gnd = Gender.female;
return Text("There is a person".gender(gnd));

Then, your translations file should use .modifier() and localizeVersion() like this:

static var _t = Translations("en_us") +
  {
    "en_us": "There is a person"
        .modifier(Gender.male, "There is a man")
        .modifier(Gender.female, "There is a woman")
        .modifier(Gender.they, "There is a person"),
    "pt_br": "Há uma pessoa"
        .modifier(Gender.male, "Há um homem")
        .modifier(Gender.female, "Há uma mulher")
        .modifier(Gender.they, "Há uma pessoa"),
  };

String gender(Gender gnd) => localizeVersion(gnd, this, _t);

Interpolation

Your translations file may declare a fill method to do interpolations:

static var _t = Translations("en_us") +
  {
    "en_us": "Hello %s, this is %s",
    "pt_br": "Olá %s, aqui é %s",
  };

String get i18n => localize(this, _t);

String fill(List<Object> params) => localizeFill(this, params);

Then you may use it like this:

print("Hello %s, this is %s".i18n.fill(["John", "Mary"]));

The above code will print Hello John, this is Mary if the locale is English, or Olá John, aqui é Mary if it's Portuguese.

It uses the sprintf package internally. I don't know how closely it follows the C sprintf specification, but here it is.

Direct use of translation objects

If you have a translation object you can use the localize function directly to perform translations:

var translations = Translations("en_us") +
    {
      "en_us": "Hi",
      "pt_br": "Olá",
    };

// Prints "Hi".
print(localize("Hi", translations, locale: "en_us");

// Prints "Olá".
print(localize("Hi", translations, locale: "pt_br");

// Prints "Hi".
print(localize("Hi", translations, locale: "not valid");

Changing the current locale

To change the current locale, do this:

I18n.of(context).locale = Locale("pt", "BR");

To return the current locale to the system default, do this:

I18n.of(context).locale = null;

Note: The above will change the current locale only for the i18n_extension, and not for Flutter as a whole.

Reading the current locale

To read the current locale, do this:

// Both ways work:
Locale locale = I18n.of(context).locale;
Locale locale = I18n.locale;

// Or get the locale as a lowercase string. Example: "en_us".
String localeStr = I18n.localeStr;

// Or get the language of the locale, lowercase. Example: "en".
String language = I18n.language;

Observing locale changes

There is a global static callback you can use to observe locale changes:

I18n.observeLocale = 
  ({required Locale oldLocale, required Locale newLocale}) 
      => print("Changed from $oldLocale to $newLocale.");

Importing and exporting

This package is optimized so that you can easily create and manage all of your translations yourself, by hand.

However, for large projects with big teams you probably need to follow a more involved process:

  • Export all your translatable strings to files in some external format your professional translator, or your crowdsourcing tool uses (see formats below).

  • Continue developing your app while waiting for the translations.

  • Import the translation files into the project and test the app in each language you added.

  • Repeat the process as needed, translating just the changes between each app revision. As necessary, perform additional localization steps yourself.

Formats

The following formats may be used with translations:

Importing

Currently, only .PO and .JSON importers are supported out-of-the-box.

Note: Those importers were contributed by Johann Bauer. If you want to help creating importers for any of the other formats above, please PR here: https://github.com/marcglasberg/i18n_extension.

Add your translation files as assets to your app in a directory structure like this:

app
 \_ assets
    \_ locales
       \_ de.po
       \_ fr.po
        ...

Then you can import them using GettextImporter or JSONImporter:

import 'package:i18n_extension/io/import.dart';
import 'package:i18n_extension/i18n_extension.dart';

class MyI18n {
  static TranslationsByLocale translations = Translations.byLocale("en");

  static Future<void> loadTranslations() async {
    translations +=
        await GettextImporter().fromAssetDirectory("assets/locales");
  }
}

extension Localization on String {
  String get i18n => localize(this, MyI18n.translations);
  String plural(value) => localizePlural(value, this, MyI18n.translations);
  String fill(List<Object> params) => localizeFill(this, params);
}

For usage in main.dart, see here.

Note: When using .po files, make sure not to include the country code, because the locales are generated from the filenames which don't contain the country code and if you'd include the country codes, you'll get errors like this: There are no translations in 'en_us' for "Hello there".

Note: If you need to import any other custom format, remember importing is easy to do because the Translation constructors use maps as input. If you can generate a map from your file format, you can then use the Translation() or Translation.byLocale() constructors to create the translation objects.

The GetStrings exporting utility

An utility script to automatically export all translatable strings from your project was contributed by Johann Bauer. Simply run flutter pub run i18n_extension:getstrings in your project root directory and you will get a list of strings to translate in strings.json. This file can then be sent to your translators or be imported in translation services like Crowdin, Transifex or Lokalise. You can use it as part of your CI pipeline in order to always have your translation templates up to date.

Note the tool simply searches the source code for strings to which getters like .i18n are applied. Since it is not very smart, you should not make it too hard:

print("Hello World!".i18n); // This would work.

// But the tool would not be able to spot this 
// since it doesn't execute the code.
var x = "Hello World";
print(x.i18n);

Other ways to export

As previously discussed, i18n_extension will automatically list all keys into a map if you use some unknown locale, run the app, and manually or automatically go through all the screens. For example, create a Greek locale if your app doesn't have Greek translations, and it will list all keys into Translations.missingTranslationCallback.

Then you can read from this map and create your exported file. There is also this package that goes through all screens automatically.

FAQ

Q: Do I need to maintain the translation files as Dart files?

A: Not really. You do have a Dart file that creates a Translation object, yes, and this object is optimized for easily creating translations by hand. But it creates them from maps. So if you can create maps from some file you can use that file. For example, a simple code generator that reads .json und outputs Dart maps would do the job: var _t = Translations("en_us") + readFromJson("myfile.json").


Q: How do you handle changing the locale? Does the I18n class pick up changes to the locale automatically or would you have to restart the app?

A: It should pick changes to the locale automatically. Also, you can change the locale manually at any time by doing I18n.of(context).locale = Locale("pt", "BR");.


Q: What's the point of importing 'default.i18n.dart'?

A: This is the default file to import from your widgets. It lets the developer add .i18n to any strings they want to mark as being a "translatable string". Later, someone will have to remove this default file and add another one with the translations. You basically just change the import later. The point of importing 'default.i18n.dart' before you create the translations for that widget is that it will record them as missing translations, so that you don't forget to add those translations later.


Q: Can I do translations outside of widgets?

A: Yes, since you don't need access to context. It actually reads the current locale from I18n.locale, which is static, and all the rest is done with pure Dart code. So you can translate anything you want, from any code you want. You can also define a locale on the fly if you want to do translations to a locale different from the current one.


Q: By using identifier keys like howAreYou, I know that there's a localization key named howAreYou because otherwise my code wouldn't compile. There is no way to statically verify that "How are you?".i18n will do what I want it to do.

A: i18n_extension lets you decide if you want to use identifier keys like howAreYou or not. Not having to use those was one thing I was trying to achieve. I hate having to come up with these keys. I found that the developer should just type the text they want and be done with it. In other words, in i18n_extension you don't need to type a key; you may type the text itself (in your default language). So there is no need to statically verify anything. Your code will always compile when you type a String, and that exact string will be used for your default language. It will never break.


Q: But how can I statically verify that a string has translations? Just showing the translatable string as defined in the source code will not hide that some translations are missing?

A: You can statically verify that a string should have translations because it has .i18n attached to it. What you can't do is statically verify that those translations were actually provided for all supported languages. But this is also the case when you use older methods. With the older methods you also just know it should have translations, because it has a translation key, but the translation itself may be missing, or worse yet, outdated. With i18n_extension at least you know that the translation to the default language exists and is not outdated.


Q: What happens if a developer tries to call i18n on a string without translations, wouldn't that be harder to catch?

A: With i18n_extension you can generate a report with all missing translations, and you can even add those checks to tests. In other words, you can just freely modify any translatable string, and before your launch you get the reports and fix all the translations.


Q: There are a lot of valid usages for String that don't deal with user-facing messages. I like to use auto-complete to see what methods are available (by typing someString.), and seeing loads of unrelated extension methods in there could be annoying.

A: The translation extension is contained in your widget file. You won't have this extension in scope for your business classes, for example. So .i18n will only appear in your auto-complete inside of your widget classes, where it makes sense.


Q: Do I actually need one .i18n.dart (a translations file) per widget?

A: No you don't. It's suggested that you create a translation file per widget if you are doing translations by hand, but that's not a requirement. The reason I think separate files is a good idea is that sometimes internationalization is not only translations. You may need to format dates in specific ways, or make complex functions to create specific strings that depend on variables etc. So in these cases you will probably need somewhere to put this code. In any case, to make translations work all you need a Translation object which you can create in many ways, by adding maps to it using the + operator, or by adding other translation objects together using the * operator. You can create this Translation objects anywhere you want, in a single file per widget, in a single file for many widgets, or in a single file for the whole app. Also, if you are not doing translations by hand but importing strings from translation files, then you don't even need a separate file. You can just add extension Localization on String { String get i18n => localize(this, Translations("en_us") + load("file.json")); } to your own widget file.


Q: Won't having multiple files with extension Localization lead to people importing the wrong file and have translations missing?

A: The package records all your missing translations, and you can also easily log or throw an exception if they are missing. So you will know if you import the wrong file. You can also add this reports to your unit tests. It will let you know even if you import the right file and translations are missing in some language, and it will let you know even if you import from .arb files and translations are missing in some language.


Q: Are there importers for X?

A: Currently, only .PO and .JSON importers are supported out-of-the-box. Keep in mind this lib development is still new, and I hope the community will help writing more importers/exporters. We hope to have those for .arb .icu .xliff .csv and .yaml, but we're not there yet. However, since the Translations object use maps as input/output, you can use whatever file you want if you convert them to a map yourself.


Q: How does it report missing translations?

A: _At the moment you should just print Translations.missingKeys and Translations.missingTranslations. We'll later create a Translations.printReport() function that correlates these two pieces of information and outputs a more readable report.


Q: The package says it's "Non-boilerplate", but doesn't .i18n.dart contain boilerplate?

A: The only necessary boilerplate for .i18n.dart files is static var _t = Translations("...") + and String get i18n => localize(this, _t);. The rest are the translations themselves. So, yeah, it's not completely without boilerplate, but saying "Less-boilerplate" is not that catchy.


The Flutter packages I've authored:

My Medium Articles:

My article in the official Flutter documentation:

---
Marcelo Glasberg:
https://github.com/marcglasberg
https://twitter.com/glasbergmarcelo
https://stackoverflow.com/users/3411681/marcg
https://medium.com/@marcglasberg

Comments
  • Extension broken with recent updates in Dev channel

    Extension broken with recent updates in Dev channel

    Target dart2js failed: Exception: ../../../hostedtoolcache/flutter/1.22.0-12.0.pre-dev/x64/.pub-cache/hosted/pub.dartlang.org/i18n_extension-1.5.1/lib/i18n_widget.dart:197:59:
    15
    Error: No named parameter with the name 'nullOk'.
    16
        var newSystemLocale = Localizations.localeOf(context, nullOk: true);
    17
                                                              ^^^^^^
    18
    ../../../hostedtoolcache/flutter/1.22.0-12.0.pre-dev/x64/packages/flutter/lib/src/widgets/localizations.dart:413:17:
    19
    Info: Found this candidate, but the arguments don't match.
    20
      static Locale localeOf(BuildContext context) {
    21
                    ^^^^^^^^
    
    

    The breaking changes might be coming to other channels:

    https://github.com/flutter/flutter/issues/74519

    opened by maxim-saplin 13
  • Cannot import

    Cannot import "real" .po files

    PO files typically start with a section: msgid "" msgstr "Lots of header information"

    This results in an empty key at the very beginning of the every file.

    The add code throws an exception when that happens, because the "key.isEmpty" is triggering. Suggestion: When an add for an empty key is attempted just return without doing anything.

    void add({
        @required String locale,
        @required String key,
        @required String translatedString,
      }) {
        if (locale == null || locale.isEmpty) throw TranslationsException("Missing locale.");
        if (key == null || key.isEmpty) throw TranslationsException("Missing key.");
        if (translatedString == null || translatedString.isEmpty)
          throw TranslationsException("Missing translatedString.");
        // ---
    
    opened by tiloc 11
  • Provide example for JSON import loadTranslations

    Provide example for JSON import loadTranslations

    By now you can use the JSON import function that is great, thank you. But it's not clear to me where and how I can import my json translations.

    I want to use a central json file for all translations through the app. The best way I think is to load the translation file one time into a variable that is used by extension Localization on String. Or do I have to load the translation file every time I need the .i18n extension?

    question 
    opened by HelloBytes 11
  • pub get fails with 4.2.0 depends on analyzer >=1.0.0 <3.0.0

    pub get fails with 4.2.0 depends on analyzer >=1.0.0 <3.0.0

    for current version

    name: i18n_extension
    version: 4.2.0
    
    environment:
      sdk: '>=2.15.0 <3.0.0'
    
    dependencies:
      analyzer: ^3.0.0
    

    so why am I getting this version solving failed message?

    Because no versions of freezed match >1.1.1 <2.0.0 and freezed 1.1.1 depends on analyzer ^3.0.0, freezed ^1.1.1 requires analyzer ^3.0.0.
    
    And because flutter_quill >=3.9.6 depends on i18n_extension ^4.2.0 which depends on analyzer >=1.0.0 <3.0.0, freezed ^1.1.1 is incompatible with flutter_quill >=3.9.6.
    

    why does it say i18n_extension ^4.2.0 which depends on analyzer >=1.0.0 <3.0.0?

    opened by pxsanghyo 10
  • .i18n.fill doesnt generate string with flutter pub run i18n_extension:getstrings

    .i18n.fill doesnt generate string with flutter pub run i18n_extension:getstrings

    When I run flutter pub run i18n_extension:getstrings all strings get exported, apart from interpolated strings that I generate with .i18n.fill Is that a bug in the getstrings generator?

    It seems that the generator ignores strings that have a linebreak before .i18n or .fill.

    so if I have

    'mysamplestring %s'
    .i18n
    .fill(test)
    

    it doesnt detect the string during generation

    but

    'mysamplestring %s'.i18n.fill(test)

    works

    I assume this is a bug in how getStrings detects strings in code files

    opened by Freundschaft 8
  • Issue importing .po files,

    Issue importing .po files, "Translation key is missing"

    When trying to import the .po files as described here, I get the message:

    ➜ Translation key in 'en_GB' is missing: ...
    

    and nothing is translated.

    My main.dart

    class MyApp extends StatefulWidget {
      // This widget is the root of your application.
      @override
      _MyAppState createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      Future<void>? loadAsync;
    
      @override
      Widget build(BuildContext context) {
        return AdaptiveTheme(
          light: ThemeData(brightness: Brightness.light),
          initial: AdaptiveThemeMode.system,
          builder: (theme, darkTheme) => MaterialApp(
            title: 'My app',
            theme: theme,
            darkTheme: darkTheme,
            localizationsDelegates: [
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
              GlobalCupertinoLocalizations.delegate,
            ],
            supportedLocales: [
              const Locale('en'),
              const Locale('de'),
            ],
            home: FutureBuilder(
              future: loadAsync,
              builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
                if (snapshot.connectionState == ConnectionState.done) {
                  return I18n(child: NavigationScreen());
                }
                return Shimmer.fromColors(
                    child: NavigationScreen(),
                    baseColor: Theme.of(context).primaryColor,
                    highlightColor: Theme.of(context).accentColor);
              },
            ),
            routes: {
              MapScreen.routeName: (context) => MapScreen(),
              AuthScreen.routeName: (context) => AuthScreen(),
            },
          ),
        );
      }
    
      @override
      void initState() {
        super.initState();
        loadAsync = MyI18n.loadTranslations();
      }
    }
    
    My i18n.dart setup (like @bauerj's setup):

    Just like here

    import 'package:i18n_extension/io/import.dart';
    
    import 'package:i18n_extension/i18n_extension.dart';
    
    class MyI18n {
      static TranslationsByLocale translations = Translations.byLocale("en_GB");
    
      static Future<void> loadTranslations() async {
        translations +=
            await GettextImporter().fromAssetDirectory("assets/locales");
      }
    }
    
    extension Localization on String {
      String get i18n => localize(this, MyI18n.translations);
      String plural(int value) => localizePlural(value, this, MyI18n.translations);
      String fill(List<Object> params) => localizeFill(this, params);
    }
    

    The translated widget

    import '../i18n.dart'; //Importing the i18n.dart file mentioned above ↑
    
    Text(
        "Welcome back 👋!".i18n,
        textAlign: TextAlign.start,
    ),
    

    The .po file (named de.po)

    msgctxt "./lib/screens/dashboard_screen.dart:47"
    msgid "Welcome back 👋!"
    msgstr "Wilkommen zurück 👋!"
    

    The problem

    I tried to do everything as described in issue #63, but I can't get it to work with the .po-files, I just keep on getting the message ➜ Translation key in 'en_GB' is missing:....

    Does anyone know how to fix this, maybe @bauerj? I don't know if it's just my .po file that's the problem, because apart from that I tried to do everything just like in the paperless_app repository from @bauerj.


    PS: I really love this package, it's the best flutter package for translations, because it's so easy and it even supports .po files, that's awesome! 🥳
    opened by Namli1 7
  • possibly an issue with the .pot format

    possibly an issue with the .pot format

    Currently, the getstrings utility puts the file name and the line number into the context field msgctxt However, the specs as I understand them, state that this exact thing shouldn't be done.

    Here's the quote:

    The msgctxt string is visible in the PO file to the translator. You should try to make it somehow canonical and never changing. Because every time you change an msgctxt, the translator will have to review the translation of msgid.

    Finding a canonical msgctxt string that doesn’t change over time can be hard. But you shouldn’t use the file name or class name containing the pgettext call – because it is a common development task to rename a file or a class, and it shouldn’t cause translator work.

    And indeed, as soon as I change a few lines of code in my project, the getstrings utility generates new context with different line numbers, which marks the strings as needing to be reviewed in Poedit, and this does cause me additional unnecessary translator work.

    Furthermore, the specs mention an example, where the line of code goes into the reference field, which is a field prefaced by #:

    I'm new to the format, so maybe I'm misunderstanding something? @bauerj could you weigh in, please?

    bug 
    opened by CodeSpartan 6
  • Make CI run tests and code check

    Make CI run tests and code check

    This adds a Github Action that runs a few sanity checks against the code on each commit:

    • the i18n_extension test suite should pass
    • the code should adhere to the Flutter recommended style
    • static code analysis through flutter analyze passes

    This makes it easier to spot issues in pull requests like the one with Windows line endings I had a while ago.

    I had to raise the required version of the args library because flutter analyze complained about its non null-safety. So I guess the CI system already paid off 🧤

    opened by bauerj 6
  • pub run i18n_extension:getstrings  malfunctions if you have comments with single apostrophes (eg don't, it's, 10's)

    pub run i18n_extension:getstrings malfunctions if you have comments with single apostrophes (eg don't, it's, 10's)

    The getstrings function seems operate by finding an opening single or double quote, and then looking for the end of the string ... then it looks to see it .i18n (etc) is applied.

    This goes wrong if you have unbalanced ' or " characters in the dart file. This can easily happen if you have comment lines with words like: don't, can't, isn't, it's .

    opened by fne00 6
  • Change static to get for a hot reload

    Change static to get for a hot reload

    Right now every documentation is tells to write static fields. But this doesn't work with hot reload. I suggest to change this to getter

    extension Localization on String {
      get _t =>
          Translations("en") +
          {
            "en": "Forgot Password?",
            "ru": "Забыли Пароль?",
          };
    
      String get i18n => localize(this, _t);
    }
    
    enhancement 
    opened by onemanstartup 6
  • How to completely suppress

    How to completely suppress "There are no translations in 'en_us' for 'someString'"?

    I just started to use your package which looks quite promising. Anyway I'm absolutely fine with having a 2 letter language code but trying

      static final _t = Translations('en') +
          {
            'en': '<Please select a country>',
            'de': '<Bitte ein Land auswählen>',
          } +
          {
            'en': 'Country',
            'de': 'Land',
          };
    

    with standard Locale( 'en', 'US' ) leads to messages

    ➜ There are no translations in 'en_us' for "Country". ➜ There are no translations in 'en_us' for "". ➜ There are no translations in 'de_de' for "Country". ➜ There are no translations in 'de_de' for "".

    Could you add some switch to Translations to only check the 2-letter language code?

    That would even reduce a little bit more boilerplate code :-)

    enhancement 
    opened by hlemcke 6
  • Issue #120: add support for markForI18n

    Issue #120: add support for markForI18n

    Thanks https://github.com/bauerj for the link. This was really useful.

    First, I created a simple test case (which failed) and then I made the (indeed trivial) changes to make the test case succeed.

    opened by MarcVanDaele90 6
  • mark a string for getstrings but don't actually translate

    mark a string for getstrings but don't actually translate

    I have a number of <name, image> pairs like "apple", "car", "bike", ... and a corresponding image.

    I want to be able to use the 'name' in both

    • "How many %s do you see"
    • "Pick the first/second/middle/... %s"

    When creating the objects, I would like to mark the name for translation (such that it gets picked up by getstrings) but don't do an actual translation. I would like to defer the actual translation to where I use the 'name', like in "How many %s do you see".i18n.fill([name.plural(100)] or something along those lines
    (note: I assume that plural(100) is valid to use after 'how many' but that's another discussion I guess)

    I currently use "apple".version since I noticed that getstrings scans for i18n, fill, plural but also for version and .allVersions. But I'm sure I'm abusing getstrings here. So a dedicated markForI18n (next to the existing i18n) would be useful IMO.

    third-party feature 
    opened by MarcVanDaele90 8
  • Incorrect plural forms from po files

    Incorrect plural forms from po files

    In some languages (e.g. Czech, Polish), there are 3 plural forms. e.g. for the English word "character" (Polish: 'znak') the 3 possible plurals as follows: znaków, znak, znaki.

    Above plurals appear for following numbers accordingly: 0 : znaków 1: znak 2,3,4: znaki 5,6,7,8,..21: znaków 22-25: znaki

    The Plural-Forms rules are correctly defined in the following pl_pl.po file:

    msgid ""
    msgstr ""
    "Project-Id-Version: poedit\n"
    "POT-Creation-Date: \n"
    "PO-Revision-Date: \n"
    "Last-Translator: \n"
    "Language-Team: \n"
    "Language: pl\n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
    "X-Generator: Poedit 3.1\n"
    
    msgid "The file contains "
    msgstr "Plik zawiera "
    
    #. This is an example of translation of a plural form of en_us word 'character' into pl_pl 'znak'
    msgid "%d character"
    msgid_plural "%d characters"
    msgstr[0] "%d znak"
    msgstr[1] "%d znaki"
    msgstr[2] "%d znaków"
    

    [Poedit 3.1 screenshot] i18n_extension_plural_issue_poedit31_screen

    The po file is loaded using the following code:

    import 'package:i18n_extension/i18n_extension.dart';
    import 'package:i18n_extension/io/import.dart';
    import 'package:logger/logger.dart';
    
    class MyI18n {
      static TranslationsByLocale translations = Translations.byLocale("en_US");
    
      static Future<void> loadTranslations() async {
        translations +=
        await GettextImporter().fromAssetDirectory("assets/locales");
        logger.i('\n\n***********\nMain. Init. Load Translations \n$translations\n*************\n\n');
      }
    }
    
    extension Localization on String {
      String get i18n => localize(this, MyI18n.translations);
      String fill(List<Object> params) => localizeFill(this, params);
      String plural(value) => localizePlural(value, this, MyI18n.translations);
      String version(Object modifier) => localizeVersion(modifier, this, MyI18n.translations);
      Map<String?, String> allVersions() => localizeAllVersions(this, MyI18n.translations);
    }
    

    And the i18n_extension translations in the code are as follows:

        Text('The file contains '.i18n
            + '%d character'.i18n.plural(0) + '.   [Not OK, should be: \'znaków\']'),
        Text('The file contains '.i18n
            + '%d character'.i18n.plural(1) + '.   [OK, should be: \'znak\']'),
        Text('The file contains '.i18n
            + '%d character'.i18n.plural(2) + '.   [OK, should be: \'znaki\']'),
        Text('The file contains '.i18n
            + '%d character'.i18n.plural(5) + '.   [Not OK, should be: \'znaków\']'),
        Text('The file contains '.i18n
            + '%d character'.i18n.plural(55) + '.   [Not OK, should be: \'znaków\']'),
        Text('The file contains '.i18n
            + '%d character'.i18n.plural(52) + '.   [OK, should be: \'znaki\']'),
    

    The output in the app is as follows:

    i18n_extension_plural_issue_app_screen

    [ISSUE] The problem is that the plural modifier is not taking into account the Plural-Forms definition from po file. Do you have any idea how to fix it?

    third-party feature 
    opened by maciejpos 2
  • Issue with updating the app language accross the app from the child widget to ancestor widget.

    Issue with updating the app language accross the app from the child widget to ancestor widget.

    Hi Team, I'm facing the issue with updating my app language which includes multiple screens. At the time I change my locale from the parent screen it changes but from the child screen, it didn't work.

    In simple terms. Consider I've changed the locale in the homeScreen & moving to a different route from homeScreen to Screen1 via Navigator. of(context).pushNamed(Screen1.routeName).

    Then if put the logic in screen2 then the app language does not change.

    I've attested the small code which you can clone it to view the issue.

    The small reproducible code is https://github.com/akshayindia/Flutter-Internalization

    opened by akshayindia 3
  • Error

    Error "Translation key in 'en_us' is missing: [...]" while using custom modifiers

    Hi there,

    I'm using a custom modifier and this error appears in the debug console:

    flutter: ➜ Translation key in 'en_us' is missing: 
    "￿Status￿CounterOrderStatus.open￾Offen￿CounterOrderStatus.progress￾in 
    Bearbeitung￿CounterOrderStatus.delivery￾in Zustellung￿CounterOrderStatus.closed￾Abgeschlossen￿
    CounterOrderStatus.canceled￾Storniert".
    

    I think the debug message is incorrect, because the translation works as expected.


    My code

    used version: 4.2.1

    My enum

    enum CounterOrderStatus {
      open,
      progress,
      delivery,
      closed,
      canceled,
    }
    

    My translation file looks like this:

    import '_IMPORT OF ENUM_';
    import 'package:i18n_extension/i18n_extension.dart';
    
    const status = "status";
     
    extension Localization on String {
      static final _t = Translations.from("en_us", {
        status: {
          "en_us": "Status"
              .modifier(CounterOrderStatus.open, 'Open')
              .modifier(CounterOrderStatus.progress, 'in progress')
              .modifier(CounterOrderStatus.delivery, 'in delivery')
              .modifier(CounterOrderStatus.closed, 'closed')
              .modifier(CounterOrderStatus.canceled, 'canceled'),
          "de_de": "Status"
              .modifier(CounterOrderStatus.open, 'Offen')
              .modifier(CounterOrderStatus.progress, 'in Bearbeitung')
              .modifier(CounterOrderStatus.delivery, 'in Zustellung')
              .modifier(CounterOrderStatus.closed, 'Abgeschlossen')
              .modifier(CounterOrderStatus.canceled, 'Storniert'),
        },
      });
    
      String get i18n => localize(this, _t);
    
      String fill(List<Object> params) => localizeFill(this, params);
    
      String plural(int value) => localizePlural(value, this, _t);
    
      String counterOrderStatus(CounterOrderStatus counterOrderStatus) =>
          localizeVersion(counterOrderStatus, this, _t);
    
    }
    
    

    My Widget

    Container(
          padding: const EdgeInsets.all(4),
          child: Text(
            status.i18n.counterOrderStatus(CounterOrderStatus.open),
          ),
       ),
    
    opened by ayumakesit 0
Owner
Marcelo Glasberg
Marcelo Glasberg
SKAlertDialog - A highly customizable, powerful and easy-to-use alert dialog for Flutter.

SKAlertDialog A highly customizable, powerful and easy-to-use alert dialog for Flutter. GIF Screenshots SKAlertDialog Basic Alert Alert with buttons A

Senthil_Kumar 7 May 18, 2022
Powerful Complete and Beautiful Search Component for Flutter

A highly customizable search component to accelerate your development. Overview There are many search or search components for Flutter, however this o

Tiagosito 31 Jul 27, 2022
PowerFileView - A powerful file view widget, support a variety of file types, such as Doc Eexcl PPT TXT PDF and so on, Android is implemented by Tencent X5, iOS is implemented by WKWebView.

PowerFileView - A powerful file view widget, support a variety of file types, such as Doc Eexcl PPT TXT PDF and so on, Android is implemented by Tencent X5, iOS is implemented by WKWebView.

Yao 8 Oct 22, 2022
A Simple and easy to use flutter package for showing progress bar.

progress_dialog A Simple and easy to use flutter package for showing progress bar. #Usage Import the package import 'package:custom_progress_dialog/cu

Vikas Jilla 6 May 23, 2022
A Flutter Package for easy building dialogs

Easy Dialog package helps you easily create basic or custom dialogs. For extended documentation visit project pub package. Star ⭐ this repo if you lik

Ricardo Niño 39 Oct 14, 2022
This flutter package provides an easy implementation of a Slider Button to cancel current transaction or screen

This flutter package provides an easy implementation of a Slider Button to cancel current transaction or screen

null 222 Nov 8, 2022
RFlutter Alert is super customizable and easy-to-use alert/popup dialogs for Flutter.

RFlutter Alert is super customizable and easy-to-use alert/popup dialogs for Flutter. You may create reusable alert styles or add buttons as much as you want with ease.

Ratel 362 Jan 1, 2023
A Redux version tailored for Flutter, which is easy to learn, to use, to test, and has no boilerplate

A Redux version tailored for Flutter, which is easy to learn, to use, to test, and has no boilerplate. Allows for both sync and async reducers.

Marcelo Glasberg 214 Dec 13, 2022
Provider support for overlay, make it easy to build toast and In-App notification.

overlay_support Provider support for overlay, make it easy to build toast and In-App notification. this library support ALL platform Interaction If yo

Bin 340 Jan 1, 2023
A popup simple topModalSheet menu button widget with handsome design and easy to use

top_modal_sheet A popup simple topModalSheet menu button widget with handsome design and easy to use. Installations Add top_modal_sheet: ^1.0.0 in you

Baldemar Alejandres 5 Jul 29, 2022
Flutter Easy Getx Implementations .

People ask me how I manage state,dependency,routes etc when I work with flutter,Here is the Simple Brief About GetX which I used for Dummy Basic Ecommerce Concept based flutter app development .

Tasnuva Tabassum oshin 13 Oct 18, 2022
A really easy to use flutter toast library

BotToast ?? A really easy to use flutter toast library! Language: English | 中文简体 ?? Overview ?? Online Demo ?? Example ?? Renderings ?? Getting starte

null 719 Dec 28, 2022
📸 Easy to use yet very customizable zoomable image widget for Flutter, Photo View provides a gesture sensitive zoomable widget.

?? Easy to use yet very customizable zoomable image widget for Flutter, Photo View provides a gesture sensitive zoomable widget. Photo View is largely used to show interacive images and other stuff such as SVG.

Blue Fire 1.7k Jan 7, 2023
Flutter Color Picker Wheel - an easy to use widget which can be heavily customized

Flutter Color Picker Wheel Flutter Color Picker Wheel is an easy to use widget which can be heavily customized. You can use the WheelColorPicker direc

Kexin Lu 35 Oct 4, 2022
Pure Dart and Flutter package for Android,IOS and Web

Fancy Flutter Alert Dialog Pure Dart and Flutter package for Android,IOS and Web A flutter Package to show custom alert Dialog,you can choose between

Dokkar Rachid Reda 119 Sep 23, 2022
A dart package to display a horizontal bar of customisable toggle tabs. Supports iOS and Android.

toggle_bar A dart package to display a horizontal bar of customisable toggle tabs. Supports iOS and Android. Installation Depend on it. dependencies:

Prem Adithya 9 Jul 13, 2022
Custom widgets and utils using Flutter framework widgets and Dart language

reuse_widgets_and_utils The custom widgets and utils using Flutter framework widgets and Dart programming language. Getting Started This project is a

null 1 Oct 29, 2021
Package ANAlysis for Dart

A library for analyzing Dart packages. It invokes executables from the Dart SDK (or from the Flutter SDK if the package uses Flutter). Reports are cre

Dart 151 Dec 30, 2022
A Flutter package to show beautiful animated snackbars directly using overlay

Easily show beautiful snack bars directly using overlays. Create custom snack bars and show them with awesome animations.

Sajad Abdollahi 11 Dec 27, 2022