slang-i18n / slang

Type-safe i18n for Dart and Flutter
https://pub.dev/packages/slang
MIT License
459 stars 39 forks source link

Cannot generate translations when fallback_strategy set to base_locale_empty_string #247

Open talhakerpicci opened 4 days ago

talhakerpicci commented 4 days ago

Describe the bug Cannot generate translations when fallback_strategy set to base_locale_empty_string

To Reproduce Steps to reproduce the behavior:

  1. Create flutter project: flutter create slang_test
  2. Add slang packages:
    flutter pub add slang
    flutter pub add slang_flutter
    dart pub add dev:build_runner
    dart pub add dev:slang_build_runner
  3. Create translations folder: mkdir lib/translations
  4. Create translation files:
    touch lib/translations/strings_en.i18n.json
    touch lib/translations/strings_tr.i18n.json
    touch lib/translations/strings_ro.i18n.json
  5. Fill the translation files:

en:

{
    "asset": {
        "attachDoc(context=TaskType, param=attachDoc)": {
            "calibrationGroup": "You can attach up to 5 documents to calibration tasks.",
            "maintenanceGroup": "You can attach up to 5 documents to maintenance tasks.",
            "retirement, breakdown, maintenance,calibration": "---"
        },
        "bulk": {
            "changeStatus": "Change <b>Status</b> of selected tasks",
            "editAssignment": "Edit <b>Assignment</b> of selected tasks",
            "expiryDate": "Set <b>Expiry Date</b> of selected tasks",
            "uplaodDocument": "Upload <b>Document</b> to selected tasks"
        },
        "calibrationGroupAttachUpToFiveDoc": "You can attach up to 5 documents to calibration tasks.",
        "calibrationGroupCheckDocsAlreadyAttached": "Please check the documents already attached to the calibration tasks you selected.",
        "checkDocsAlreadyAttached(context=TaskType, param=checkDocsAlreadyAttached)": {
            "calibrationGroup": "Please check the documents already attached to the calibration tasks you selected.",
            "maintenanceGroup": "Please check the documents already attached to the maintenance tasks you selected.",
            "retirement, breakdown, maintenance,calibration": "---"
        },
        "detail(context=TaskType, param=detail)": {
            "calibration": "Planned Calibration",
            "maintenance": "Planned Maintenance",
            "retirement, breakdown, maintenanceGroup,calibrationGroup": "---"
        }
    }
}

tr:

{
  "asset": {
    "attachDoc(context=TaskType, param=attachDoc)": {
      "calibrationGroup": "Kalibrasyon görevlerine en fazla 5 belge ekleyebilirsiniz.",
      "maintenanceGroup": "Bakım görevlerine en fazla 5 belge ekleyebilirsiniz.",
      "retirement, breakdown, maintenance,calibration": "---"
    },
    "bulk": {
      "changeStatus": "Seçili görevlerin <b>Durumunu</b> değiştir",
      "editAssignment": "Seçilen görevlerin <b>Atamasını</b> düzenle",
      "expiryDate": "Seçili görevlerin <b>Bitiş Tarihini</b> ayarla",
      "uplaodDocument": "Seçili görevlere <b>Belge</b> yükle"
    },
    "calibrationGroupAttachUpToFiveDoc": "Kalibrasyon görevlerine en fazla 5 belge ekleyebilirsiniz.",
    "calibrationGroupCheckDocsAlreadyAttached": "Lütfen seçtiğiniz kalibrasyon görevlerine eklenmiş olan belgeleri kontrol edin.",
    "checkDocsAlreadyAttached(context=TaskType, param=checkDocsAlreadyAttached)": {
      "calibrationGroup": "Lütfen seçtiğiniz kalibrasyon görevlerine eklenmiş olan belgeleri kontrol edin.",
      "maintenanceGroup": "Lütfen seçtiğiniz bakım görevlerine eklenmiş olan belgeleri kontrol edin.",
      "retirement, breakdown, maintenance,calibration": "---"
    },
    "detail(context=TaskType, param=detail)": {
      "calibration": "Planlı Kalibrasyon",
      "maintenance": "Planlı Bakım",
      "retirement, breakdown, maintenanceGroup,calibrationGroup": "---"
    }
  }
}

ro:

{
  "asset": {
    "attachDoc(context=TaskType, param=attachDoc)": {
      "calibrationGroup": "Poti adauga maxim 5 documente pentru calibrari.",
      "maintenanceGroup": "Ati atins limita de 5 documente. Daca doriti sa adaugati alte documente puteti sterge din cele existente.",
      "retirement, breakdown, maintenance,calibration": "---"
    },
    "bulk": {
      "changeStatus": "Schimba <b>Statusul</b> sarcinii de lucru selectate.",
      "editAssignment": "Editati <b> Atribuirea </b> la sarcinilor de lucru selectate.",
      "expiryDate": "Setati <b>Data de expirare</b> a sarcinii de lucru selectate.",
      "uplaodDocument": "Incarcati <b>un document </b> la sarcina de lucru selectata."
    },
    "calibrationGroupAttachUpToFiveDoc": "Poti adauga maxim 5 documente pentru calibrari.",
    "calibrationGroupCheckDocsAlreadyAttached": "Va rugam sa verificati documentele deja introduse.",
    "checkDocsAlreadyAttached(context=TaskType, param=checkDocsAlreadyAttached)": {
      "calibrationGroup": "Please check the documents already attached to the calibration tasks you selected.",
      "maintenanceGroup": "Please check the documents already attached to the maintenance tasks you selected.",
      "retirement, breakdown, maintenance,calibration": "---"
    },
    "detail(context=TaskType, param=detail)": {
      "calibration": "Calibrare planificata",
      "maintenance": "Mentenanta Planificata",
      "retirement, breakdown, maintenanceGroup,calibrationGroup": "---"
    }
  }
}
  1. Create the enum file: touch lib/task_type_enum.dart
  2. Fill the enum file:
    
    enum TaskType {
    breakdown,
    maintenance,
    maintenanceGroup,
    calibration,
    calibrationGroup,
    retirement,
    }
8. Create build.yaml file: `touch build.yaml`
9. Fill the build.yaml file:

targets: $default: builders: slang_build_runner: options: input_directory: lib/translations output_directory: lib/translations translate_var: t fallback_strategy: base_locale_empty_string string_interpolation: braces imports:

Found build.yaml!

-> fileType: json -> baseLocale: en -> fallbackStrategy: baseLocaleEmptyString -> inputDirectory: lib/translations -> inputFilePattern: .i18n.json -> outputDirectory: lib/translations -> outputFileName: strings.g.dart -> outputFileFormat: singleFile -> localeHandling: true -> flutterIntegration: true -> namespaces: false -> translateVar: t -> enumName: AppLocale -> translationClassVisibility: private -> keyCase: null (no change) -> keyCase (for maps): null (no change) -> paramCase: null (no change) -> stringInterpolation: braces -> renderFlatMap: true -> translationOverrides: false -> renderTimestamp: true -> renderStatistics: true -> maps: [] -> pluralization/auto: cardinal -> pluralization/default_parameter: n -> pluralization/cardinal: [] -> pluralization/ordinal: [] -> contexts:

Scanning translations...

(base) en -> lib/translations/strings_en.i18n.json ro -> lib/translations/strings_ro.i18n.json tr -> lib/translations/strings_tr.i18n.json Unhandled exception: Null check operator used on a null value

0 _digestContextEntries (package:slang/builder/builder/translation_model_builder.dart:855:62)

1 _parseMapNode. (package:slang/builder/builder/translation_model_builder.dart:416:31)

2 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)

3 _parseMapNode (package:slang/builder/builder/translation_model_builder.dart:236:8)

4 _parseMapNode. (package:slang/builder/builder/translation_model_builder.dart:320:20)

5 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)

6 _parseMapNode (package:slang/builder/builder/translation_model_builder.dart:236:8)

7 TranslationModelBuilder.build (package:slang/builder/builder/translation_model_builder.dart:78:28)

8 TranslationModelListBuilder.build. (package:slang/builder/builder/translation_model_list_builder.dart:47:48)

9 MappedIterator.moveNext (dart:_internal/iterable.dart:403:20)

10 new _GrowableList._ofOther (dart:core-patch/growable_array.dart:202:26)

11 new _GrowableList.of (dart:core-patch/growable_array.dart:152:26)

12 new List.of (dart:core-patch/array_patch.dart:39:18)

13 Iterable.toList (dart:core/iterable.dart:498:7)

14 TranslationModelListBuilder.build (package:slang/builder/builder/translation_model_list_builder.dart:62:8)

15 GeneratorFacade.generate (package:slang/src/builder/generator_facade.dart:19:62)

