diff --git a/coilsnake/lang/en.json b/coilsnake/lang/en.json new file mode 100644 index 0000000..0483ffc --- /dev/null +++ b/coilsnake/lang/en.json @@ -0,0 +1,158 @@ +{ + "coilsnake_name": "CoilSnake ", + "enable_debug_mode": "Enable Debug Mode", + "disable_debug_mode": "Disable Debug Mode", + "ask_disable_debug": "Disable Debug Mode?", + "ask_enable_debug": "Enable Debug Mode?", + "ask_disable_debug_prompt": "Would you like to disable Debug mode?", + "ask_enable_debug_prompt": "Would you like to enable Debug mode? Debug mode will provide you with more detailed output while ", + "coilsnake_is_running": "CoilSnake is running.", + "advanced_users": "This is generally only needed by advanced users.", + "select_emu_exe": "Select the Emulator Executable", + "select_an_emu": "Select an Emulator Executable", + "coilsnake_use_emu": "Select an emulator executable for CoilSnake to use.\n\n", + "emu_hint": "Hint: It is probably named either mesen.exe, snes9x.exe, or higan-accuracy.exe", + "ccscript_offset": "Input CCScript Offset", + "which_ccscript_compile": "Specify the hexidecimal offset to which CCScript should compile text.\n", + "default_F10000": "(The default value is F10000)\n\n", + "know_what_youre_doing": "You should leave this setting alone unless you really know what you are doing.", + "error": "Error", + "cant_find_emu": "CoilSnake could not find an emulator. Please configure your emulator in the Settings menu.", + "cant_find_java": "CoilSnake could not find Java. Please configure Java in the Settings menu.", + "cant_patch_rom": "Could not patch ROM: Invalid patch format. Please end patchfile with either .ebp or .ips.", + "invalid_format": "Could not create patch because patch does not end in .ips or .ebp", + "name_exists": "A profile with that name already exists.", + "cant_delete_prof": "Cannot delete the only profile.", + "not_a_valid_hex": "{} is not a valid hexidecimal number.", + "config_java": "Configure Java", + "java_following_loc": "CoilSnake has detected Java at the following location:\n\n", + "select_yes": "To use this installation of Java, select \"Yes\".\n\n", + "select_no": "To override and instead use a different version of Java, select \"No\".", + "select_the_java_exe": "Select the Java Executable", + "java_for_coilsnake": "Select a Java executable for CoilSnake to use.\n\n", + "on_windows_info": "On Windows, it might be called \"javaw.exe\" or \"java.exe\".", + "are_you_sure": "Are You Sure?", + "ask_upgrade": "Are you sure you would like to upgrade this project? This operation", + "ask_upgrade_2": "cannot be undone.\n\n", + "backup_info": "It is recommended that you backup your project before proceeding.", + "ask_perm_overwrite": "Are you sure you would like to permanently overwrite the ", + "ask_perm_overwrite_2": "contents of the selected output directory?", + "ask_expand_rom": "Expand Your Base ROM?", + "attempt_compile": "You are attempting to compile using a base ROM which is ", + "attempt_compile_2": "unexpanded. It is likely that this will not succeed, as CoilSnake ", + "attempt_compile_3": "needs the extra space in an expanded ROM to store additional data.", + "ask_expand_base": "Would you like to expand this base ROM before proceeding? This ", + "ask_expand_base_2": "will permanently overwrite your base ROM.", + "start_comp": "Starting compilation…", + "decomp_script_prompt": "Are you sure you would like to decompile the script into this ", + "decomp_script_prompt_2": "project? This operation cannot be undone.\n\n", + "decompile_text": "Decompile", + "compile_text": "Compile", + "upgrade": "Upgrade", + "decomp_script": "Decompile Script", + "apply_patch": "Apply Patch", + "create_patch": "Create Patch", + "about_coilsnake": "About CoilSnake", + "eb_proj_edit": "EB Project Editor", + "expand_to_32": "Expand your ROM to 32 MBit", + "expand_to_48": "Expand your ROM to 48 MBit", + "add_header": "Add Header to ROM", + "remove_header": "Remove Header from ROM", + "tools": "Tools", + "config_emu": "Configure Emulator", + "config_ccscript": "Configure CCScript", + "settings": "Settings", + "coilsnake_site": "CoilSnake Website", + "help_text": "Help", + "decomp_rom_new_proj": "Decompile a ROM to create a new project.", + "comp_rom_new_proj": "Compile a project to create a new ROM.", + "rom": "ROM", + "output_dir": "Output Directory", + "base_rom": "Base ROM", + "project": "Project", + "output_rom": "Output ROM", + "upgrade_info": "Upgrade a project created using an older version of CoilSnake.", + "clean_rom": "Clean ROM", + "decomp_rom_script": "Decompile a ROM's script to an already existing project.", + "apply_patch_info": "Apply an EBP or IPS patch to a ROM", + "patched_rom": "Patched ROM", + "patch_rom": "Patch ROM", + "patch": "Patch", + "header_ips_only": "ROM Header (IPS only)", + "create_ebp_info": "Create EBP patch from a ROM", + "modded_rom": "Modified ROM", + "author": "Author", + "desc": "Description", + "title": "Title", + "ebp_patch": "EBP Patch", + "input_ebp": "Input EBP Patch Info", + "ok": "OK", + "profile": "Profile:", + "new_prof_name": "New Profile Name", + "specify_name": "Specify the name of the new profile.", + "save": "Save", + "delete": "Delete", + "new": "New", + "browse": "Browse…", + "run": "Run", + "open_text": "Open", + "edit": "Edit", + "yes": "Yes", + "no": "No", + "language_setting": "Change Language...", + "console_proj_already_updated": "Project is already up to date.", + "console_upgrading_version": "Upgrading project from version {old} to {new}", + "console_upgrading": "Upgrading {}...", + "console_finished_upgrading": "Finished upgrading {} in {:.2f}s", + "console_upgrading_in": "Upgraded {} in {:.2f}s", + "console_compiling_ccs": "Compiling CCScript", + "console_finished_ccs": "Finished compiling CCScript", + "console_error_outout": "CCScript compilation failed with output:\n", + "console_comp_proj": "Compiling Project {}", + "console_compiling": "Compiling {}...", + "console_finished_comp": "Finished compiling {} in {:.2f}s", + "console_saving_rom": "Saving ROM", + "console_comp_to_finish": "Compiled to {} in {:.2f}s, finished at {}", + "console_decomp_rom": "Decompiling ROM {rom}", + "console_decompiling": "Decompiling {}...", + "console_finish_decomp": "Finished decompiling {class_name} in {duration:.2f}s", + "console_saving_proj": "Saving Project", + "console_decomp_to": "Decompiled to {} in {:.2f}s", + "console_error_decomp_script": "Cannot decompile script of a non-Earthbound rom. A {} rom was supplied.", + "console_decomp_script": "Decompiled script to {} in {:.2f}s", + "console_patching_rom": "Patching ROM {} with patch {}", + "console_error_unknown_patch": "Unknown patch format.", + "console_title_author": "Patch: {title} by {author}", + "console_patched_to": "Patched to {} in {:.2f}s", + "console_creating_ebp": "Creating EBP patch by ", + "console_with_desc": " with description \"", + "console_called": "\" called ", + "console_creating_ips": "Creating IPS patch...", + "console_error_creating_patch": "There was an error creating the patch: ", + "console_patch_success": "The patch {} was successfully created in {:.2f}s.", + "console_error_compatibility": "This project is not compatible with this version of CoilSnake. Please use this project", + "console_error_compatibility_2": " with a newer version of CoilSnake.", + "console_error_upgrade_before_op": "This project must be upgraded before performing this operation.", + "console_error_rom_type": "Rom type {} does not match Project type {}", + "console_warning_music_echo": "Patching music engine to avoid sample corruption due to echo.", + "console_error_music_end_bank": "Music pack did not end before bank boundary.", + "console_error_brr_missing_term": "BRR data at ARAM address ${:04X} is missing a terminator", + "console_error_config_inst_def": "Error at line {} in config.txt: expected instrument definition", + "console_error_config_line": "Error at line {} in config.txt: expecting '{{'", + "console_warning_tileset_reference": "Tileset from background frame {} does not match reference.", + "console_read_pointer_tbls": "Reading pointer tables", + "console_read_tileset": "Reading tileset #{}", + "console_read_pal": "Reading palettes", + "console_write_collision": "Writing collisions", + "console_write_pal": "Writing palettes", + "console_write_tileset": "Writing tileset #{}", + "console_write_pal_settings": "Writing palette settings", + "console_write_tileset_number": "Writing tileset #{}", + "console_read_tileset_number": "Reading tileset #{}", + "console_read_pal_settings": "Reading palette settings", + "console_yml_game_unset": "YAML table load function called with game unset - falling back to last loaded (%s)", + "console_attempt_game_table": "Attempted to create a table for a game without a YAML file.", + "console_relocate_song_to_address": "Relocating song $%02X to address $%04X" + + } + diff --git a/coilsnake/lang/ja.json b/coilsnake/lang/ja.json new file mode 100644 index 0000000..dbb0644 --- /dev/null +++ b/coilsnake/lang/ja.json @@ -0,0 +1,156 @@ +{ + "coilsnake_name": "まきへびドライ ", + "enable_debug_mode": "デバッグモードを有効にする", + "disable_debug_mode": "デバッグモードを無効にする", + "ask_disable_debug": "デバッグモードを無効にしますか?", + "ask_enable_debug": "デバッグモードを有効にしますか?", + "ask_disable_debug_prompt": "デバッグモードを無効にしますか?", + "ask_enable_debug_prompt": "デバッグモードを有効にしますか?デバッグモードでは、", + "coilsnake_is_running": "まきへびドライのより詳細な出力を確認できます。", + "advanced_users": "こちらは通常、上級ユーザーのみが必要とするものです。", + "select_emu_exe": "エミュレータの.exeファイルを選択", + "select_an_emu": "エミュレータの.exeファイルを選択", + "coilsnake_use_emu": "まきへびドライで使用するエミュレータの.exeファイルを選択します。\n\n", + "emu_hint": "ヒント:おそらく、mesen.exe、snes9x.exe、またはhigan-accuracy.exeのいずれかを使います。", + "ccscript_offset": "CCScriptオフセットの入力", + "which_ccscript_compile": "CCScriptがテキストをコンパイルする16進オフセットを指定します。\n", + "default_F10000": "(デフォルト値はF10000です)\n\n", + "know_what_youre_doing": "この設定は、一部の上級者以外、変更しないでください。", + "error": "エラー", + "cant_find_emu": "まきへびドライがエミュレータを見つけられませんでした。設定メニューでエミュレータを設定してください。", + "cant_find_java": "まきへびドライがJavaを見つけられませんでした。設定メニューでJavaを設定してください。", + "cant_patch_rom": "ROMのパッチ処理に失敗しました。無効なパッチ形式です。パッチファイルの拡張子を.ebpまたは.ipsにしてください。", + "invalid_format": "パッチが.ipsまたは.ebpではありません。パッチを作成できませんでした", + "name_exists": "その名前のプロフィールはすでに存在します。", + "cant_delete_prof": "唯一のプロフィールを削除できません。", + "not_a_valid_hex": "{}は有効な16進数ではありません。", + "config_java": "Javaの設定", + "java_following_loc": "まきへびドライは次の場所にJavaを検出しました:\n\n", + "select_yes": "このJavaを使用するには、\"「はい」\"を選択します。\n\n", + "select_no": "別のバージョンのJavaを使用するには、\"「いいえ」\"を選択してください。", + "select_the_java_exe": "Java.exe ファイルの選択", + "java_for_coilsnake": "まきへびドライが使用するJava.exeファイルを選択します。\n\n", + "on_windows_info": "Windowsでは、\"「javaw.exe」\"または\"「java.exe」\"と表記される場合があります。", + "are_you_sure": "よろしいですか?", + "ask_upgrade": "このプロジェクトをアップグレードしてもよろしいですか?", + "ask_upgrade_2": "一度アップグレードしたら元に戻せません。\n\n", + "backup_info": "作業を続行する前にプロジェクトのバックアップを作成することをおススメします。", + "ask_perm_overwrite": "選択した出力ディレクトリのコンテンツを上書きしてもよろしいですか?", + "ask_perm_overwrite_2": " ", + "ask_expand_rom": "ベースROMを展開しますか?", + "attempt_compile": "展開されていないベースROMを使用してコンパイルを試みています。", + "attempt_compile_2": "まきへびドライは追加データを保存するために展開されたROMの追加容量を必要とするため、", + "attempt_compile_3": "この操作は成功しない可能性が高いです。", + "ask_expand_base": "処理を続行する前に、このベースROMを展開しますか?", + "ask_expand_base_2": "ベースROMが上書きされます。", + "start_comp": "コンパイルを開始します…", + "decomp_script_prompt": "このプロジェクトにスクリプトをデコンパイルしますか?", + "decomp_script_prompt_2": "一度行った操作は元に戻せません。\n\n", + "decompile_text": "デコンパイル", + "compile_text": "コンパイル", + "upgrade": "アップグレード", + "decomp_script": "スクリプトをデコンパイル", + "apply_patch": "パッチの適用", + "create_patch": "パッチの作成", + "about_coilsnake": "まきへびドライについて", + "eb_proj_edit": "M2プロジェクトエディター", + "expand_to_32": "ROMを32 Mbitに拡張", + "expand_to_48": "ROMを48 Mbitに拡張", + "add_header": "ROMにヘッダーを追加", + "remove_header": "ROMからヘッダーを削除", + "tools": "ツール", + "config_emu": "エミュレーターの設定", + "config_ccscript": "CCScriptの設定", + "settings": "設定", + "coilsnake_site": "まきへびドライのウェブサイト", + "help_text": "ヘルプ", + "decomp_rom_new_proj": "ROMをデコンパイルして新しいプロジェクトを作成します。", + "comp_rom_new_proj": "プロジェクトをコンパイルして新しいROMを作成します。", + "rom": "ROM", + "output_dir": "出力ディレクトリ", + "base_rom": "ベースROM", + "project": "プロジェクト", + "output_rom": "出力ROM", + "upgrade_info": "まきへびドライの旧バージョンで作成したプロジェクトをアップグレードします。", + "clean_rom": "ROMをクリーンアップ", + "decomp_rom_script": "ROMのスクリプトを既存のプロジェクトにデコンパイルします。", + "apply_patch_info": "ROMにEBPまたはIPSパッチを適用します。", + "patched_rom": "パッチROM", + "patch_rom": "ROMをパッチ", + "patch": "パッチ", + "header_ips_only": "ROMヘッダー(IPSのみ)", + "create_ebp_info": "ROMからEBPパッチを作成", + "modded_rom": "パッチ済みROM", + "author": "作成者", + "desc": "説明", + "title": "タイトル", + "ebp_patch": "EBPパッチ", + "input_ebp": "EBPパッチ情報を入力", + "ok": "OK", + "profile": "プロフィール:", + "new_prof_name": "新しいプロフィール名", + "specify_name": "新しいプロフィール名を指定してください。", + "save": "保存", + "delete": "削除", + "new": "新規", + "browse": "参照…", + "run": "実行", + "open_text": "開く", + "edit": "編集", + "yes": "はい ", + "no": "いいえ", + "language_setting": "言語...", + "console_proj_already_updated": "プロジェクトは既に最新の状態です。", + "console_upgrading_version": "プロジェクトをバージョン{old}から{new}にアップグレードしています", + "console_upgrading": "{}をアップグレード中...", + "console_finished_upgrading": "{}のアップグレードが完了しました({:.2f}秒)", + "console_upgrading_in": "{}のアップグレードが完了しました({:.2f}秒)", + "console_compiling_ccs": "CCScriptをコンパイル中", + "console_finished_ccs": "CCScriptのコンパイルが完了しました", + "console_error_outout": "CCScriptのコンパイルに失敗しました。出力内容:\n", + "console_comp_proj": "プロジェクト{}をコンパイル中", + "console_compiling": "{}をコンパイル中...", + "console_finished_comp": "{}のコンパイルが完了しました({:.2f}秒)", + "console_saving_rom": "ROMを保存中", + "console_comp_to_finish": "{}にコンパイル完了({:.2f}秒)、終了時刻: {}", + "console_decomp_rom": "ROM {rom}をデコンパイル中", + "console_decompiling": "{}をデコンパイル中...", + "console_finish_decomp": "{class_name}のデコンパイルが完了しました({duration:.2f}秒)", + "console_saving_proj": "プロジェクトを保存中", + "console_decomp_to": "{}にデコンパイル完了({:.2f}秒)", + "console_error_decomp_script": "非『MOTHER2』ROMのスクリプトをデコンパイルすることはできません。{} ROMが指定されました。", + "console_decomp_script": "スクリプトを{}にデコンパイルしました({:.2f}秒)", + "console_patching_rom": "ROM {}にパッチ{}を適用中", + "console_error_unknown_patch": "不明なパッチ形式です。", + "console_title_author": "パッチ: {title} 作成者: {author}", + "console_patched_to": "{}にパッチ適用完了({:.2f}秒)", + "console_creating_ebp": "EBPパッチを作成中", + "console_with_desc": " 説明付き \"", + "console_called": "\" っていう ", + "console_creating_ips": "IPSパッチを作成中...", + "console_error_creating_patch": "パッチ作成中にエラーが発生しました: ", + "console_patch_success": "パッチ{}が正常に作成されました({:.2f}秒)。", + "console_error_compatibility": "このプロジェクトはこのバージョンのまきへびドライとは互換性がありません。新しいバージョンのまきへびドライを使用してください。", + "console_error_compatibility_2": "このプロジェクトには新しいバージョンのまきへびドライが必要です。", + "console_error_upgrade_before_op": "この操作を実行する前にプロジェクトをアップグレードする必要があります。", + "console_error_rom_type": "ROMタイプ{}はプロジェクトタイプ{}と一致しません", + "console_warning_music_echo": "エコーによるサンプル破損を防ぐために音楽エンジンを修正中。", + "console_error_music_end_bank": "ミュージックパックがバンクの境界前に終了しませんでした。", + "console_error_brr_missing_term": "ARAM アドレス ${:04X} の BRR データに終端子がありません。", + "console_error_config_inst_def": "config.txt の {} 行目でエラー: 楽器の定義が必要です。", + "console_error_config_line": "config.txt の {} 行目でエラー: '{{' が必要です。", + "console_warning_tileset_reference_1": "背景フレーム {} のタイルセットが参照と一致しません。", + "console_read_pointer_tbls": "ポインタテーブルの読み取り", + "console_read_tileset": "タイルセット #{} を読み取り中", + "console_read_pal": "パレットを読み取り中", + "console_write_collision": "衝突判定を書き込み中", + "console_write_pal": "パレットを書き込み中", + "console_write_tileset": "タイルセット #{} を書き込み中", + "console_write_pal_settings": "パレット設定を書き込み中", + "console_write_tileset_number": "タイルセット #{} を書き込み中", + "console_read_tileset_number": "タイルセット #{} を読み取り中", + "console_read_pal_settings": "パレット設定を読み取り中", + "console_yml_game_unset": "ゲームが設定されていない状態でYAMLテーブルのロード関数が呼び出されました - 最後にロードされたもの (%s) にフォールバックします", + "console_attempt_game_table": "YAMLファイルなしでゲームのためにテーブルを作成しようとしました。", + "console_relocate_song_to_address": "曲 $%02X をアドレス $%04X に再配置中" + } \ No newline at end of file diff --git a/coilsnake/model/common/blocks.py b/coilsnake/model/common/blocks.py index 805582d..c53df2f 100644 --- a/coilsnake/model/common/blocks.py +++ b/coilsnake/model/common/blocks.py @@ -262,7 +262,11 @@ def is_allocated(self, range): def deallocate(self, range): check_range_validity(range, self.size) - # TODO do some check so that unallocated ranges don't overlap + cur_begin, cur_end = range + for prev_begin, prev_end in self.unallocated_ranges: + if prev_begin <= cur_end and cur_begin <= prev_end: + raise InvalidArgumentError(f"Couldn't mark range ({cur_begin:#x}, {cur_end:#x}) as free " + f"because it overlaps with ({prev_begin:#x}, {prev_end:#x})") # TODO attach contiguous unallocated ranges if possible self.unallocated_ranges.append(range) @@ -321,9 +325,12 @@ def get_largest_unallocated_range(self): ROM_TYPE_NAME_UNKNOWN = "Unknown" ROM_TYPE_NAME_EARTHBOUND = "Earthbound" +ROM_TYPE_NAME_MOTHER2 = "Mother 2" ROM_TYPE_NAME_EARTHBOUND_ZERO = "Earthbound Zero" ROM_TYPE_NAME_SUPER_MARIO_BROS = "Super Mario Bros" +ROM_TYPE_GROUP_EBM2 = (ROM_TYPE_NAME_EARTHBOUND, ROM_TYPE_NAME_MOTHER2) + class Rom(AllocatableBlock): def reset(self, size=0): super(Rom, self).reset(size) @@ -398,7 +405,7 @@ def _detect_type(self): return ROM_TYPE_NAME_UNKNOWN def add_header(self): - if self.type == ROM_TYPE_NAME_EARTHBOUND: + if self.type in ROM_TYPE_GROUP_EBM2: for i in range(0x200): self.data.insert(0, 0) self.size += 0x200 @@ -406,7 +413,7 @@ def add_header(self): raise NotImplementedError("Don't know how to add header to ROM of type[%s]" % self.type) def expand(self, desired_size): - if self.type == ROM_TYPE_NAME_EARTHBOUND: + if self.type in ROM_TYPE_GROUP_EBM2: if (desired_size != 0x400000) and (desired_size != 0x600000): raise InvalidArgumentError("Cannot expand an %s ROM to size[%#x]" % (self.type, self.size)) else: @@ -422,5 +429,4 @@ def expand(self, desired_size): for i in range(0x8000, 0x8000 + 0x8000): self[0x400000 + i] = self[i] else: - raise NotImplementedError("Don't know how to expand ROM of type[%s]" % self.type) - + raise NotImplementedError("Don't know how to expand ROM of type[%s]" % self.type) \ No newline at end of file diff --git a/coilsnake/ui/gui.py b/coilsnake/ui/gui.py index 319c18b..229c5ef 100644 --- a/coilsnake/ui/gui.py +++ b/coilsnake/ui/gui.py @@ -1,6 +1,8 @@ #! /usr/bin/env python -import sys +from typing import Callable +import sys +import tkinter as tk import tkinter from functools import partial import logging @@ -19,7 +21,7 @@ import os from PIL import ImageTk -from coilsnake.model.common.blocks import Rom, ROM_TYPE_NAME_EARTHBOUND +from coilsnake.model.common.blocks import Rom, ROM_TYPE_GROUP_EBM2 from coilsnake.ui import information, gui_util from coilsnake.ui.common import decompile_rom, compile_project, upgrade_project, setup_logging, decompile_script, \ patch_rom, create_patch @@ -31,20 +33,80 @@ from coilsnake.util.common.project import PROJECT_FILENAME from coilsnake.util.common.assets import asset_path +from coilsnake.ui.language import TranslationStringManager, LANGUAGES, global_strings +# Set up logging log = logging.getLogger(__name__) +# Constants for button and label widths BUTTON_WIDTH = 15 LABEL_WIDTH = 20 +class GuiTranslationHelper: + def __init__(self, strings: TranslationStringManager): + self.strings = strings + + def change_language(self, language=None, language_name=None): + return self.strings.change_language(language, language_name) + + def get(self, string_name: str) -> str: + return self.strings.get(string_name) + + def register_callback(self, cb, invoke=True): + self.strings.register_callback(cb, invoke) + + def register_widget(self, elem: Widget, string_name: str, invoke=True): + cb = lambda: elem.configure(text=self.get(string_name)) + self.strings.register_callback(cb, invoke=invoke) + + def register_notebook_frame(self, notebook, frame, string_name, invoke=True): + cb = lambda: notebook.tab(frame, text=self.get(string_name)) + self.strings.register_callback(cb, invoke=invoke) + class CoilSnakeGui(object): def __init__(self): self.preferences = CoilSnakePreferences() self.preferences.load() self.components = [] self.progress_bar = None + self.guistrings = GuiTranslationHelper(global_strings) + + # Function to open the language selection window + def open_language_window(self): + # Create a new top-level window for language selection + language_window = tk.Toplevel(self.root) + language_window.title("Select Language") + language_window.geometry("250x200") + + # StringVar to store the selected language + selected_language = tk.StringVar(value="en") # Default to English - # Preferences functions + # Frame for language selection + language_frame = tk.LabelFrame(language_window, text="Select Language") + language_frame.pack(pady=10, padx=10, fill="both") + + for language in LANGUAGES: + tk.Radiobutton( + language_frame, + text=language.full_name, + variable=selected_language, + value=language.iso639_1_name + ).pack(anchor="w") + + # Function to apply the selected language + def apply_language(): + language = selected_language.get() + self.guistrings.change_language(language_name=language) + language_window.destroy() + + + # OK Button to confirm selection + ok_button = tk.Button(language_window, text="OK", command=apply_language) + ok_button.pack(pady=10) + + # Cancel Button to close without applying changes + cancel_button = tk.Button(language_window, text="Cancel", command=language_window.destroy) + cancel_button.pack(pady=5) def refresh_debug_logging(self): if self.preferences["debug mode"]: @@ -57,13 +119,13 @@ def refresh_debug_mode_command_label(self): self.pref_menu.entryconfig(5, label=self.get_debug_mode_command_label()) def get_debug_mode_command_label(self): - return 'Disable Debug Mode' if self.preferences["debug mode"] else 'Enable Debug Mode' + return self.guistrings.get("disable_debug_mode") if self.preferences["debug mode"] else self.guistrings.get("enable_debug_mode") def set_debug_mode(self): if self.preferences["debug mode"]: confirm = tkinter.messagebox.askquestion( - "Disable Debug Mode?", - "Would you like to disable Debug mode?", + self.guistrings.get("ask_disable_debug"), + self.guistrings.get("ask_disable_debug_prompt"), icon="question" ) @@ -71,10 +133,10 @@ def set_debug_mode(self): self.preferences["debug mode"] = False else: confirm = tkinter.messagebox.askquestion( - "Enable Debug Mode?", - "Would you like to enable Debug mode? Debug mode will provide you with more detailed output while " - + "CoilSnake is running.\n\n" - + "This is generally only needed by advanced users.", + self.guistrings.get("ask_enable_debug"), + self.guistrings.get("ask_enable_debug_prompt") + + self.guistrings.get("coilsnake_is_running") + + self.guistrings.get("advanced_users"), icon="question" ) @@ -88,25 +150,25 @@ def set_debug_mode(self): def set_emulator_exe(self): tkinter.messagebox.showinfo( - "Select the Emulator Executable", - "Select an emulator executable for CoilSnake to use.\n\n" - "Hint: It is probably named either zsnesw.exe, snes9x.exe, or higan-accuracy.exe" + self.guistrings.get("select_emu_exe"), + self.guistrings.get("coilsnake_use_emu"), + self.guistrings.get("emu_hint") ) emulator_exe = tkinter.filedialog.askopenfilename( parent=self.root, initialdir=os.path.expanduser("~"), - title="Select an Emulator Executable") + title=self.guistrings.get("select_an_emu")) if emulator_exe: self.preferences["emulator"] = emulator_exe self.preferences.save() def set_ccscript_offset(self): ccscript_offset_str = tkinter.simpledialog.askstring( - title="Input CCScript Offset", - prompt=("Specify the hexidecimal offset to which CCScript should compile text.\n" - + "(The default value is F10000)\n\n" - + "You should leave this setting alone unless if you really know what you are doing."), + title=self.guistrings.get("ccscript_offset"), + prompt=(self.guistrings.get("which_ccscript_compile") + + self.guistrings.get("default_F10000") + + self.guistrings.get("know_what_youre_doing")), initialvalue="{:x}".format(self.preferences.get_ccscript_offset()).upper()) if ccscript_offset_str: @@ -114,8 +176,8 @@ def set_ccscript_offset(self): ccscript_offset = int(ccscript_offset_str, 16) except: tkinter.messagebox.showerror(parent=self.root, - title="Error", - message="{} is not a valid hexidecimal number.".format(ccscript_offset_str)) + title=self.guistrings.get("error"), + message=self.guistrings.get("not_a_valid_hex").format(ccscript_offset_str)) return self.preferences.set_ccscript_offset(ccscript_offset) @@ -129,11 +191,11 @@ def set_java_exe(self): if system_java_exe: confirm = tkinter.messagebox.askquestion( - "Configure Java", - "CoilSnake has detected Java at the following location:\n\n" + self.guistrings.get("config_java"), + self.guistrings.get("java_following_loc") + system_java_exe + "\n\n" - + "To use this installation of Java, select \"Yes\".\n\n" - + "To override and instead use a different version of Java, select \"No\".", + + self.guistrings.get("select_yes") + + self.guistrings.get("select_yes"), icon="question" ) if confirm == "yes": @@ -142,14 +204,14 @@ def set_java_exe(self): return tkinter.messagebox.showinfo( - "Select the Java Executable", - "Select a Java executable for CoilSnake to use.\n\n" - "On Windows, it might be called \"javaw.exe\" or \"java.exe\"." + self.guistrings.get("select_the_java_exe"), + self.guistrings.get("java_for_coilsnake"), + self.guistrings.get("on_windows_info") ) java_exe = tkinter.filedialog.askopenfilename( parent=self.root, - title="Select the Java Executable", + title=self.guistrings.get("select_the_java_exe"), initialfile=(self.preferences["java"] or system_java_exe)) if java_exe: self.preferences["java"] = java_exe @@ -203,9 +265,8 @@ def run_rom(self, entry): rom_filename = entry.get() if not self.preferences["emulator"]: tkinter.messagebox.showerror(parent=self.root, - title="Error", - message="""CoilSnake could not find an emulator. -Please configure your emulator in the Settings menu.""") + title=self.guistrings.get("error"), + message=self.guistrings.get("cant_find_emu")) elif rom_filename: Popen([self.preferences["emulator"], rom_filename]) @@ -218,9 +279,8 @@ def open_ebprojedit(self, entry=None): java_exe = self.get_java_exe() if not java_exe: tkinter.messagebox.showerror(parent=self.root, - title="Error", - message="""CoilSnake could not find Java. -Please configure Java in the Settings menu.""") + title=self.guistrings.get("error"), + message=self.guistrings.get("cant_find_java")) return command = [java_exe, "-jar", asset_path(["bin", "EbProjEdit.jar"])] @@ -237,9 +297,9 @@ def do_decompile(self, rom_entry, project_entry): if rom and project: if os.path.isdir(project): - confirm = tkinter.messagebox.askquestion("Are You Sure?", - "Are you sure you would like to permanently overwrite the " - + "contents of the selected output directory?", + confirm = tkinter.messagebox.askquestion(self.guistrings.get("are_you_sure"), + self.guistrings.get("ask_perm_overwrite") + + self.guistrings.get("ask_perm_overwrite_2"), icon='warning') if confirm != "yes": return @@ -274,14 +334,14 @@ def do_compile(self, project_entry, base_rom_entry, rom_entry): base_rom_rom = Rom() base_rom_rom.from_file(base_rom) - if base_rom_rom.type == ROM_TYPE_NAME_EARTHBOUND and len(base_rom_rom) == 0x300000: - confirm = tkinter.messagebox.askquestion("Expand Your Base ROM?", - "You are attempting to compile using a base ROM which is " - "unexpanded. It is likely that this will not succeed, as CoilSnake " - "needs the extra space in an expanded ROM to store additional data." - "\n\n" - "Would you like to expand this base ROM before proceeding? This " - "will permanently overwrite your base ROM.", + if base_rom_rom.type in ROM_TYPE_GROUP_EBM2 and len(base_rom_rom) == 0x300000: + confirm = tkinter.messagebox.askquestion(self.guistrings.get("ask_expand_rom"), + self.guistrings.get("attempt_compile"), + self.guistrings.get("attempt_compile_2"), + self.guistrings.get("attempt_compile_3"), + "\n\n", + self.guistrings.get("ask_expand_base"), + self.guistrings.get("ask_expand_base_2"), icon='warning') if confirm == "yes": base_rom_rom.expand(0x400000) @@ -294,7 +354,7 @@ def do_compile(self, project_entry, base_rom_entry, rom_entry): self.progress_bar.clear() - log.info("Starting compilation...") + log.info(self.guistrings.get("start_comp")) thread = Thread(target=self._do_compile_help, args=(project, base_rom, rom)) thread.start() @@ -316,10 +376,10 @@ def do_upgrade(self, rom_entry, project_entry): project = project_entry.get() if rom and project: - confirm = tkinter.messagebox.askquestion("Are You Sure?", - "Are you sure you would like to upgrade this project? This operation " - + "cannot be undone.\n\n" - + "It is recommended that you backup your project before proceeding.", + confirm = tkinter.messagebox.askquestion(self.guistrings.get("are_you_sure"), + self.guistrings.get("ask_upgrade") + + self.guistrings.get("ask_upgrade_2") + + self.guistrings.get("backup_info"), icon='warning') if confirm != "yes": return @@ -349,10 +409,10 @@ def do_decompile_script(self, rom_entry, project_entry): project = project_entry.get() if rom and project: - confirm = tkinter.messagebox.askquestion("Are You Sure?", - "Are you sure you would like to decompile the script into this " - "project? This operation cannot be undone.\n\n" - + "It is recommended that you backup your project before proceeding.", + confirm = tkinter.messagebox.askquestion(self.guistrings.get("are_you_sure"), + self.guistrings.get("decomp_script_prompt"), + self.guistrings.get("decomp_script_prompt_2"), + + self.guistrings.get("backup_info"), icon='warning') if confirm != "yes": return @@ -427,7 +487,7 @@ def _do_create_patch_help(self, clean_rom, hacked_rom, patch_path, author, descr elif patch_path.endswith(".ips"): create_patch(clean_rom, hacked_rom, patch_path, "", "", "", progress_bar=self.progress_bar) else: - log.info("Could not patch ROM: Invalid patch format. Please end patchfile with either .ebp or .ips.") + log.info(self.guistrings.get("cant_patch_rom")) return except Exception as inst: log.debug(format_exc()) @@ -442,7 +502,8 @@ def main(self): def create_gui(self): self.root = Tk() - self.root.wm_title("CoilSnake " + information.VERSION) + self.guistrings.change_language(language_name="en") #replace this with [whatever is in Preferences when we put default language in the preferences stuff] + self.guistrings.register_callback(lambda: self.root.wm_title(self.guistrings.get("coilsnake_name") + information.VERSION)) if platform.system() == "Windows": self.root.tk.call("wm", "iconbitmap", self.root._w, asset_path(["images", "CoilSnake.ico"])) @@ -470,22 +531,28 @@ def create_gui(self): self.notebook = tkinter.ttk.Notebook(self.root) decompile_frame = self.create_decompile_frame(self.notebook) - self.notebook.add(decompile_frame, text="Decompile") + self.notebook.add(decompile_frame) + self.guistrings.register_notebook_frame(self.notebook, decompile_frame, "decompile_text") compile_frame = self.create_compile_frame(self.notebook) - self.notebook.add(compile_frame, text="Compile") + self.notebook.add(compile_frame) + self.guistrings.register_notebook_frame(self.notebook, compile_frame, "compile_text") upgrade_frame = self.create_upgrade_frame(self.notebook) - self.notebook.add(upgrade_frame, text="Upgrade") + self.notebook.add(upgrade_frame) + self.guistrings.register_notebook_frame(self.notebook, upgrade_frame, "upgrade") decompile_script_frame = self.create_decompile_script_frame(self.notebook) - self.notebook.add(decompile_script_frame, text="Decompile Script") + self.notebook.add(decompile_script_frame) + self.guistrings.register_notebook_frame(self.notebook, decompile_script_frame, "decomp_script") patcher_patch_frame = self.create_apply_patch_frame(self.notebook) - self.notebook.add(patcher_patch_frame, text="Apply Patch") + self.notebook.add(patcher_patch_frame) + self.guistrings.register_notebook_frame(self.notebook, patcher_patch_frame, "apply_patch") patcher_create_frame = self.create_create_patch_frame(self.notebook) - self.notebook.add(patcher_create_frame, text="Create Patch") + self.notebook.add(patcher_create_frame) + self.guistrings.register_notebook_frame(self.notebook, patcher_create_frame, "create_patch") self.notebook.pack(fill=X) self.notebook.select(self.preferences.get_default_tab()) @@ -577,6 +644,29 @@ def create_about_window(self): self.about_menu.protocol('WM_DELETE_WINDOW', self.about_menu.withdraw) + last_used_temporary_menu_index = 1 + def add_menu_item_and_get_index(self, menu: Menu, command=None, submenu=None) -> int: + templabel = f"MyTemp{self.last_used_temporary_menu_index}" + self.last_used_temporary_menu_index += 1 + if command: + menu.add_command(label=templabel, command=command) + elif submenu: + menu.add_cascade(label=templabel, menu=submenu) + item_index = menu.index(templabel) + return item_index + + def _add_translated_menu_item(self, menu: Menu, label_string_name: str, command=None, submenu=None): + index = self.add_menu_item_and_get_index(menu, command=command, submenu=submenu) + def translation_callback(): + menu.entryconfigure(index, label=self.guistrings.get(label_string_name)) + self.guistrings.register_callback(translation_callback) + + def add_translated_menu_command(self, menu: Menu, label_string_name: str, command: Callable[[], None]): + self._add_translated_menu_item(menu, label_string_name, command=command) + + def add_translated_menu_cascade(self, menu: Menu, label_string_name: str, submenu: Menu): + self._add_translated_menu_item(menu, label_string_name, submenu=submenu) + def create_menubar(self): menubar = Menu(self.root) @@ -590,37 +680,32 @@ def show_about_window(): if platform.system() == "Darwin": app_menu = Menu(menubar, name='apple') menubar.add_cascade(menu=app_menu) - app_menu.add_command(label="About CoilSnake", command=show_about_window) + self.add_translated_menu_command(app_menu, "about_coilsnake", show_about_window) # Tools pulldown menu tools_menu = Menu(menubar, tearoff=0) - tools_menu.add_command(label="EB Project Editor", - command=self.open_ebprojedit) + self.add_translated_menu_command(tools_menu, "eb_proj_edit", self.open_ebprojedit) tools_menu.add_separator() - tools_menu.add_command(label="Expand ROM to 32 MBit", - command=partial(gui_util.expand_rom, self.root)) - tools_menu.add_command(label="Expand ROM to 48 MBit", - command=partial(gui_util.expand_rom_ex, self.root)) + self.add_translated_menu_command(tools_menu, "expand_to_32", partial(gui_util.expand_rom, self.root)) + self.add_translated_menu_command(tools_menu, "expand_to_48", partial(gui_util.expand_rom_ex, self.root)) tools_menu.add_separator() - tools_menu.add_command(label="Add Header to ROM", - command=partial(gui_util.add_header_to_rom, self.root)) - tools_menu.add_command(label="Remove Header from ROM", - command=partial(gui_util.strip_header_from_rom, self.root)) - menubar.add_cascade(label="Tools", menu=tools_menu) + self.add_translated_menu_command(tools_menu, "add_header", partial(gui_util.add_header_to_rom, self.root)) + self.add_translated_menu_command(tools_menu, "remove_header", partial(gui_util.strip_header_from_rom, self.root)) + self.add_translated_menu_cascade(menubar, "tools", tools_menu) # Preferences pulldown menu self.pref_menu = Menu(menubar, tearoff=0) - self.pref_menu.add_command(label="Configure Emulator", - command=self.set_emulator_exe) - self.pref_menu.add_command(label="Configure Java", - command=self.set_java_exe) + self.add_translated_menu_command(self.pref_menu, "config_emu", self.set_emulator_exe) + self.add_translated_menu_command(self.pref_menu, "config_java", self.set_java_exe) + self.pref_menu.add_separator() + self.add_translated_menu_command(self.pref_menu, "config_ccscript", self.set_ccscript_offset) self.pref_menu.add_separator() - self.pref_menu.add_command(label="Configure CCScript", - command=self.set_ccscript_offset) + debug_mode_index = self.add_menu_item_and_get_index(self.pref_menu, command=self.set_debug_mode) + self.guistrings.register_callback(lambda: self.pref_menu.entryconfigure(debug_mode_index, label=self.get_debug_mode_command_label())) self.pref_menu.add_separator() - self.pref_menu.add_command(label=self.get_debug_mode_command_label(), - command=self.set_debug_mode) - menubar.add_cascade(label="Settings", menu=self.pref_menu) + self.add_translated_menu_command(self.pref_menu, "language_setting", self.open_language_window) + + self.add_translated_menu_cascade(menubar, "settings", self.pref_menu) # Help menu help_menu = Menu(menubar, tearoff=0) @@ -629,11 +714,11 @@ def open_coilsnake_website(): webbrowser.open(information.WEBSITE, 2) if platform.system() != "Darwin": - help_menu.add_command(label="About CoilSnake", command=show_about_window) + self.add_translated_menu_command(help_menu, "about_coilsnake", show_about_window) - help_menu.add_command(label="CoilSnake Website", command=open_coilsnake_website) + self.add_translated_menu_command(help_menu, "coilsnake_site", open_coilsnake_website) - menubar.add_cascade(label="Help", menu=help_menu) + self.add_translated_menu_cascade(menubar, "help_text", help_menu) self.root.config(menu=menubar) @@ -641,15 +726,15 @@ def create_decompile_frame(self, notebook): self.decompile_fields = dict() decompile_frame = tkinter.ttk.Frame(notebook) - self.add_title_label_to_frame(text="Decompile a ROM to create a new project.", frame=decompile_frame) + self.add_title_label_to_frame("decomp_rom_new_proj", frame=decompile_frame) profile_selector_init = self.add_profile_selector_to_frame(frame=decompile_frame, tab="decompile", fields=self.decompile_fields) - input_rom_entry = self.add_rom_fields_to_frame(name="ROM", frame=decompile_frame) + input_rom_entry = self.add_rom_fields_to_frame("rom", frame=decompile_frame) self.decompile_fields["rom"] = input_rom_entry - project_entry = self.add_project_fields_to_frame(name="Output Directory", frame=decompile_frame) + project_entry = self.add_project_fields_to_frame("output_dir", frame=decompile_frame) self.decompile_fields["output_directory"] = project_entry profile_selector_init() @@ -657,8 +742,10 @@ def create_decompile_frame(self, notebook): def decompile_tmp(): self.do_decompile(input_rom_entry, project_entry) - decompile_button = Button(decompile_frame, text="Decompile", command=decompile_tmp) + decompile_button = Button(decompile_frame, command=decompile_tmp) decompile_button.pack(fill=X, expand=1) + self.guistrings.register_widget(decompile_button, "decompile_text") + self.components.append(decompile_button) return decompile_frame @@ -667,17 +754,17 @@ def create_compile_frame(self, notebook): self.compile_fields = dict() compile_frame = tkinter.ttk.Frame(notebook) - self.add_title_label_to_frame(text="Compile a project to create a new ROM.", frame=compile_frame) + self.add_title_label_to_frame("comp_rom_new_proj", frame=compile_frame) profile_selector_init = self.add_profile_selector_to_frame(frame=compile_frame, tab="compile", fields=self.compile_fields) - base_rom_entry = self.add_rom_fields_to_frame(name="Base ROM", frame=compile_frame) + base_rom_entry = self.add_rom_fields_to_frame("base_rom", frame=compile_frame) self.compile_fields["base_rom"] = base_rom_entry - project_entry = self.add_project_fields_to_frame(name="Project", frame=compile_frame) + project_entry = self.add_project_fields_to_frame("project", frame=compile_frame) self.compile_fields["project"] = project_entry - output_rom_entry = self.add_rom_fields_to_frame(name="Output ROM", frame=compile_frame, save=True) + output_rom_entry = self.add_rom_fields_to_frame("output_rom", frame=compile_frame, save=True) self.compile_fields["output_rom"] = output_rom_entry profile_selector_init() @@ -685,19 +772,19 @@ def create_compile_frame(self, notebook): def compile_tmp(): self.do_compile(project_entry, base_rom_entry, output_rom_entry) - compile_button = Button(compile_frame, text="Compile", command=compile_tmp) + compile_button = Button(compile_frame, command=compile_tmp) compile_button.pack(fill=X, expand=1) + self.guistrings.register_widget(compile_button, "compile_text") self.components.append(compile_button) return compile_frame def create_upgrade_frame(self, notebook): upgrade_frame = tkinter.ttk.Frame(notebook) - self.add_title_label_to_frame(text="Upgrade a project created using an older version of CoilSnake.", - frame=upgrade_frame) + self.add_title_label_to_frame("upgrade_info", frame=upgrade_frame) - rom_entry = self.add_rom_fields_to_frame(name="Clean ROM", frame=upgrade_frame) - project_entry = self.add_project_fields_to_frame(name="Project", frame=upgrade_frame) + rom_entry = self.add_rom_fields_to_frame("clean_rom", frame=upgrade_frame) + project_entry = self.add_project_fields_to_frame("project", frame=upgrade_frame) def upgrade_tmp(): self.preferences["default upgrade rom"] = rom_entry.get() @@ -705,8 +792,9 @@ def upgrade_tmp(): self.preferences.save() self.do_upgrade(rom_entry, project_entry) - self.upgrade_button = Button(upgrade_frame, text="Upgrade", command=upgrade_tmp) + self.upgrade_button = Button(upgrade_frame, command=upgrade_tmp) self.upgrade_button.pack(fill=X, expand=1) + self.guistrings.register_widget(self.upgrade_button, "upgrade") self.components.append(self.upgrade_button) if self.preferences["default upgrade rom"]: @@ -721,11 +809,10 @@ def upgrade_tmp(): def create_decompile_script_frame(self, notebook): decompile_script_frame = tkinter.ttk.Frame(notebook) - self.add_title_label_to_frame(text="Decompile a ROM's script to an already existing project.", - frame=decompile_script_frame) + self.add_title_label_to_frame("decomp_rom_script", frame=decompile_script_frame) - input_rom_entry = self.add_rom_fields_to_frame(name="ROM", frame=decompile_script_frame) - project_entry = self.add_project_fields_to_frame(name="Project", frame=decompile_script_frame) + input_rom_entry = self.add_rom_fields_to_frame("rom", frame=decompile_script_frame) + project_entry = self.add_project_fields_to_frame("project", frame=decompile_script_frame) def decompile_script_tmp(): self.preferences["default decompile script rom"] = input_rom_entry.get() @@ -733,8 +820,9 @@ def decompile_script_tmp(): self.preferences.save() self.do_decompile_script(input_rom_entry, project_entry) - button = Button(decompile_script_frame, text="Decompile Script", command=decompile_script_tmp) + button = Button(decompile_script_frame, command=decompile_script_tmp) button.pack(fill=X, expand=1) + self.guistrings.register_widget(button, "decomp_script") self.components.append(button) if self.preferences["default decompile script rom"]: @@ -749,13 +837,13 @@ def decompile_script_tmp(): def create_apply_patch_frame(self, notebook): patcher_patch_frame = tkinter.ttk.Frame(notebook) - self.add_title_label_to_frame("Apply an EBP or IPS patch to a ROM", patcher_patch_frame) + self.add_title_label_to_frame("apply_patch_info", frame=patcher_patch_frame) - clean_rom_entry = self.add_rom_fields_to_frame(name="Clean ROM", frame=patcher_patch_frame, padding_buttons=0) - patched_rom_entry = self.add_rom_fields_to_frame(name="Patched ROM", frame=patcher_patch_frame, save=True, + clean_rom_entry = self.add_rom_fields_to_frame("clean_rom", frame=patcher_patch_frame, padding_buttons=0) + patched_rom_entry = self.add_rom_fields_to_frame("patched_rom", frame=patcher_patch_frame, save=True, padding_buttons=0) - patch_entry = self.add_patch_fields_to_frame(name="Patch", frame=patcher_patch_frame) - headered_var = self.add_headered_field_to_frame(name="ROM Header (IPS only)", frame=patcher_patch_frame) + patch_entry = self.add_patch_fields_to_frame("patch", frame=patcher_patch_frame) + headered_var = self.add_headered_field_to_frame("header_ips_only", frame=patcher_patch_frame) def patch_rom_tmp(): self.preferences["default clean rom"] = clean_rom_entry.get() @@ -764,8 +852,9 @@ def patch_rom_tmp(): self.preferences.save() self.do_patch_rom(clean_rom_entry, patched_rom_entry, patch_entry, headered_var) - button = Button(patcher_patch_frame, text="Patch ROM", command=patch_rom_tmp) + button = Button(patcher_patch_frame, command=patch_rom_tmp) button.pack(fill=X, expand=1) + self.guistrings.register_widget(button, "patch_rom") self.components.append(button) if self.preferences["default clean rom"]: @@ -782,12 +871,11 @@ def patch_rom_tmp(): def create_create_patch_frame(self, notebook): patcher_create_frame = tkinter.ttk.Frame(notebook) - self.add_title_label_to_frame("Create EBP patch from a ROM", patcher_create_frame) + self.add_title_label_to_frame("create_patch", patcher_create_frame) - clean_rom_entry = self.add_rom_fields_to_frame(name="Clean ROM", frame=patcher_create_frame, padding_buttons=0) - hacked_rom_entry = self.add_rom_fields_to_frame(name="Modified ROM", frame=patcher_create_frame, - padding_buttons=0) - patch_entry = self.add_patch_fields_to_frame(name="Patch", frame=patcher_create_frame, save=True) + clean_rom_entry = self.add_rom_fields_to_frame("clean_rom", frame=patcher_create_frame, padding_buttons=0) + hacked_rom_entry = self.add_rom_fields_to_frame("modded_rom", frame=patcher_create_frame, padding_buttons=0) + patch_entry = self.add_patch_fields_to_frame("patch", frame=patcher_create_frame, save=True) def create_patch_tmp(author, description, title): self.preferences["default clean rom"] = clean_rom_entry.get() @@ -802,29 +890,29 @@ def create_patch_do_first(): elif patch_entry.get().endswith(".ips"): create_patch_tmp("", "", "") else: - exc = Exception("Could not create patch because patch does not end in .ips or .ebp") + exc = Exception(self.guistrings.get("invalid_format")) log.error(exc) def popup_ebp_patch_info(self, notebook): if self.preferences["default author"] is None: - self.preferences["default author"] = "Author" + self.preferences["default author"] = self.gui_string_manager.get("author") author = self.preferences["default author"] if self.preferences["default description"] is None: - self.preferences["default description"] = "Description" + self.preferences["default description"] = self.gui_string_manager.get("desc") description = self.preferences["default description"] if self.preferences["default title"] is None: - self.preferences["default title"] = "Title" + self.preferences["default title"] = self.gui_string_manager.get("title") title = self.preferences["default title"] top = self.top = Toplevel(notebook) - top.wm_title("EBP Patch") - l = Label(top,text="Input EBP Patch Info.") + top.wm_title(self.gui_string_manager.get("ebp_patch")) + l = Label(top,text=self.gui_string_manager.get("input_ebp")) l.pack() auth = Entry(top) auth.delete(0,) @@ -849,11 +937,12 @@ def cleanup(): self.top.destroy() create_patch_tmp(author, description, title) - self.b=Button(top,text='OK',command=cleanup) + self.b=Button(top,text=self.gui_string_manager.get("ok"),command=cleanup) self.b.pack() - button = Button(patcher_create_frame, text="Create Patch", command=create_patch_do_first) + button = Button(patcher_create_frame, command=create_patch_do_first) button.pack(fill=X, expand=1) + self.guistrings.register_widget(button, "create_patch") self.components.append(button) if self.preferences["default clean rom"]: @@ -869,13 +958,17 @@ def cleanup(): return patcher_create_frame - def add_title_label_to_frame(self, text, frame): - Label(frame, text=text, justify=CENTER).pack(fill=BOTH, expand=1) + def add_title_label_to_frame(self, text_string_name, frame): + label = Label(frame, justify=CENTER) + label.pack(fill=BOTH, expand=1) + self.guistrings.register_widget(label, text_string_name) def add_profile_selector_to_frame(self, frame, tab, fields): profile_frame = tkinter.ttk.Frame(frame) - Label(profile_frame, text="Profile:", width=LABEL_WIDTH).pack(side=LEFT) + label = Label(profile_frame, width=LABEL_WIDTH) + label.pack(side=LEFT) + self.guistrings.register_widget(label, "profile") def tmp_select(profile_name): for field_id in fields: @@ -902,12 +995,12 @@ def tmp_reload_options(selected_profile_name=None): tmp_select(selected_profile_name) def tmp_new(): - profile_name = tkinter.simpledialog.askstring("New Profile Name", "Specify the name of the new profile.") + profile_name = tkinter.simpledialog.askstring(self.guistrings.get("new_prof_name"), self.guistrings.get("specify_name")) if profile_name: profile_name = profile_name.strip() if self.preferences.has_profile(tab, profile_name): tkinter.messagebox.showerror(parent=self.root, - title="Error", + title=self.guistrings.get("error"), message="A profile with that name already exists.") return @@ -924,23 +1017,26 @@ def tmp_save(): def tmp_delete(): if self.preferences.count_profiles(tab) <= 1: tkinter.messagebox.showerror(parent=self.root, - title="Error", - message="Cannot delete the only profile.") + title=self.guistrings.get("error"), + message=self.guistrings.get("cant_delete_prof")) else: self.preferences.delete_profile(tab, profile_var.get()) tmp_reload_options() self.preferences.save() - button = Button(profile_frame, text="Save", width=BUTTON_WIDTH, command=tmp_save) + button = Button(profile_frame, width=BUTTON_WIDTH, command=tmp_save) button.pack(side=LEFT) + self.guistrings.register_widget(button, "save") self.components.append(button) - button = Button(profile_frame, text="Delete", width=BUTTON_WIDTH, command=tmp_delete) + button = Button(profile_frame, width=BUTTON_WIDTH, command=tmp_delete) button.pack(side=LEFT) + self.guistrings.register_widget(button, "delete") self.components.append(button) - button = Button(profile_frame, text="New", width=BUTTON_WIDTH, command=tmp_new) + button = Button(profile_frame, width=BUTTON_WIDTH, command=tmp_new) button.pack(side=LEFT) + self.guistrings.register_widget(button, "new") self.components.append(button) profile_frame.pack(fill=X, expand=1) @@ -950,10 +1046,12 @@ def tmp_reload_options_and_select_default(): return tmp_reload_options_and_select_default - def add_rom_fields_to_frame(self, name, frame, save=False, padding_buttons=1): + def add_rom_fields_to_frame(self, text_string_name, frame, save=False, padding_buttons=1): rom_frame = tkinter.ttk.Frame(frame) - Label(rom_frame, text="{}:".format(name), width=LABEL_WIDTH, justify=RIGHT).pack(side=LEFT) + label = Label(rom_frame, width=LABEL_WIDTH, justify=RIGHT) + label.pack(side=LEFT) + self.guistrings.register_callback(lambda: label.configure(text="{}:".format(self.guistrings.get(text_string_name)))) rom_entry = Entry(rom_frame) rom_entry.pack(side=LEFT, fill=BOTH, expand=1, padx=1) self.components.append(rom_entry) @@ -964,15 +1062,17 @@ def browse_tmp(): def run_tmp(): self.run_rom(rom_entry) - button = Button(rom_frame, text="Browse...", command=browse_tmp, width=BUTTON_WIDTH) + button = Button(rom_frame, command=browse_tmp, width=BUTTON_WIDTH) button.pack(side=LEFT) + self.guistrings.register_widget(button, "browse") self.components.append(button) - button = Button(rom_frame, text="Run", command=run_tmp, width=BUTTON_WIDTH) + button = Button(rom_frame, command=run_tmp, width=BUTTON_WIDTH) button.pack(side=LEFT) + self.guistrings.register_widget(button, "run") self.components.append(button) - for i in range(padding_buttons): + for _ in range(padding_buttons): button = Button(rom_frame, text="", width=BUTTON_WIDTH, state=DISABLED, takefocus=False) button.pack(side=LEFT) button.lower() @@ -981,10 +1081,12 @@ def run_tmp(): return rom_entry - def add_project_fields_to_frame(self, name, frame): + def add_project_fields_to_frame(self, text_string_name, frame): project_frame = tkinter.ttk.Frame(frame) - Label(project_frame, text="{}:".format(name), width=LABEL_WIDTH, justify=RIGHT).pack(side=LEFT) + label = Label(project_frame, width=LABEL_WIDTH, justify=RIGHT) + label.pack(side=LEFT) + self.guistrings.register_callback(lambda: label.configure(text="{}:".format(self.guistrings.get(text_string_name)))) project_entry = Entry(project_frame) project_entry.pack(side=LEFT, fill=BOTH, expand=1, padx=1) self.components.append(project_entry) @@ -998,28 +1100,31 @@ def open_tmp(): def edit_tmp(): self.open_ebprojedit(project_entry) - button = Button(project_frame, text="Browse...", command=browse_tmp, width=BUTTON_WIDTH) + button = Button(project_frame, command=browse_tmp, width=BUTTON_WIDTH) button.pack(side=LEFT) + self.guistrings.register_widget(button, "browse") self.components.append(button) - button = Button(project_frame, text="Open", command=open_tmp, width=BUTTON_WIDTH) + button = Button(project_frame, command=open_tmp, width=BUTTON_WIDTH) button.pack(side=LEFT) + self.guistrings.register_widget(button, "open_text") self.components.append(button) - button = Button(project_frame, text="Edit", command=edit_tmp, width=BUTTON_WIDTH) + button = Button(project_frame, command=edit_tmp, width=BUTTON_WIDTH) button.pack(side=LEFT) + self.guistrings.register_widget(button, "edit") self.components.append(button) project_frame.pack(fill=X, expand=1) return project_entry - def add_patch_fields_to_frame(self, name, frame, save=False): + def add_patch_fields_to_frame(self, text_string_name, frame, save=False): patch_frame = tkinter.ttk.Frame(frame) - Label( - patch_frame, text="{}:".format(name), width=LABEL_WIDTH, justify=RIGHT - ).pack(side=LEFT) + label = Label(patch_frame, width=LABEL_WIDTH, justify=RIGHT) + label.pack(side=LEFT) + self.guistrings.register_callback(lambda: label.configure(text="{}:".format(self.guistrings.get(text_string_name)))) patch_entry = Entry(patch_frame) patch_entry.pack(side=LEFT, fill=BOTH, expand=1, padx=1) self.components.append(patch_entry) @@ -1027,8 +1132,9 @@ def add_patch_fields_to_frame(self, name, frame, save=False): def browse_tmp(): browse_for_patch(self.root, patch_entry, save) - button = Button(patch_frame, text="Browse...", command=browse_tmp, width=BUTTON_WIDTH) + button = Button(patch_frame, command=browse_tmp, width=BUTTON_WIDTH) button.pack(side=LEFT) + self.guistrings.register_widget(button, "browse") self.components.append(button) button = Button(patch_frame, text="", width=BUTTON_WIDTH, state=DISABLED, takefocus=False) @@ -1039,14 +1145,15 @@ def browse_tmp(): return patch_entry - def add_headered_field_to_frame(self, name, frame): + def add_headered_field_to_frame(self, text_string_name, frame): patch_frame = tkinter.ttk.Frame(frame) headered_var = BooleanVar() - headered_check = Checkbutton(patch_frame, text=name, variable=headered_var) + headered_check = Checkbutton(patch_frame, variable=headered_var) headered_check.pack( side=LEFT, fill=BOTH, expand=1 ) + self.guistrings.register_widget(headered_check, text_string_name) self.components.append(headered_check) patch_frame.pack(fill=BOTH, expand=1) @@ -1055,4 +1162,4 @@ def add_headered_field_to_frame(self, name, frame): def main(): gui = CoilSnakeGui() - sys.exit(gui.main()) + sys.exit(gui.main()) \ No newline at end of file diff --git a/coilsnake/ui/language.py b/coilsnake/ui/language.py new file mode 100644 index 0000000..c0a55dc --- /dev/null +++ b/coilsnake/ui/language.py @@ -0,0 +1,163 @@ +from collections import defaultdict +from dataclasses import dataclass +import json +import logging +from typing import Dict, List, Optional + +@dataclass +class Language: + iso639_1_name: str + full_name: str + alternative_names: List[str] + + def get_json_path(self) -> str: + return f"coilsnake/lang/{self.iso639_1_name}.json" + +LANGUAGES: List[Language] = [ + Language("en", "English", []), + Language("ja", "日本語", ["Japanese", "jp"]), +] + +def _build_language_lookup() -> Dict[str, Language]: + ret = {} + for language in LANGUAGES: + for option in (language.iso639_1_name, language.full_name, *language.alternative_names): + lookup = option.lower() + assert lookup not in ret, "duplicate language name" + ret[lookup] = language + return ret + +_LANGUAGE_LOOKUP = _build_language_lookup() + +def get_language_by_string(language_str: str) -> Optional[Language]: + return _LANGUAGE_LOOKUP.get(language_str.lower(), None) + +class TranslationStringManager: + _TRANSLATIONS_LANGUAGE_NOT_LOADED = defaultdict( lambda: "Translation not loaded" ) + + @staticmethod + def _json_to_translations(json_data): + missing = "Missing localization string" + return defaultdict(lambda: missing, json_data) + + @classmethod + def _load_language(cls, language: Language): + try: + with open(language.get_json_path(), "r", encoding="utf-8") as file: + json_data = json.load(file) + return cls._json_to_translations(json_data) + except: + return None + + def __init__(self): + self.translations = self._TRANSLATIONS_LANGUAGE_NOT_LOADED + self.callbacks = set() + + def get(self, string_name: str) -> str: + return self.translations[string_name] + + def change_language(self, language: Language = None, language_name: str = None) -> None: + if not language: + language = get_language_by_string(language_name) + if not language: + return False + translations = self._load_language(language) + if not translations: + return False + self.translations = translations + for cb in self.callbacks: + cb() + return True + + def register_callback(self, cb, invoke=True): + self.callbacks.add(cb) + if invoke: + cb() + +global_strings = TranslationStringManager() + +class TranslatedLogRecord(logging.LogRecord): + def getMessage(self): + msg = str(self.msg) # see logging cookbook + if self.args: + args = self.args + if isinstance(args, list) and len(args) == 1: + args = args[0] + assert isinstance(args, dict), "Incorrect use of log.info_t() or similar method" + msg = msg.format(**args) + return msg + +class TranslatedLogger(logging.Logger): + def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func = None, extra = None, sinfo = None): + assert extra is None, "extra not supported" + return TranslatedLogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo) + + def debug_t(self, msg_string_name: str, **replacements) -> None: + 'Same as `logger.debug()` but takes the name of a translated string in place of a message.' + self.debug(global_strings.get(msg_string_name), replacements) + def info_t(self, msg_string_name: str, **replacements) -> None: + 'Same as `logger.info()` but takes the name of a translated string in place of a message.' + self.info(global_strings.get(msg_string_name), replacements) + def warn_t(self, msg_string_name: str, **replacements) -> None: + 'Deprecated - use `warning_t` instead' + self.warn(global_strings.get(msg_string_name), replacements) + def warning_t(self, msg_string_name: str, **replacements) -> None: + 'Same as `logger.warning()` but takes the name of a translated string in place of a message.' + self.warning(global_strings.get(msg_string_name), replacements) + def error_t(self, msg_string_name: str, **replacements) -> None: + 'Same as `logger.error()` but takes the name of a translated string in place of a message.' + self.error(global_strings.get(msg_string_name), replacements) + +def getLogger(name: str) -> TranslatedLogger: + oldLoggerClass = logging.getLoggerClass() + try: + logging.setLoggerClass(TranslatedLogger) + logger = logging.getLogger(name) + finally: + logging.setLoggerClass(oldLoggerClass) + return logger + +''' +HOW TO USE THE LOGGER FOR EASY TRANSLATED STRINGS: + +1. Change the translated strings to use format strings with a field name. +(https://docs.python.org/3/library/string.html#formatstrings) + +In practice, this looks like taking format strings which look like this: + "console_finish_decomp": "Finished decompiling {} in {:.2f}s", + +... and changing them to this format: + "console_finish_decomp": "Finished decompiling {class_name} in {duration:.2f}s", + +This assigns names to each element in the format string, so we can refer to them later. + +If the string has no fields to be formatted, you don't do anything here. + +2. Change the code to use `getLogger()` from language.py instead of logging. + +If before we had this: + import logging + # Set up logging + log = logging.getLogger(__name__) + +Change to: + from coilsnake.ui.language import getLogger + # Set up logging + log = getLogger(__name__) + +3. Change invocations of `log.info` or any other logging method to `log.info_t` +and use the translated string name (such as "console_finish_decomp") instead of +getting the string value. + +An example would be: + # JSON has: "console_finish_decomp": "Finished decompiling {} in {:.2f}s" + log.info(strings.get("console_finish_decomp").format(module_class.NAME, time.time() - start_time)) + +This becomes: + # JSON has: "console_finish_decomp": "Finished decompiling {class_name} in {duration:.2f}s" + log.info_t("console_finish_decomp", class_name=module_class.NAME, duration=time.time() - start_time) + +If the string has no fields to be formatted, you call `log.info_t()` with only the +string name, and no added arguments. + Ex: log.info_t("console_proj_already_updated") +'''