From 09f94b6f876b5980e685094cb814ff96e3370bea Mon Sep 17 00:00:00 2001 From: JackTriton <35764777+JackTriton@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:01:01 +0900 Subject: [PATCH 1/3] Fallback to English --- Language.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/Language.py b/Language.py index 6043cc337..5456f64b5 100644 --- a/Language.py +++ b/Language.py @@ -10,6 +10,7 @@ import os import re import unicodedata +from copy import deepcopy from functools import reduce from typing import TypedDict @@ -251,20 +252,78 @@ def visit_Call(self, node: ast.Call): class Language: - def __init__(self, lang: str): + FALLBACK_LANG = "English" + + @staticmethod + def _load_property_file(lang: str) -> dict: with open(os.path.join(lang_path(lang), "property.json"), mode="r", encoding="utf-8") as f: - message = json.load(f) + return json.load(f) + + @classmethod + def _merge_with_fallback(cls, fallback, override): + """Return fallback recursively overlaid by override. + + Missing dict keys and missing list entries are kept from the fallback + language. Existing override values, including empty strings and null, + are treated as intentional translations. + """ + if isinstance(fallback, dict) and isinstance(override, dict): + merged = {k: deepcopy(v) for k, v in fallback.items()} + for key, value in override.items(): + if key in merged: + merged[key] = cls._merge_with_fallback(merged[key], value) + else: + merged[key] = deepcopy(value) + return merged + + if isinstance(fallback, list) and isinstance(override, list): + merged = [] + for index in range(max(len(fallback), len(override))): + if index < len(fallback) and index < len(override): + merged.append(cls._merge_with_fallback(fallback[index], override[index])) + elif index < len(override): + merged.append(deepcopy(override[index])) + else: + merged.append(deepcopy(fallback[index])) + return merged + + return deepcopy(override) + + @classmethod + def _load_property_with_fallback(cls, lang: str) -> dict: + if lang == cls.FALLBACK_LANG: + return cls._load_property_file(lang) + + fallback_message = cls._load_property_file(cls.FALLBACK_LANG) + + try: + message = cls._load_property_file(lang) + except FileNotFoundError: + return fallback_message + + return cls._merge_with_fallback(fallback_message, message) + + @staticmethod + def _collect_language_data(lang: str, extensions: tuple[str, ...]) -> dict[str, str]: + path = lang_path(lang) + if not os.path.isdir(path): + return {} + return { + fname: os.path.join(path, fname) + for fname in os.listdir(path) + if fname.lower().endswith(extensions) + } + + def __init__(self, lang: str): + message = self._load_property_with_fallback(lang) self.__dict__.update(message) self.base = self.lang_property["base"] extensions = (".bin", ".ia4", ".zobj") self.path = lang_path(lang) - self.data = { - fname: os.path.join(lang_path(lang), fname) - for fname in os.listdir(lang_path(lang)) - if fname.lower().endswith(extensions) - } + self.data = self._collect_language_data(self.FALLBACK_LANG, extensions) + self.data.update(self._collect_language_data(lang, extensions)) def _dict_get(self, obj, key): if isinstance(obj, (list, tuple)): From 41a230c5b0b0676fd246e0efe5b6461b18f36aa9 Mon Sep 17 00:00:00 2001 From: JackTriton <35764777+JackTriton@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:08:39 +0900 Subject: [PATCH 2/3] Fallback while maintaining lang.data --- Language.py | 27 +++++++++++++++++++++++++-- Patches.py | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Language.py b/Language.py index 5456f64b5..289d2716e 100644 --- a/Language.py +++ b/Language.py @@ -321,9 +321,32 @@ def __init__(self, lang: str): self.base = self.lang_property["base"] extensions = (".bin", ".ia4", ".zobj") + self.lang = lang self.path = lang_path(lang) - self.data = self._collect_language_data(self.FALLBACK_LANG, extensions) - self.data.update(self._collect_language_data(lang, extensions)) + # Do not globally fall back binary/texture/object data files. + # Register only files that actually exist in the selected language. + # Individual patch sites that need a fallback should resolve it explicitly. + self.data = self._collect_language_data(lang, extensions) + + def blue_fire_arrow_item_name_path(self) -> str: + """Return the Blue Fire Arrow item-name texture for this language base. + + This is intentionally the only lang.data-style asset fallback. + Use the selected language's matching-base file when present; otherwise + fall back to Japanese for jp-base languages and English for all others. + """ + if self.base == "jp": + filename = "blue_fire_arrow_item_name_jap.ia4" + fallback_lang = "Japanese" + else: + filename = "blue_fire_arrow_item_name_eng.ia4" + fallback_lang = self.FALLBACK_LANG + + local_file = self.data.get(filename) + if local_file is not None and os.path.isfile(local_file): + return local_file + + return os.path.join(lang_path(fallback_lang), filename) def _dict_get(self, obj, key): if isinstance(obj, (list, tuple)): diff --git a/Patches.py b/Patches.py index d59100fbf..00d0b3bf7 100644 --- a/Patches.py +++ b/Patches.py @@ -1877,7 +1877,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p bfa_message = make_player_message(bfa_message, lang) update_message_by_id(messages, 0x0071, bfa_message, lang, 0x23, allow_duplicates=True) - with open(lang.data['blue_fire_arrow_item_name_jap.ia4' if lang.base == "jp" else 'blue_fire_arrow_item_name_eng.ia4'], 'rb') as stream: + with open(lang.blue_fire_arrow_item_name_path(), 'rb') as stream: bfa_name_bytes = stream.read() rom.write_bytes(0x883000 if lang.base == "jp" else 0x8A1C00, bfa_name_bytes) From 8a45ee464188dc180181ff89d0e1a1503c438074 Mon Sep 17 00:00:00 2001 From: JackTriton <35764777+JackTriton@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:06:15 +0900 Subject: [PATCH 3/3] List issue resolved --- Language.py | 62 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/Language.py b/Language.py index 289d2716e..8ff08e965 100644 --- a/Language.py +++ b/Language.py @@ -259,13 +259,57 @@ def _load_property_file(lang: str) -> dict: with open(os.path.join(lang_path(lang), "property.json"), mode="r", encoding="utf-8") as f: return json.load(f) + @staticmethod + def _is_id_list(value) -> bool: + """Return True for property lists whose entries can be merged by id. + + Ordinary translation-choice lists must never be padded from the fallback + language, because that can mix English options into another language. + Lists whose entries are dictionaries with an ``id`` field are treated as + keyed records instead: existing ids are overlaid and missing ids are + filled from the fallback language. + """ + return ( + isinstance(value, list) + and len(value) > 0 + and all(isinstance(item, dict) and "id" in item for item in value) + ) + + @classmethod + def _merge_id_list_with_fallback(cls, fallback: list, override: list) -> list: + """Merge a list of ``{"id": ...}`` dictionaries by id. + + The selected language controls the visible order for translated entries. + Fallback-only records are appended afterward in fallback order. + """ + fallback_by_id = {item["id"]: item for item in fallback} + override_ids = set() + merged = [] + + for item in override: + item_id = item["id"] + override_ids.add(item_id) + if item_id in fallback_by_id: + merged.append(cls._merge_with_fallback(fallback_by_id[item_id], item)) + else: + merged.append(deepcopy(item)) + + for item in fallback: + if item["id"] not in override_ids: + merged.append(deepcopy(item)) + + return merged + @classmethod def _merge_with_fallback(cls, fallback, override): """Return fallback recursively overlaid by override. - Missing dict keys and missing list entries are kept from the fallback - language. Existing override values, including empty strings and null, - are treated as intentional translations. + Dicts are merged by key. Lists are normally replaced wholesale by the + selected language to prevent fallback-language strings from being mixed + into translated choice lists. The one exception is a list of dictionaries + with an ``id`` field, which is merged by id so missing records can still + be filled from the fallback language. Existing override values, + including empty strings and null, are treated as intentional translations. """ if isinstance(fallback, dict) and isinstance(override, dict): merged = {k: deepcopy(v) for k, v in fallback.items()} @@ -277,15 +321,9 @@ def _merge_with_fallback(cls, fallback, override): return merged if isinstance(fallback, list) and isinstance(override, list): - merged = [] - for index in range(max(len(fallback), len(override))): - if index < len(fallback) and index < len(override): - merged.append(cls._merge_with_fallback(fallback[index], override[index])) - elif index < len(override): - merged.append(deepcopy(override[index])) - else: - merged.append(deepcopy(fallback[index])) - return merged + if cls._is_id_list(fallback) and (cls._is_id_list(override) or len(override) == 0): + return cls._merge_id_list_with_fallback(fallback, override) + return deepcopy(override) return deepcopy(override)