Git Product home page Git Product logo

i18n_extension's Introduction

pub package

Translate your app!

"Thank you for making the i18n_extension plugin. It has helped me a lot in my latest project and I will surely use it again in my next Flutter project. It is so easy to set up and use and the code boilerplate is indeed very minimal."

— Tomáš Jeřábek, Consultant/Developer

This is a Flutter package. For a Dart-only package, see i18n_extension_core

This package was mentioned by Google during the Dart 2.7 announcement

Read the Medium article

 

Start with a widget containing some text:

Text('How are you?')

Translate it by simply adding .i18n to the string:

Text('How are you?'.i18n)

If the current locale is 'pt' (the language code for Portuguese) or pt_BR (language code for Brazilian Portuguese), then the text shown in the screen will be 'Como vai?', the Portuguese translation to the above text. And so on for any other locales you want to support:

// Shows 'How are you?' when current locale is en_US.
// Shows '¿Cómo estás?' when current locale is es.
// Shows 'Comment ça va?' when current locale is fr.
Text('How are you?'.i18n)

As shown above, the original English text is itself the "translation key" that's used to look up the translation.

But you can actually use objects of any type as translation keys. By adding .i18n they will turn into translated strings in the current locale:

// Const values  
const greetings = UniqueKey();
greetings.i18n // Turns into 'How are you?' in en, 'Como vai?' in pt  

// Final variables  
final faq = 'faq';
faq.i18n // 'FAQ' in en, 'Perguntas frequentes' in pt

// Enums  
enum MyColors { red, blue }
MyColors.red.i18n // 'Red' in en, 'Vermelho' in pt
MyColors.blue.i18n // 'Blue' in en, 'Azul' in pt

// Numbers, booleans, Dates  
12.i18n // 'Twelve' in en, 'Doze' in pt
true.i18n // 'Yes' in en, 'Sim' in pt
false.i18n // 'No' in en, 'Não'
DateTime(2021, 1, 1).i18n // 'New Year' in en, 'Ano Novo' in pt

// Your own object types  
class User { ... }
User('John').i18n // 'Mr. John' in en, 'Sr. John' in pt

You can also provide different translations depending on modifiers, such as 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.
// 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, align_positioned is about creating layouts using fewer widgets, and themed is about simplifying the usage of colors and fonts. Likewise, the current package is about reducing boilerplate for translations. It does everything the plain old Localizations.of(context) does, but much easier.

