Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot generate translations when fallback_strategy set to base_locale_empty_string #247

Open
talhakerpicci opened this issue Sep 26, 2024 · 0 comments
Labels
bug Something isn't working

Comments

@talhakerpicci
Copy link

talhakerpicci commented Sep 26, 2024

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
  1. Create translations folder: mkdir lib/translations
  2. Create translation files:
touch lib/translations/strings_en.i18n.json
touch lib/translations/strings_tr.i18n.json
touch lib/translations/strings_ro.i18n.json
  1. 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,
}
  1. Create build.yaml file: touch build.yaml
  2. 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:
            - 'package:slang_test/task_type_enum.dart'

          contexts:
            TaskType:
              generate_enum: false

      freezed:
        options:
          maybe_when: false
          maybe_map: false
  1. Run the generator: dart run slang
  2. The following output is shown:
Generating translations...

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: 
    - TaskType { (inferred) }
 -> interfaces: no interfaces
 -> obfuscation: disabled
 -> imports: [package:slang_test/task_type_enum.dart]

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.<anonymous closure> (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.<anonymous closure> (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.<anonymous closure> (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)
<asynchronous suspension>
#17     main (file:///home/talha/.pub-cache/hosted/pub.dev/slang-3.31.2/bin/slang.dart:142:7)
<asynchronous suspension>

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

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:

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant