ilteoood / flutter_i18n

I18n made easy, for Flutter!
MIT License
217 stars 57 forks source link

Fallback is not working with nested missing translations #183

Closed dirkvranckaert closed 2 years ago

dirkvranckaert commented 2 years ago

The Issue

I'm using the library for all my translation work in all my Flutter apps. However on every setup I face the same issue, translation fallback...

This is the setup I'm using in my main.dart for my latest project:

      supportedLocales: [
        const Locale("en"),
        const Locale("nl"),
        const Locale("fr"),
        const Locale("de"),
        const Locale("es"),
        const Locale("es-ES"),
      ],
      localizationsDelegates: [
        FlutterI18nDelegate(
          translationLoader: FileTranslationLoader(
            fallbackFile: "en",
            basePath: "assets/i18n",
            useCountryCode: false,
            decodeStrategies: [JsonDecodeStrategy()],
          ),
          missingTranslationHandler: (key, locale) {
            print(
                "--- Missing Key: $key, languageCode: ${locale!.languageCode}");
          },
        ),
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],

So a bunch of supported languages, and fallback locale file set to en. See below some sample json's to explain the problem. If I would leave out a root-json-block (such as the block in the example json's) then the standard fallback mechanism using spread operator would merge the two files and I would end up with the french title and the english block. However in the samples below you see that a nested key is missing, and with the standard implementation the maps are not 'correctly' merged together. The missing link here is no deep merge of the maps.

So with the below provided translation files, translating block.label2 would result in a proper French or English translated text. However translating block.label1 will always end up to be translated when the device is in English locale, but when in French locale that single key is not falled back for.

The Fix

However the fix should be quite easy. To achieve this I'm using MyFileTranslationLoader which is an exact copy of the FileTranslationLoader but I replaced the _loadFallback() with this one:

  Future _loadFallback() async {
    try {
      final Map fallbackMap = await loadFile(fallbackFile);
      _decodedMap = MapUtil.deepMergeMaps(fallbackMap, _decodedMap);
      debugPrint("Maps merged");
    } catch (e) {
      debugPrint("'Error loading translation fallback $e'");
    }
  }

The MapUtil class (file: map_util.dart) looks like this:

class MapUtil {
  static Map<K, V> deepMergeMaps<K, V>(
    Map<K, V> map1,
    Map<K, V> map2,
  ) {
    var result = Map<K, V>.of(map1);

    map2.forEach((key, mapValue) {
      var p1 = result[key] as V;
      var p2 = mapValue;

      V mapResult;
      if (result.containsKey(key)) {
        if (p1 is Map && p2 is Map) {
          Map map1 = p1 as Map;
          Map map2 = p2 as Map;
          mapResult = deepMergeMaps(map1, map2) as V;
        } else {
          mapResult = p2;
        }
      } else {
        mapResult = mapValue;
      }

      result[key] = mapResult;
    });
    return result;
  }
}

The sample files

The English file (en.json):

{
    "title": "test",
    "block": {
        "label1": "Label1",
        "label2": "Label2",
    }
}

The French file (fr.json):

{
    "title": "french test",
    "block": {
        "label2": "Label in Frenh, n°2"
    }
}
ilteoood commented 2 years ago

Hi, it LGTM. Whould you like to do a PR?

Otherwise I'll implement it in a couple of weeks.

Thank you so much

dirkvranckaert commented 2 years ago

@ilteoood Created PR #186

ilteoood commented 2 years ago

PR merged

dirkvranckaert commented 2 years ago

@ilteoood when can we expect an update of the library including this fix?

ilteoood commented 2 years ago

@dirkvranckaert Has been released with version 0.32.0