It's meant for both 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 (or other objects serving as translation keys) should be translated by simply adding .i18n to them. These are called "translatable strings" or "translatable identifiers".

  2. When you want to start your translation efforts, it can automatically list for you all strings that need translation. If you miss any of them, 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: ...

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.byText('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, 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.byText('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 factory is called the default locale. For example, in Translations.byText('en_us') the default locale is en_us. The strings inside your Text widget 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.byId<T>() factory:

import 'package:flutter/foundation.dart';

final appbarTitle = UniqueKey();
final greetings = UniqueKey();

extension Localization on UniqueKey {
    
  static final _t = Translations.byId<UniqueKey>('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.


Note: The native way of doing translation in Flutter forces 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 can access the translation like this: MyLocalizations.of(context).greetings.

With i18n_extension, you can use ANY object type as translation keys. Just use Translations.byId<T>() and provide the type T of your identifier. Your T can be anything, including String, int, double, DateTime, or even your own custom object types, as long as they implement == and hashCode.

Don't forget that your extensions need to be on your type. For example, if you use int as your key type, you need to declare extension Localization on int { ... }.

If your T is Object or Object? or dynamic, then anything can be translated, and you need to write: extension Localization on Objec? { ... }

Recommended way

We believe having to define identifiers is a boring task, and makes it difficult for you to remember the exact text of the translations without having to look at the translation file.

For this reason we recommend you to simply type the text you want as a String inside your Text() widgets, and add .i18n to them.

The exception is when you have very large texts that you need to translate, like for example privacy policies, terms of use, long explanations etc. In those cases, you may want to use identifiers, while keeping the rest as string keys.

In the example below, privacyPolicy and termsOfUse are used as identifiers, while My Settings, Ok and Back are used as string keys:

import 'package:flutter/foundation.dart';

final privacyPolicy = UniqueKey();
final termsOfUse = UniqueKey();

extension Localization on Object {
    
  static final _t = Translations.byId<Object>('en_us', {
    privacyPolicy: { 'en_us': 'Very Looong text', 'pt_br': 'Very Looong text' },
    termsOfUse: { 'en_us': 'Very Looong text', 'pt_br': 'Very Looong text' },
    'My Settings': { 'en_us': 'My Settings', 'pt_br': 'Meus ajustes' },    
    'Ok': { 'en_us': 'Ok', 'pt_br': 'Salvar ajustes' },
    'Back': { 'en_us': 'Back', 'pt_br': 'Voltar' },
  });

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

You use them like this, respectively:

Text(privacyPolicy.i18n);
Text(termsOfUse.i18n);
Text('My Settings'.i18n);
Text('Ok'.i18n);
Text('Back'.i18n);

Finding missing translations

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 $key.');

Defining translations by locale instead of by key

As explained, by using the Translations.byText() 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 could omit the translations for it.

Combining translations

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

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

var t2 = Translations.byText('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.byText('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 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 int, or if that fails to 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 using int.tryParse; Then it will convert 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.byText('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.byText('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.byText('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.');

Const Translations

The ConstTranslations class allows you to define the translations as a const object, all at once. This not only is a little bit more efficient, but it's also better for "hot reload", since a const variable will respond to hot reloads, while final variables will not. Here you provide all locale translations of the first translatable string, then all locale translations of the second one, and so on:

static const _t = ConstTranslations('en_us',
   {
     'i18n Demo': {
       'en_us': 'i18n Demo',
       'pt_br': 'Demonstração i18n',
     },
     'Some text': {
       'en_us': 'Some text',
       'pt_br': 'Algum texto',
     }
   },
);

IMPORTANT: Make sure the locales you provide are correct (no spaces, lowercase etc). Since this constructor is const, the package can't normalize the locales for you. If you are not sure, call ConstTranslations.normalizeLocale(locale) on the locale before using it.

Unfortunately, the ConstTranslations class is not as flexible as the Translations class, as you can't define modifiers like plural() etc with it. This limits its usefulness.

Dart-only package

This i18n_extension Flutter package depends on the Dart-only package i18n_extension_core.

If you are creating code for a Dart server (backend) like Celest, or developing some Dart-only package yourself that does not depend on Flutter, then you can use the i18n_extension_core package directly:

import 'package:i18n_extension_core/i18n_extension_core.dart';

extension Localization on String {
   static var t = Translations.byText('en_us') + {'en_us':'Hello', 'pt_br':'Olá'};
   String get i18n => localize(this, t);
}

DefaultLocale.set('es_ES');
expect('Hello'.i18n, 'Hola');

The only important difference is that you must use DefaultLocale.set() instead of I18n.of(context).locale = ... to set the locale. And you won't have access and won't need to use the i18n widget, obviously.

Importing and exporting

The i18n_extension 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

Up to version 8.0.0, the i18n_extension package contained the importer library. It has now been separated and is now independently available as a standalone package. You can find it at: https://pub.dev/packages/i18n_extension_importer.

Note: Those importers were contributed by Johann Bauer, and were separated into their own package by Xiang Li.

Currently, only .PO and .JSON importers are supported out-of-the-box. If you want to help creating importers for any of the other formats above, please PR there.

It also includes the GetStrings exporting utility, which is a useful script designed to automate the export of all translatable strings from your project.

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_importer/io/import.dart';
import 'package:i18n_extension/i18n_extension.dart';

class MyI18n {
  static var 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

A utility script to automatically export all translatable strings from your project was also contributed by Johann Bauer.

Simply run flutter pub run i18n_extension_importer: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.byText('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.byText('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.byText('...') + 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

i18n_extension's People

Contributors

bauerj avatar blmayer avatar creativecreatorormaybenot avatar crizant avatar hatch01 avatar iuriegutu95 avatar marcglasberg avatar marciorr avatar mark8044 avatar maxkreja avatar namli1 avatar sachin-dahal avatar smitsonani avatar softwyer avatar tiloc avatar veselv2010 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

i18n_extension's Issues

[Suggestions] Load from file

Did you think that is a good idea to load translations from file through a singleton like following ?
(Why i do that ? Because a json file can be easily be shared / generated with my team)

It works, but i didn't know if it's a very bad practice...

import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:i18n_extension/i18n_extension.dart';

class TranslationData {
  List translations;

  static final TranslationData _singleton = TranslationData._internal();

  factory TranslationData() => _singleton;

  TranslationData._internal();

  Future initTranslations() async {
    String json = await rootBundle.loadString("assets/translations.json");
    TranslationData()..translations = jsonDecode(json);
  }
}

extension Localization on String {
  static _load() {
    Translations data = Translations("id");
    List translations = TranslationData().translations;
    for (dynamic item in translations) {
      Map<String, String> lol = Map<String, String>.from(item);
      data += lol;
    }
    return data;
  }

  static var _t = _load();

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

main

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  TranslationData translations = TranslationData();
  await translations.initTranslations(),
  runApp(App());
}

The json file

[
  {
    "id": "sign_in",
    "en": "SIGN IN",
    "fr": "SE CONNECTER",
    "es": "CONECTARSE"
  },
  {
    "id": "sign_up",
    "en": "SIGN UP",
    "fr": "S'ENREGISTER",
    "es": "REGISTRARSE"
  },
  {
    "id": "email_label",
    "en": "Email",
    "fr": "Email",
    "es": "Email"
  },
...
]

some tests are failing with recent flutter

yuecelm@laptop:~/git/i18n_extension$ git rev-parse HEAD
08e4a78b00b165a26fb834640c48a615f3c56556
yuecelm@laptop:~/git/i18n_extension$ flutter test
Running "flutter pub get" in i18n_extension...                      1.3s
00:07 +10: /home/yuecelm/git/i18n_extension/test/i18n_extension_test.dart: Translate manually.                                                                                                             
➜ Translation key in 'en_us' is missing: "Goodbye".
00:07 +11: /home/yuecelm/git/i18n_extension/test/default_import_test.dart: Record missing keys used with .i18n of the default import.                                                                      
➜ Translation key in '' is missing: "Hello".
00:07 +11: /home/yuecelm/git/i18n_extension/test/i18n_extension_test.dart: Translate using the extension.                                                                                                  
➜ Translation key in 'en_us' is missing: "XYZ".
➜ Translation key in 'en_us' is missing: "XYZ".
00:07 +12: /home/yuecelm/git/i18n_extension/test/default_import_test.dart: Record missing keys used with .i18n of the default import.                                                                      
➜ Translation key in '' is missing: "Goodbye".
00:07 +12: /home/yuecelm/git/i18n_extension/test/i18n_extension_test.dart: Record missing keys and missing translations.                                                                                   
➜ Translation key in 'en_us' is missing: "Unknown text".
➜ There are no translations in 'xx_yy' for "Hi.".
00:07 +14: /home/yuecelm/git/i18n_extension/test/default_import_test.dart: Record missing keys used with .plural() of the default import.                                                                  
➜ Translation key in '' is missing: "There are %d people".
00:07 +16: /home/yuecelm/git/i18n_extension/test/i18n_extension_test.dart: Translations with version.                                                                                                      
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
➜ Translation key in 'en_us' is missing: "�MyKey�0�abc�1�def�2�ghi�M�jkl�5�mno".
00:08 +24 -1: /home/yuecelm/git/i18n_extension/test/key_and_value_test.dart: The translatable key is usually the same as the default locale translation, but it can be different.For example { '': 'Good evening' } means that 'Good evening' is the key, and you can provide an English translation which is different than that, even if English is the default locale. This is to support the use case when you want to have fixed keys, which is NOT the preferred way to do it, but may be necessary for some advanced use cases. [E]
  No default translation for 'en_us'.
  package:i18n_extension/i18n_extension.dart 345:7  Translations.+
  key_and_value_test.dart 93:9                      Localization.t
  key_and_value_test.dart 79:14                     Localization.t
  key_and_value_test.dart 99:37                     Localization.i18n
  key_and_value_test.dart 49:20                     main.<fn>
  
00:08 +24 -2: /home/yuecelm/git/i18n_extension/test/key_and_value_test.dart: Should not accept empty keys or values. [E]                                                                                   
  No default translation for 'en_us'.
  package:i18n_extension/i18n_extension.dart 345:7  Translations.+
  key_and_value_test.dart 68:35                     main.<fn>
  
00:08 +24 -2: /home/yuecelm/git/i18n_extension/test/fallback_test.dart: If the translation to the exact locale is found, this will be returned. Otherwise, it tries to return a translation for the general language of the locale. Otherwise, it tries to return a translation for any locale with that language. Otherwise, it tries to return the key itself (which is the translation for the default locale).
➜ There are no translations in 'en_uk' for "Mobile phone".
➜ There are no translations in 'en' for "Mobile phone".
➜ There are no translations in 'pt_mo' for "Mobile phone".
➜ There are no translations in 'pt' for "Mobile phone".
➜ There are no translations in 'xx' for "Mobile phone".
➜ There are no translations in 'xx_yy' for "Mobile phone".
➜ There are no translations in 'pt_pt' for "Address".
➜ There are no translations in 'xx' for "Address".
➜ There are no translations in 'xx_yy' for "Address".
00:08 +25 -2: /home/yuecelm/git/i18n_extension/test/fallback_test.dart: If the translation to the exact locale is found, this will be returned. Otherwise, it tries to return a translation for the general language of the locale. Otherwise, it tries to return a translation for any locale with that language. Otherwise, it tries to return the key itself (which is the translation for the default locale).
➜ There are no translations in 'en_us' for "Mobile phone".
➜ There are no translations in 'en__us' for "Mobile phone".
➜ There are no translations in 'pt_mo' for "Mobile phone".
➜ There are no translations in 'pt' for "Mobile phone".
➜ There are no translations in 'xx' for "Mobile phone".
➜ There are no translations in 'xx_yy' for "Mobile phone".
➜ There are no translations in 'pt__br' for "Mobile phone".
➜ There are no translations in 'pt_pt' for "Address".
➜ There are no translations in 'xx' for "Address".
➜ There are no translations in 'xx_yy' for "Address".
00:08 +27 -2: Some tests failed.                                                                                                                                                                           

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 :-)

Import and Export to .po

Hello,

I see in the documentary that there is an export and an import for po. Unfortunately I don't understand how I can use it.

Are the commands for the shell?

Thanks

Add option to always (and only) use language code

Enhancement Request:

New parameter for I18n bool useLanguageCodeOnly = false

Description: whatever locale is set by initialLocale or I18n.of(context).locale it should only use the language code from the given locale. This allows to reduce translations files like this:

extension Localization on String {
  String get i18n => localize(this, _t);
  static final _t = Translations('en') +
      {
        'en': 'Current i18n locale is',
        'de': 'Aktuelle i18n Sprache ist',
      } + ...

Set locale without context

Hi,

I need this to work with flutter localization.
I have a custom AppLocalizations, when ever there is locale changed, I want to propagate the changes to i18n too, without the context.

Btw, could you provide a example or sample code how to have this work with flutter localization.

Thanks in advance.

_systemLocale null

image

I/flutter (14367): [2019-11-23 04:10:51.568618 | ConsoleHandler | INFO] NoSuchMethodError: The method 'replaceAll' was called on null.
I/flutter (14367): Receiver: null
I/flutter (14367): Tried calling: replaceAll(RegExp: pattern=^[_ ]+|[_ ]+$ flags=, "")
I/flutter (14367): [2019-11-23 04:10:51.574261 | ConsoleHandler | INFO]
I/flutter (14367): [2019-11-23 04:10:51.576518 | ConsoleHandler | INFO] ------- STACK TRACE -------
I/flutter (14367): [2019-11-23 04:10:51.598902 | ConsoleHandler | INFO] #0      Object.noSuchMethod  (dart:core-patch/object_patch.dart:51:5)
I/flutter (14367): [2019-11-23 04:10:51.602977 | ConsoleHandler | INFO] #1      I18n._trimLocale 
package:i18n_extension/i18n_widget.dart:53
I/flutter (14367): [2019-11-23 04:10:51.603718 | ConsoleHandler | INFO] #2      I18n.localeStr 
package:i18n_extension/i18n_widget.dart:48
I/flutter (14367): [2019-11-23 04:10:51.604203 | ConsoleHandler | INFO] #3      _effectiveLocale 
package:i18n_extension/i18n_extension.dart:213
I/flutter (14367): [2019-11-23 04:10:51.604531 | ConsoleHandler | INFO] #4      localize 
package:i18n_extension/i18n_extension.dart:48
I/flutter (14367): [2019-11-23 04:10:51.604829 | ConsoleHandler | INFO] #5      Localization.i18n 

There are no translations in 'en_' for key

The locale en_ does not get mapped onto en locale.

So when the local is en_, then with the translations from below it will throw the error:

➜ There are no translations in 'en_' for "not_found".

import 'package:i18n_extension/i18n_extension.dart';

extension Localization on String {

  static var t = Translations.byLocale("de") +
      {
        "en": {
          "not_found": "404 not found",
        },
        "de": {
          "not_found": "404 nicht gefunden",
        },
      };

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

Logic of missing translations

Follow up #26 but when those missing translations are not recorded, the error message should not be shown in console as well.

I have created #27 to fix it.
Sorry I should have tested before.

Cannot dynamically change locale - I18n.of(context).locale = Locale("pt_BR") not working

Hello. Thanks for the great plugin!

I'm having issues reloading the i18n locale programatically. If I change the language and hot reload the app everything works fine, but I basically want all the translations to change to the chosen language on the tap of a button.
I used I18n.of(context).locale = Locale($MY_NEW_LOCALE); like the docs suggested but I get the following error, and my strings don't change language... any idea how I can proceed?
I'm changing from english to arabic, so when I tap my change language button the entire app is becoming RTL (base locale is changing) but the translations don't change until I reload the app.

Thanks for the help

I/flutter (17829): ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════ I/flutter (17829): The following NoSuchMethodError was thrown while handling a gesture: I/flutter (17829): The getter 'data' was called on null. I/flutter (17829): Receiver: null I/flutter (17829): Tried calling: data I/flutter (17829): I/flutter (17829): When the exception was thrown, this was the stack: I/flutter (17829): #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5) I/flutter (17829): #1 I18n.of (package:i18n_extension/i18n_widget.dart:74:73) I/flutter (17829): #2 _SettingsPageState.languageButton.<anonymous closure> (package:anghamicreators/widgets/settings_page.dart:92:18) I/flutter (17829): #3 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:706:14) I/flutter (17829): #4 _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:789:36) I/flutter (17829): #5 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182:24) I/flutter (17829): #6 TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:486:11) I/flutter (17829): #7 BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:264:5) I/flutter (17829): #8 BaseTapGestureRecognizer.acceptGesture (package:flutter/src/gestures/tap.dart:236:7) I/flutter (17829): #9 GestureArenaManager.sweep (package:flutter/src/gestures/arena.dart:156:27) I/flutter (17829): #10 GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:222:20) I/flutter (17829): #11 GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:198:22) I/flutter (17829): #12 GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:156:7) I/flutter (17829): #13 GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:102:7) I/flutter (17829): #14 GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:86:7) I/flutter (17829): #18 _invoke1 (dart:ui/hooks.dart:273:10) I/flutter (17829): #19 _dispatchPointerDataPacket (dart:ui/hooks.dart:182:5) I/flutter (17829): (elided 3 frames from package dart:async) I/flutter (17829): I/flutter (17829): Handler: "onTap" I/flutter (17829): Recognizer: I/flutter (17829): TapGestureRecognizer#bef9f I/flutter (17829): ════════════════════════════════════════════════════════════════════════════════════════════════════

Newbie info for readme

Hello,

I just want add i18n support to my app.I just find your awesome extension.I followed tutorial but its not worked(Its not change lang) so i just searched for my mistakes.Followed official flutter i18n tutorial and checked this extension issues.I found my problem.I need add these to MaterialApp

https://flutter.dev/docs/development/accessibility-and-localization/internationalization

MaterialApp(
          localizationsDelegates: [
            // ... app-specific localization delegate[s] here
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          supportedLocales: [
            const Locale('en', "US"), // En
            const Locale('tr', "TR"), // TR
          ],
          ...
          ),
          home: I18n(child: screen()))

So its my first time on i18n and i dont know to do this.If you add this to readme file i think its will be helpfull for others

Where to correctly place `I18n` in wiget tree?

Hi,

the readme says, I should put I18n for parameter home below MaterialApp. But when I provide routes to MaterialApp then I cannot use home, because I have one route named '/'. Where should I put I18n correctly?

Logic of missing translations

When using Translations.byLocale, we should not need to provide the translations of the default locale like:

var t = Translations.byLocale("en_US") +
        {
          "en_us": {
            "Hi.": "Hi.",
            "Goodbye.": "Goodbye.",
          },
        };

This is boilerplate.

But if we don't provide it, an error is shown: ➜ There are no translations in 'en_US' for "Hi".
This behavior should be fixed.

I have created #25 to fix this issue, please take a look of it. Thanks.

How use i18n_extension in Isolate ?

Hi,

I've background task running in isolate, some of them throw message like this "content_of_message".i18n. Obviously i18n is not loaded in isolate, i didn't have context, so message aren't translated :).

I used to achieve that I18n.define(Locale(locale)); It works, but I got some warning because it must be used only in test environnement.

What is the best way to use i18n_extension in isolate ?

Translation.byLocale

Hi,

I'm trying the Translation by locale:

extension Localization on String {
  static var _t = Translations.byLocale("en") +
      {
        "en": {
          "Hi": "Hi",
          "Goodbye.": "Goodbye.",
        },
      } +
      {
        "nl": {
          "Hi": "Hallo",
          "Goodbye.": "Vaarwel",
        }
      };

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

and implement

return I18n(
      initialLocale: Locale("nl"),
      ...
                          Text(
                            'Hi'.i18n,
                          ),
     ...

but i get the following error

The following NoSuchMethodError was thrown building Consumer<RegisterViewModel>(dirty, dependencies: [_InheritedProviderScope<RegisterViewModel>]):
The method '[]' was called on null.
Receiver: null
Tried calling: []("Hi")

It does work when I use to normal way

import 'package:i18n_extension/i18n_extension.dart';

extension Localization on String {
  static var _t = Translations("en") +
      {
        "en": "Hi",
        "nl": "Hallo",
      };

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

What am I doing wrong?
I would want to use the byLocale because I think it's easier to maintain.

Thanks for any help
regards
Steven

Feature Request: Integration with Flutter Localizations

Flutter localizations are still required to translated Flutter widgets. It would be great to have this integration realized within i18n.

Please have a look at the example app from https://github.com/djarjo/flutter_input

I was able to combine changing both localizations with InputLanguage.
But it required to have I18n as topmost widget and even one more instance variable of Locale. Maybe there is a much more elegant solution by providing something like I18n.locale to MatterialApp even if I18n is a child of MaterialApp?

error: This requires the 'extension-methods' experiment to be enabled

I copyed the example code:

`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);
}`

but I get this error:

Error

how can i fix this bug?

issue with plurals

looks like plural is not working

Text('0 Selected'.i18n.plural(_itemsSelected.length)),

  {
    "en": "0 Selected"
    .zero("0 Selected")
    .one("1 Selected")
    .two("2 Selected")
    .many("%d Selected"),
    "es": "0 Seleccionado"
    .zero("0 Seleccionados")
    .one("1 Seleccionado")
    .two("2 Seleccionados")
    .many("%d Seleccionados"),
  } 

The following TranslationsException object was thrown building ViewBookNotesScreen(dirty, dependencies: [MediaQuery], state: _ViewBookNotesScreenState#c9334):
Translation key in 'en' is missing: '�0 Selected�0�0 Selected�1�1 Selected�2�2 Selected�M�%d Selected'.

Initial system locale is not loading

Hi, I try to fetch system locale to show content in the language of the device system but the locale that is set by default is always null which results in displaying the first language.
I have the exact same result in your example app. In your example app you support two languages: English US and Portuguese Brazil. But when I open the app, it always show content in English, even if I set device language to the second one.

EDIT: Ok, I took wrong locale to check, systemLocale is showing default locale (en_US). Yet still, the content is shown in default language instead of device locale.

Translation file produces pub.dev issue when uploading

Each translation file like flutter_input/lib/src/country.i18n.dart from https://github.com/djarjo/flutter_input

extension Localization on String {
String get i18n => localize(this, _t);
static final _t = Translations('en') +
{
'en': 'Australia',
'de': 'Australien',
} +

produces a pub.dev issue when uploading a package:

Local variable _t not used.

I modified the file to have _t as a class local variable but that seems a bit strange.

Creating locale not changed globally

Hi, I started to use your plugin this week, so I'm still at discovery stage.

One of features I try to implement is having translations by language not key. And today after your update I tried to follow docs, specifically this one:

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

But in the plugin itself (1.3.0) I cannot create Locale by two Strings, it still require one combined String. It may be desired behaviour but still it's inconsistency between docs and actual code.

Default language not working

It seems to be the default language (fallback) is not working. If some unknown language is set, for example, de_DE, the dutch(nl) translation is shown. Any English locale will display the English translation.

Main:

class MyApp extends StatelessWidget {
    
  @override
  Widget build(BuildContext context) {
    return MaterialApp(

      title: 'Timesheet',
      home: I18n(child: LoginScreen()),
      debugShowCheckedModeBanner: false,
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: const [
        Locale('nl', 'NL'), // nl
        Locale('en', 'US'), // English
        Locale('en', 'UK'), // English
      ]
    );
  }
}

Translation:

extension Localization on String {
  String get i18n => localize(this, t);

  String plural(int value) => localizePlural(value, this, t);
  static var t = Translations('en') +
      {
        'en': 'Sign in success',
        'nl': 'Inloggen gelukt',
      } +
      {
        'en': 'Sign in fail',
        'nl': 'Inloggen mislukt',
      } +
      ...
}

adding the following seems to fix the issue.

 localeResolutionCallback: (locale, locales) {
    print("FALLBACK TO ${locale.toLanguageTag()}");

    return locale;
  },

Routes issue

Good day Sir, i have an issue or maybe i just dont get something. This is my widgets tree

class MyMainApp extends StatelessWidget {
  const MyMainApp({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<SharedPreferences>(
      future: SharedPreferences.getInstance(),
      builder:
          (BuildContext context, AsyncSnapshot<SharedPreferences> snapshot) {
        if (!snapshot.hasData) {
          return _MySplashScreen();
        }
        return ChangeNotifierProvider<MyAppSettings>.value(
          value: MyAppSettings(snapshot.data),
          child: _MyMainApp(),
        );},);}}

Here i try to call I18n with routes table

class _MyMainApp extends StatelessWidget {
  const _MyMainApp({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
   return **I18n** (child:  MaterialApp(
      title: 'DS',
      theme: Provider.of<MyAppSettings>(context).isDarkMode
          ? DarkTheme
          : LightTheme,
      routes: AppRoutingTable,
)); }
}

this is my route table map

final Map<String, WidgetBuilder> AppRoutingTable = {
  Navigator.defaultRouteName: (context) => HomeRoute,
  for (MyRoute route in AllRoutes) route.routeName: (context) => route,
};
const HomeRoute =
MyRoute ( child: 
MyHomePage(),
 title: "DEMO2",
routeName: Navigator.defaultRouteName,
);

MyHomePage contain Scaffold with

 floatingActionButton: FloatingActionButton(onPressed: () =>
     I18n.of(context).locale = (I18n.localeStr == "pt_br") ? null : Locale("pt_BR")),

and

bottomNavigationBar: BottomNavigationBar(
        items:
        <BottomNavigationBarItem>[
    BottomNavigationBarItem(
      backgroundColor: Colors.blue,
      title: Text(
        "Hello.i18n",
        textAlign: TextAlign.center,
        style: TextStyle(fontSize: 20),
      ))

and with this i've create home_page.i18n.dart with

extension Localization on String {
  //
  static var t = Translations("en_us") +
      {
        "en_us": "Hello",
        "pt_br": "Demonstração i18n",
      };
  String get i18n => localize(this, t);
  String fill(List<Object> params) => localizeFill(this, params);
  String plural(int value) => localizePlural(value, this, t);
  String version(Object modifier) => localizeVersion(modifier, this, t);
  Map<String, String> allVersions() => localizeAllVersions(this, t);
}

so i got and error NoSuchMethodError: The method "replaceAll' was called on null.

Getting localizations from api or local json file

Is there a way to implement localizations strings from an api or local json file. I could not find an example. Since the api call and getting local json file is async, I could not be able to find a way to implement this.

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

Detect missing translations or keys without running the code

Is there a way to detect missing translations without running the code?
e.g. in my app I am not testing every Snackbar, Dialog or smaller UI Widget, therefore those string do not show up when using expect(Translations.missingKeys, isEmpty); expect(Translations.missingTranslations, isEmpty); in a test.

I am thinking about something like a additional rule for the dart analyzer, which seems not possible at the moment as mentioned here dart-lang/linter#697. An alternative for static analysis is mentioned here: dart-lang/linter#697 (comment)

Or is there a different way one could achieve this functionality?

.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

No fallback?

Considering:

static var t = Translations("pt") +
    {
      "pt": "ENTRAR COM GOOGLE",
      "en": "ENTER WITH GOOGLE",
      "es": "INICIA COM GOOGLE",
    };

Results in:

➜ There are no translations in 'pt_br' for "ENTRAR COM GOOGLE".

I was expecting something like pt_pt, pt_ao, pt_br too fallback to pt (I'm lazy and don't want to translate every variant of portuguese, english and spanish)

BTW, my phone is en_US (don't know why it is trying to find pt_br)

Where to put the I18n widget in relation to MaterialApp

════════ Exception caught by gesture ═══════════════════════════════════════════════════════════════
The following NoSuchMethodError was thrown while handling a gesture:
The getter 'data' was called on null.
Receiver: null
Tried calling: data

When the exception was thrown, this was the stack:
#0 Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)
#1 I18n.of (package:i18n_extension/i18n_widget.dart:74:73)
#2 ShopAddMenu.build. (package:jtw_app_flutter/ui/entertainment/entertainmentHome/shopPublish/shopAddMenu/shop_add_menu.dart:55:16)
#3 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:706:14)
#4 _InkResponseState.build. (package:flutter/src/material/ink_well.dart:789:36)
...
Handler: "onTap"
Recognizer: TapGestureRecognizer#8c92c
debugOwner: GestureDetector
state: possible
won arena
finalPosition: Offset(398.0, 52.8)
finalLocalPosition: Offset(33.6, 28.8)
button: 1
sent tap down
════════════════════════════════════════════════════════════════════════════════════════════════════

Howto: missingKeyCallback

can you provide more details about how to use this? or upload an example to your example proyect.

Translations.missingKeyCallback = (key, locale)
=> throw TranslationsException("Translation key in '$locale' is missing: '$key'.");

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

Breaks when used with injectable

Whenever I try to use this package with injectable I get this error

NoSuchMethodError: The getter 'major' was called on null.
Receiver: null
Tried calling: major
[SEVERE] injectable_generator:injectable_builder on test/widget_test.dart:

Milad-Akarie/injectable#51

How to combine 'TranslationsByLocale' with 'Translations'

Hello,

I'm using your extension were I import the translation 'bylocale' from a json file.
It is working great thank you.
I didn't find the right syntax to load plurals rules by locale, so I created them as a "regular" translations map but when I try to add them together (using + or * operator) I get
'TranslationsByLocale' is not a subtype of type 'Translations'

Please provide both:

  1. how to add plurals rules using Translations.byLocale()?
  2. how to merge (add) Translations Map with Translations.byLocale Maps
    Thx

i18n.fill() does not work

The method isn't found when I try to do this:

Text("hello %s".i18n.fill(["Test"]));

I goes the method to overwrite it in the extension is missing?
Something like
String get i18n => localize(this, _t); String plural(int value) => localizePlural(value, this, _t);

Set locale without I18n invoking setState()

Enhancement Request:

New method setLocale( Locale newLocaleToUse ) which uses the given locale for all further translations but without invoking setState() from within I18n.

Reason: there is NO solution to properly integrate with MaterialApp (see #10 ). I added additional reasons under that issue.

New parameter for I18n to set locale

Enhancement Request:

New parameter for I18n to set the locale to use. Supersedes parameter initialLocale (if both were set). Allows clean integration with MaterialApp.

Reason: there is NO solution to properly integrate with MaterialApp (see #10 ). I have added additional reasons under that issue.

Printing missing keys via tests

I'm a little bit confused. I'm trying to add some tests, just to get in touch with it but for some reason it doesn't work as I expected. Actually, I'm trying to understand this example:

Translations.missingKeys.clear();
Translations.missingTranslations.clear();

// This should call recordKey().
"Hello World".i18n;

expect(Translations.missingKeys.length, 1);
expect(Translations.missingKeys.single.locale, "");
expect(Translations.missingKeys.single.text, "Marked habit");
expect(Translations.missingTranslations, isEmpty);

When I'm running the test file, I get the output ➜ Translation key in '' is missing: "Hello World".. Why the output doesn't show the locale?

Plural improvements for Czech language.

Hello Marcelo,

First of all, thank you for making i18n_extension plugin - it has helped me a lot in my latest project and I will surely use it again in my next Flutter project - it is so easy to set up and use and the code boilerplate is indeed very minimal.

Let me suggest a simple addition to your code (although I have a Github account, I have never used a pull request to contribute to code and I do not want to break things).

For plurals you use:
/// Plural modifier for zero elements.
String zero(String text) => modifier("0", text);

/// Plural modifier for 1 element.
String one(String text) => modifier("1", text);

/// Plural modifier for 2 elements.
String two(String text) => modifier("2", text);

However, in Czech (my language) we use special word form also for amounts 3 and 4 (I am not sure if this applies to other languages as well). For amounts 5 and higher the form stays the same. Simply adding these two modifiers fixed this for me:

/// Plural modifier for 3 elements.
String three(String text) => modifier("3", text);

/// Plural modifier for 4 elements.
String four(String text) => modifier("4", text);
Example:

1 beer 1 pivo

2 beers 2 piva

3 beers 3 piva

4 beers 4 piva

5 beers 5 piv

6 beers 6 piv

7 ... 7 ...

The 'times' modifier can stay as is as most people would not care about 3 and 4 times as we Czechs do.

/// Plural modifier for any number of elements, except 0, 1 and 2.
String times(int numberOfTimes, String text) {
assert(numberOfTimes != null && (numberOfTimes < 0 || numberOfTimes > 2));
return modifier(numberOfTimes, text);
}

Please consider adding this to your code and thanks again for your excellent work.

Tomáš Jeřábek
Consultant/Developer

Did I forgot something?

Hi, I'm trying to translate a simple flutter application, but for some reason it doesn't work.

pubspec.yaml
i18n_extension: ^1.2.0

main.dart

import 'package:i18n_extension/i18n_widget.dart';
import 'main.i18n.dart';
...
home: I18n(child: MyHomePage(),)
...
appBar: AppBar(title: Text("i18n Demo" .i18n))

main.i18n.dart

import 'package:i18n_extension/i18n_extension.dart';

extension Localization on String {
  static var t = Translations("en_us") +
      {
        "en_us": "i18n Demo1",
        "de": "KLAPPT?",
      };

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

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

  String plural(int value) => localizePlural(value, this, t);

  String version(Object modifier) => localizeVersion(modifier, this, t);

  Map<String, String> allVersions() => localizeAllVersions(this, t);
}

Result: Nothing. There is no error message. However, the appbar title doesn't translate - neither in english nor in german.

Translation key in '' is missing: "Something"

Hello, I've really loved the plugin

but when I tried to use it in a personal project it always returns Translation key in '' is missing: "Something".

it's not recognizing the Locale?

My project is this: https://github.com/luizwalber/TaskManager

It's a mess, I'm learning and building it

the translations are in the main.i18n.dart file and I use the .i18n in the files inside ui->screens

Thanks a lot for the plugin, the way to implement internationalization in flutter is horrible.

Localize app name (launcher name)

Hello,

Thanks for the great library. Does it support app/launcher name localization? If not, is it possible to add this new feature?

Allow to export translation templates

Having worked with gettext before, the approach used in your project is very familiar in that source strings are used as identifiers.

xgettext is a tool that automatically generates the '.po' files which can then be sent to translators (or online services). As far as I understand, your project does not contain such a tool yet. Would you think it might be a good idea to add a small Dart utility for that purpose to your project?

It might be sufficient to run a regular expression against project files (at least for most cases). But maybe I am overlooking something there. If you feel this could benefit your project I might work on a pull request.

[Web] InheritedI18n is not supported

When I ran my Flutter app on the Web I ran across this issue:

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
js_primitives.dart:30 The following UnsupportedError was thrown building _InheritedI18n:
Unsupported operation: Platform._operatingSystem
The relevant error-causing widget was:
_InheritedI18n
file:///home/dennis/.pub-cache/hosted/pub.dartlang.org/i18n_extension-1.4.0/lib/i18n_widget.dart:154:12

This is strange because it has nothing to do with native functionality and, besides, it is listed as supported on pub.dev

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.