16 generateTranslations (file:///home/talha/.pub-cache/hosted/pub.dev/slang-3.31.2/bin/slang.dart:318:34)

#17 main (file:///home/talha/.pub-cache/hosted/pub.dev/slang-3.31.2/bin/slang.dart:142:7) ``` **Expected behavior** Generator should generate translations without any error. **Additional context** Works fine when `fallback_strategy` set to `none` Before upgrading the package to latest version, i had the following versions and it was working fine: ``` slang: ^3.28.0 slang_flutter: ^3.23.0 build_runner: ^2.4.6 slang_build_runner: ^3.23.0 ``` In past, we opened this issue: [issue](https://github.com/slang-i18n/slang/issues/182) We saw your comment saying that the said the problem was solved, so we decided to update slang to latest version and encountered this problem. And at that time, we created the following script which solved our problem: ```python import json import os import shutil import subprocess def read_translation_data(lang, translation_files_path): file_path = os.path.join(translation_files_path, f"strings_{lang}.i18n.json") with open(file_path, 'r', encoding='utf-8') as translation_file: return json.load(translation_file) def write_translation_data(lang, translation_data, translation_files_path): file_path = os.path.join(translation_files_path, f"strings_{lang}.i18n.json") with open(file_path, 'w', encoding='utf-8') as translation_file: json.dump(translation_data, translation_file, ensure_ascii=False, indent=2, sort_keys=True) def update_translations(reference_lang, translations_path): reference_data = read_translation_data(reference_lang, translations_path) for lang in ['tr', 'ro']: translation_data = read_translation_data(lang, translations_path) add_missing_keys(lang, reference_data, translation_data) create_temp_file(lang, translations_path) fill_empty_keys_with_english(reference_data, translation_data) with open(os.path.join(translations_path, f"strings_{lang}.i18n.json"), 'w', encoding='utf-8') as updated_translation_file: json.dump(translation_data, updated_translation_file, ensure_ascii=False, indent=2, sort_keys=True) def create_temp_file(lang, translations_path): temp_translation_file_path = os.path.join(translations_path, f"temp_strings_{lang}.i18n.json") shutil.copy(os.path.join(translations_path, f"strings_{lang}.i18n.json"), temp_translation_file_path) def add_missing_keys(lang, reference, translation): for key, value in reference.items(): if isinstance(value, dict): if key not in translation: translation[key] = {} add_missing_keys(lang, value, translation[key]) else: if key not in translation: translation[key] = "" write_translation_data(lang, translation, translations_path) def fill_empty_keys_with_english(reference, translation): for key, value in reference.items(): if isinstance(value, dict): fill_empty_keys_with_english(value, translation[key]) elif key in translation and not translation[key]: translation[key] = value def sort_translation_data(translation_data): for key, value in translation_data.items(): if isinstance(value, dict): translation_data[key] = sort_translation_data(value) return dict(sorted(translation_data.items())) def remove_unused_keys_recursive(reference, translation): for key in list(translation.keys()): if key not in reference: del translation[key] elif isinstance(reference[key], dict) and isinstance(translation[key], dict): remove_unused_keys_recursive(reference[key], translation[key]) def remove_unused_keys(reference_lang, translations_path): reference_data = read_translation_data(reference_lang, translations_path) for lang in ['tr', 'ro']: translation_data = read_translation_data(lang, translations_path) remove_unused_keys_recursive(reference_data, translation_data) write_translation_data(lang, translation_data, translations_path) if __name__ == "__main__": reference_language = "en" translations_path = "lib/translations" print("\n###############################################\n") print("Translations is starting to update...") print("Checking for missing and empty values") update_translations(reference_language, translations_path) # Run the dart command after processing all language files print("json files are ready to generate dart files!") print("Running dart command...") subprocess.run([ "dart", "run", "slang"]) enPath = "strings_en.i18n.json" trPath = "strings_tr.i18n.json" roPath = "strings_ro.i18n.json" # Değişiklik yapılacak dosyaları belirle files_to_change = [trPath, roPath] # Dosyaları değiştir for lang_file in files_to_change: lang = lang_file.split('_')[1].split('.')[0] # Dosya adından dil bilgisini çıkar temp_file_path = os.path.join(translations_path, f"temp_{lang_file}") original_file_path = os.path.join(translations_path, lang_file) shutil.move(temp_file_path, original_file_path) # İngilizce haricindeki dillerin kullanılmayan key'leri sil remove_unused_keys(reference_language, translations_path) # En dosyasını sırala ve güncelle en_data = read_translation_data("en", translations_path) en_data_sorted = sort_translation_data(en_data) write_translation_data("en", en_data_sorted, translations_path) print("JSON files updated successfully.") print("SUCCESS!") print("\n###############################################\n") ``` What changes have been made and what/how should we implement it? I would be happy if you can navigate me to find out whats the problem.