├── .DS_Store ├── .gitignore ├── README.md ├── dialogs ├── Map001.json └── README.md ├── dialogs_gaya └── CommonEvents.json ├── dialogs_translator.py ├── images ├── en1.jpg ├── en2.jpg ├── ita1.jpg └── ita2.jpg ├── objects ├── Actors.json └── README.md ├── objects_translator.py ├── print_neatly.py └── requirements.txt /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davide97l/rpgmaker-mv-translator/f4ff2a146b72edbd61ba9259be95e16172c6bbfd/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | dialogs_* 133 | objects_* 134 | *.zip 135 | log*.txt 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RPGMaker-MV Translator 🕹️ 2 | 3 | 🎮 Use AI to translate all the dialogs and texts of your RPGMaker *automatically*. 4 | 5 | 👊 You worked hard to make your game, now let *AI* work hard for you. 6 | 7 | | Original language 🇮🇹 | Automatic Translation 🇺🇸 | 8 | |:----------------------:|:---------------------------:| 9 | | ![](images/ita2.jpg) | ![](images/en2.jpg) | 10 | | ![](images/ita1.jpg) | ![](images/en1.jpg) | 11 | 12 | ## Why should you use it? 🤔 13 | 14 | 👉 RPG games usually consist of many thousands of dialog events and other forms of text displaying valuable information 15 | to understand the plot of the game, but also to know the effect and proprieties of various objects present in the game 16 | such as items, weapons, skills, enemies, ... . 17 | 18 | 👉 Translating an RPG game from one language to another would thus require an enormous amount of time 19 | and effort for a human, but it is a task that can be easily accomplished by a machine, or at least speed up considerably the 20 | work of a human translator saving costs and time. 21 | 22 | 👉 This project implements a tool that is able to automatically translate a game deployed with [RPGMaker-MV](https://www.rpgmakerweb.com/products/rpg-maker-mv). 23 | 24 | ## Game files overview 🎮 25 | 26 | Game files containing the data we want to translate are usually contained in the folder `data/`. 27 | Among these json files, the ones we are going to translate are the following: 28 | - `Armors.json`: contains the **name** and the **description** of all armors 🛡️. ️ 29 | - `Weapons.json`: contains the **name** and the **description** of all weapons 🗡️. 30 | - `Items.json`: contains the **name** and the **description** of all items 💡. 31 | - `Skills.json`: contains the **name** and the **description** of all skills ⚡. 32 | - `Enemies.json`: contains the **name** of all enemies 👾. 33 | - `MapInfos.json`: contains the **name** of all maps 🗺️. 34 | - `Classes.json`: contains the **name** of all classes 🧙. 35 | - `States.json`: contains the **name** and the relative **messages** of all states ✨. 36 | - `Actors.json`: contains the **profile** of all characters 👩. 37 | 38 | Other files that need to be translated, but deserve particular attention are: 39 | - `CommonEvents.json`: contains the **dialogs** relative to the common events in the game 🤖. 40 | - `MapXXX.json`: contains the **dialogs** relative to all the maps. Basically it contains most of the dialogs on the 41 | game which would probably require a massive amount of time if translated manually 🗺️. 42 | 43 | The remaining files are not translated since they don't contain much text to translate such as `System.json` or there is 44 | nothing critical to translate such as `Animations.json`. 45 | 46 | ## Usage 💡 47 | 48 | ### Translate the dialogs files 49 | 50 | **Note**: the program uses a Google Translate API to perform translations, thus a **stable** internet connection is required. 51 | 52 | 1. Clone this repo: `git clone https://github.com/davide97l/rpgmaker-mv-translator`. 53 | 2. Install dependencies: `pip install -r requirements.txt`. 54 | 3. Copy `CommonEvents.json` and all the `MapXXX.json` files from you game `data/folder` to this project `dialogs` folder. 55 | 4. For a basic usage, run the command: 56 | ``` 57 | python dialogs_translator.py --print_neatly --source_lang it --dest_lang en 58 | ``` 59 | 3. Most important arguments explanation: 60 | - `source_lang`: (string) the **original language** of your game (en - english, it - italian, zh - chinese, fr -french, 61 | sp - spanish, de - deutsch, ...). 62 | - `dest_lang`: (string) the language you want to **translate** your game. 63 | - `verbose`: (bool) if True, show each original and corresponding translated sentence during execution. 64 | - `input_folder`: (string) the folder containing the files to translate (default: `dialogs`). 65 | - `print_neatly`: (bool) if True, adapts the translated sentence to fit the dialog window. 66 | This is because, by default, each dialog window row is a unique string itself and its length can change after translation. 67 | This option also improves the translation quality because each dialog window would be translated at once without 68 | translating each row one by one which causes loss of context. If you are curious how this algorithm works you can 69 | check this [blog](https://davideliu.com/2019/12/22/print-neatly/). 70 | - `max_len` (int): Used only when `print_neatly` is True. Indicates the length of the dialog window. 71 | 4. After execution, which may take a while depending on the number and size of files, your translated files will be saved in `data_xx` 72 | where `xx` is the code of the translated language (`dialogs_en` if `--dest_lang en`). 73 | 5. Copy back the content of `dialogs_xx` to the folder `data` of your game replacing the old files. 74 | 75 | Example of **print neatly** with `max_len=32` translating from english to italian: 76 | ``` 77 | |The hunter has won the battle | 78 | |and unlocked a new secret | 79 | |skill. | 80 | 81 | After italian translation without print neatly: 82 | 83 | |Il cacciatore ha vinto la batta|glia 84 | |e sbloccato una nuovo segreto | 85 | |potere. | 86 | 87 | After print neatly: 88 | 89 | |Il cacciatore ha vinto la | 90 | |battaglia e sbloccato un nuovo | 91 | |potere segreto. | 92 | ``` 93 | 94 | ### Translate the object files 95 | 96 | 1. Copy the files you want to translate among `Armors.json`, `Weapons.json`, `Items.json`, `Skills.json`, `Enemies.json`, `MapInfos.json`, `Classes.json`, `States.json`, `Actors.json` 97 | from you game `data/` folder to this project `object` folder. 98 | 3. For a basic usage, run the command: 99 | ``` 100 | python objects_translator.py --source_lang it --dest_lang en 101 | ``` 102 | 3. The arguments are the same as the ones used by `dialogs_translator.py`, and print neatly is automatically used in the **description** field. 103 | By default `input_folder` is set to `objects`). 104 | 4. After execution, your translated files will be saved in `objects_xx` 105 | where `xx` is the code of the translated language (`objects_en` if `--dest_lang en`). 106 | 5. Copy back the content of `objects_xx` to the folder `data` of your game replacing the old files. 107 | 108 | ## Support 109 | If you found this project interesting please support me by giving it a :star:, I would really appreciate it :grinning: 110 | 111 | ## More 112 | Check this [link](https://davideliu.com/category/videogames/) to play some of my RPGs 😍. -------------------------------------------------------------------------------- /dialogs/Map001.json: -------------------------------------------------------------------------------- 1 | { 2 | "events":[ 3 | {"id":1,"name":"Riflessione","note":"Da qui inizia il gioco","pages":[{"conditions":{"actorId":1,"actorValid":true,"itemId":1,"itemValid":false,"selfSwitchCh":"A","selfSwitchValid":false,"switch1Id":1,"switch1Valid":false,"switch2Id":1,"switch2Valid":false,"variableId":3,"variableValid":false,"variableValue":0},"directionFix":false,"image":{"characterIndex":0,"characterName":"","direction":2,"pattern":0,"tileId":0},"list":[{"code":356,"indent":0,"parameters":["EnableTurnOrderDisplay"]},{"code":356,"indent":0,"parameters":["HidePartyLimitGauge"]},{"code":136,"indent":0,"parameters":[0]},{"code":101,"indent":0,"parameters":["",0,2,0]},{"code":401,"indent":0,"parameters":["All'orizzonte il cielo blu andava a cadere nelle"]},{"code":401,"indent":0,"parameters":["limpide acque che si affacciavano timide sulle coste"]},{"code":401,"indent":0,"parameters":["sabbiose."]},{"code":101,"indent":0,"parameters":["",0,2,0]},{"code":401,"indent":0,"parameters":["Un ragazzo attendeva lo scorrere del tempo, per lui"]},{"code":401,"indent":0,"parameters":["esisteva solo quel posto, l'isola, la giungla, il mare."]},{"code":101,"indent":0,"parameters":["",0,2,0]},{"code":401,"indent":0,"parameters":["Non aveva ricordi del suo passato, ma aveva un nome:"]},{"code":401,"indent":0,"parameters":["Destiny"]},{"code":355,"indent":0,"parameters":["Galv.QUEST.activate(1);"]},{"code":129,"indent":0,"parameters":[1,0,false]},{"code":121,"indent":0,"parameters":[1,1,0]},{"code":123,"indent":0,"parameters":["A",0]},{"code":0,"indent":0,"parameters":[]}],"moveFrequency":3,"moveRoute":{"list":[{"code":0,"parameters":[]}],"repeat":true,"skippable":false,"wait":false},"moveSpeed":4,"moveType":0,"priorityType":2,"stepAnime":false,"through":false,"trigger":3,"walkAnime":false},{"conditions":{"actorId":1,"actorValid":true,"itemId":1,"itemValid":false,"selfSwitchCh":"A","selfSwitchValid":true,"switch1Id":1,"switch1Valid":false,"switch2Id":1,"switch2Valid":false,"variableId":1,"variableValid":false,"variableValue":0},"directionFix":false,"image":{"characterIndex":0,"characterName":"","direction":2,"pattern":0,"tileId":0},"list":[{"code":0,"indent":0,"parameters":[]}],"moveFrequency":3,"moveRoute":{"list":[{"code":0,"parameters":[]}],"repeat":true,"skippable":false,"wait":false},"moveSpeed":3,"moveType":0,"priorityType":0,"stepAnime":false,"through":false,"trigger":0,"walkAnime":true}],"x":0,"y":0}, 4 | {"id":2,"name":"Salvataggio","note":"","pages":[{"conditions":{"actorId":2,"actorValid":true,"itemId":1,"itemValid":false,"selfSwitchCh":"A","selfSwitchValid":false,"switch1Id":1,"switch1Valid":false,"switch2Id":1,"switch2Valid":false,"variableId":1,"variableValid":false,"variableValue":0},"directionFix":true,"image":{"tileId":0,"characterName":"!Crystal","direction":4,"pattern":0,"characterIndex":3},"list":[{"code":352,"indent":0,"parameters":[]},{"code":0,"indent":0,"parameters":[]}],"moveFrequency":3,"moveRoute":{"list":[{"code":0,"parameters":[]}],"repeat":true,"skippable":false,"wait":false},"moveSpeed":3,"moveType":0,"priorityType":1,"stepAnime":true,"through":false,"trigger":0,"walkAnime":true}],"x":27,"y":19} 5 | ] 6 | } -------------------------------------------------------------------------------- /dialogs/README.md: -------------------------------------------------------------------------------- 1 | # Dialogs folder 2 | 3 | Put the files you want to translate among: 4 | - `CommonEvents.json` 5 | - `MapXXX.json` -------------------------------------------------------------------------------- /dialogs_gaya/CommonEvents.json: -------------------------------------------------------------------------------- 1 | [ 2 | null, 3 | {"id":1,"list":[{"code":314,"indent":0,"parameters":[0,0]},{"code":0,"indent":0,"parameters":[]}],"name":"Recupero totale","switchId":1,"trigger":0}, 4 | {"id":2,"list":[{"code":356,"indent":0,"parameters":["EnemyBook open"]},{"code":351,"indent":0,"parameters":[]},{"code":0,"indent":0,"parameters":[]}],"name":"EnemyBook","switchId":1,"trigger":0}, 5 | {"id":3,"list":[{"code":122,"indent":0,"parameters":[35,35,0,3,7,0,0]},{"code":111,"indent":0,"parameters":[1,35,0,7,5]},{"code":101,"indent":1,"parameters":["",0,0,2]},{"code":401,"indent":1,"parameters":["La mappa è visualizzabile solo nella mappa del mondo."]},{"code":115,"indent":1,"parameters":[]},{"code":0,"indent":1,"parameters":[]},{"code":412,"indent":0,"parameters":[]},{"code":231,"indent":0,"parameters":[1,"Gaya",0,0,0,0,100,100,255,0]},{"code":122,"indent":0,"parameters":[33,33,0,3,5,-1,0]},{"code":122,"indent":0,"parameters":[34,34,0,3,5,-1,1]},{"code":122,"indent":0,"parameters":[33,33,3,0,816]},{"code":122,"indent":0,"parameters":[33,33,4,0,140]},{"code":122,"indent":0,"parameters":[34,34,3,0,624]},{"code":122,"indent":0,"parameters":[34,34,4,0,140]},{"code":231,"indent":0,"parameters":[2,"stella",1,1,33,34,100,100,255,0]},{"code":118,"indent":0,"parameters":["loop"]},{"code":111,"indent":0,"parameters":[11,"cancel"]},{"code":235,"indent":1,"parameters":[1]},{"code":235,"indent":1,"parameters":[2]},{"code":115,"indent":1,"parameters":[]},{"code":0,"indent":1,"parameters":[]},{"code":412,"indent":0,"parameters":[]},{"code":119,"indent":0,"parameters":["loop"]},{"code":0,"indent":0,"parameters":[]}],"name":"World map","switchId":1,"trigger":0}, 6 | {"id":4,"list":[{"code":312,"indent":0,"parameters":[0,0,0,1,16]},{"code":0,"indent":0,"parameters":[]}],"name":"Procedure fine battaglia","switchId":1,"trigger":0}, 7 | {"id":5,"list":[{"code":121,"indent":0,"parameters":[181,181,1]},{"code":121,"indent":0,"parameters":[182,182,1]},{"code":121,"indent":0,"parameters":[183,183,1]},{"code":121,"indent":0,"parameters":[184,184,1]},{"code":121,"indent":0,"parameters":[185,185,1]},{"code":121,"indent":0,"parameters":[186,186,1]},{"code":121,"indent":0,"parameters":[187,187,1]},{"code":121,"indent":0,"parameters":[188,188,1]},{"code":122,"indent":0,"parameters":[18,18,0,0,0]},{"code":0,"indent":0,"parameters":[]}],"name":"Reset Enigma Wushu 1","switchId":1,"trigger":0}, 8 | {"id":6,"list":[{"code":311,"indent":0,"parameters":[0,1,1,0,9999,true]},{"code":0,"indent":0,"parameters":[]}],"name":"Morte Destiny","switchId":1,"trigger":0}, 9 | {"id":7,"list":[{"code":311,"indent":0,"parameters":[0,0,0,0,9999,false]},{"code":0,"indent":0,"parameters":[]}],"name":"Recupero HP","switchId":1,"trigger":0}, 10 | {"id":8,"list":[{"code":313,"indent":0,"parameters":[0,0,0,14]},{"code":313,"indent":0,"parameters":[0,0,0,16]},{"code":0,"indent":0,"parameters":[]}],"name":"Add Scudo e Resistenza","switchId":1,"trigger":0}, 11 | {"id":9,"list":[{"code":129,"indent":0,"parameters":[1,1,false]},{"code":129,"indent":0,"parameters":[2,1,false]},{"code":129,"indent":0,"parameters":[3,1,false]},{"code":129,"indent":0,"parameters":[4,1,false]},{"code":129,"indent":0,"parameters":[5,1,false]},{"code":129,"indent":0,"parameters":[6,1,false]},{"code":129,"indent":0,"parameters":[7,1,false]},{"code":129,"indent":0,"parameters":[8,1,false]},{"code":129,"indent":0,"parameters":[9,1,false]},{"code":129,"indent":0,"parameters":[10,1,false]},{"code":129,"indent":0,"parameters":[13,1,false]},{"code":129,"indent":0,"parameters":[15,1,false]},{"code":129,"indent":0,"parameters":[16,1,false]},{"code":129,"indent":0,"parameters":[17,1,false]},{"code":129,"indent":0,"parameters":[18,1,false]},{"code":129,"indent":0,"parameters":[31,1,false]},{"code":0,"indent":0,"parameters":[]}],"name":"Togli Party","switchId":1,"trigger":0}, 12 | {"id":10,"list":[{"code":231,"indent":0,"parameters":[1,"Stige",0,0,220,80,100,100,255,0]},{"code":250,"indent":0,"parameters":[{"name":"Monster4","volume":90,"pitch":100,"pan":0}]},{"code":0,"indent":0,"parameters":[]}],"name":"Stige","switchId":1,"trigger":0}, 13 | {"id":11,"list":[{"code":231,"indent":0,"parameters":[1,"Nemesi",0,0,0,50,100,100,255,0]},{"code":250,"indent":0,"parameters":[{"name":"Darkness4","volume":90,"pitch":100,"pan":0}]},{"code":0,"indent":0,"parameters":[]}],"name":"Nemesi","switchId":1,"trigger":0}, 14 | {"id":12,"list":[{"code":231,"indent":0,"parameters":[1,"Tanato",0,0,180,50,100,100,255,0]},{"code":250,"indent":0,"parameters":[{"name":"Thunder6","volume":90,"pitch":100,"pan":0}]},{"code":0,"indent":0,"parameters":[]}],"name":"Tanato","switchId":1,"trigger":0}, 15 | {"id":13,"list":[{"code":231,"indent":0,"parameters":[1,"Astreo",0,0,150,120,100,100,255,0]},{"code":250,"indent":0,"parameters":[{"name":"Skill3","volume":90,"pitch":100,"pan":0}]},{"code":0,"indent":0,"parameters":[]}],"name":"Astreo","switchId":1,"trigger":0}, 16 | {"id":14,"list":[{"code":231,"indent":0,"parameters":[1,"Crono",0,0,130,100,100,100,255,0]},{"code":250,"indent":0,"parameters":[{"name":"Monster3","volume":90,"pitch":100,"pan":0}]},{"code":0,"indent":0,"parameters":[]}],"name":"Crono","switchId":1,"trigger":0}, 17 | {"id":15,"list":[{"code":231,"indent":0,"parameters":[1,"Chaos",0,0,30,0,100,100,255,0]},{"code":250,"indent":0,"parameters":[{"name":"Darkness1","volume":90,"pitch":100,"pan":0}]},{"code":0,"indent":0,"parameters":[]}],"name":"Chaos","switchId":1,"trigger":0}, 18 | {"id":16,"list":[{"code":129,"indent":0,"parameters":[1,0,false]},{"code":129,"indent":0,"parameters":[2,0,false]},{"code":129,"indent":0,"parameters":[3,0,false]},{"code":129,"indent":0,"parameters":[4,0,false]},{"code":129,"indent":0,"parameters":[5,0,false]},{"code":129,"indent":0,"parameters":[6,0,false]},{"code":129,"indent":0,"parameters":[8,0,false]},{"code":0,"indent":0,"parameters":[]}],"name":"Aggiungi Party","switchId":1,"trigger":0}, 19 | {"id":17,"list":[{"code":111,"indent":0,"parameters":[0,212,0]},{"code":129,"indent":1,"parameters":[10,0,false]},{"code":0,"indent":1,"parameters":[]},{"code":412,"indent":0,"parameters":[]},{"code":111,"indent":0,"parameters":[0,233,0]},{"code":129,"indent":1,"parameters":[13,0,false]},{"code":0,"indent":1,"parameters":[]},{"code":412,"indent":0,"parameters":[]},{"code":0,"indent":0,"parameters":[]}],"name":"Aggiungi Party Extra","switchId":1,"trigger":0}, 20 | {"id":18,"list":[{"code":121,"indent":0,"parameters":[191,191,1]},{"code":121,"indent":0,"parameters":[192,192,1]},{"code":121,"indent":0,"parameters":[193,193,1]},{"code":121,"indent":0,"parameters":[194,194,1]},{"code":121,"indent":0,"parameters":[195,195,1]},{"code":122,"indent":0,"parameters":[25,25,0,0,0]},{"code":0,"indent":0,"parameters":[]}],"name":"Reset Pulsanti Emera","switchId":1,"trigger":0}, 21 | {"id":19,"list":[{"code":0,"indent":0,"parameters":[]}],"name":"","switchId":1,"trigger":0}, 22 | {"id":20,"list":[{"code":0,"indent":0,"parameters":[]}],"name":"","switchId":1,"trigger":0}, 23 | {"id":21,"list":[{"code":0,"indent":0,"parameters":[]}],"name":"","switchId":1,"trigger":0}, 24 | {"id":22,"list":[{"code":0,"indent":0,"parameters":[]}],"name":"","switchId":1,"trigger":0}, 25 | {"id":23,"list":[{"code":111,"indent":0,"parameters":[0,87,0]},{"code":101,"indent":1,"parameters":["Nature",7,0,2]},{"code":401,"indent":1,"parameters":["ATTENZIONE! Una volta attivata questa"]},{"code":401,"indent":1,"parameters":["abilità verrai trasportato sull'Isola e"]},{"code":401,"indent":1,"parameters":["non sarà possibile tornare su Gaya fino al"]},{"code":401,"indent":1,"parameters":["completamento della storia. "]},{"code":101,"indent":1,"parameters":["Nature",7,0,2]},{"code":401,"indent":1,"parameters":["Prima di continuare assicurati che la tua"]},{"code":401,"indent":1,"parameters":["squadra sia sufficientemente preparata e"]},{"code":401,"indent":1,"parameters":["cerca di concludere tutte le attività"]},{"code":401,"indent":1,"parameters":["secondarie lasciate in sospeso."]},{"code":101,"indent":1,"parameters":["Nature",7,0,2]},{"code":401,"indent":1,"parameters":["Sei pronto per il viaggio?"]},{"code":102,"indent":1,"parameters":[["Si","No"],1,0,2,0]},{"code":402,"indent":1,"parameters":[0,"Si"]},{"code":101,"indent":2,"parameters":["Actor1",0,0,2]},{"code":401,"indent":2,"parameters":["Allora procediamo."]},{"code":101,"indent":2,"parameters":["People6",1,0,2]},{"code":401,"indent":2,"parameters":["Vi avevo promesso che sarei venuto con voi"]},{"code":401,"indent":2,"parameters":["quando sareste stati pronti per partire ed"]},{"code":401,"indent":2,"parameters":["eccomi qua."]},{"code":129,"indent":2,"parameters":[7,0,false]},{"code":101,"indent":2,"parameters":["Actor1",0,0,2]},{"code":401,"indent":2,"parameters":["Bene, ora che la squadra è al completo "]},{"code":401,"indent":2,"parameters":["possiamo partire per un viaggio senza"]},{"code":401,"indent":2,"parameters":["ritorno."]},{"code":0,"indent":2,"parameters":[]},{"code":402,"indent":1,"parameters":[1,"No"]},{"code":115,"indent":2,"parameters":[]},{"code":0,"indent":2,"parameters":[]},{"code":404,"indent":1,"parameters":[]},{"code":0,"indent":1,"parameters":[]},{"code":412,"indent":0,"parameters":[]},{"code":201,"indent":0,"parameters":[0,1,6,6,2,0]},{"code":0,"indent":0,"parameters":[]}],"name":"Teletrasporto Isola","switchId":1,"trigger":0}, 26 | {"id":24,"list":[{"code":231,"indent":0,"parameters":[1,"Hyperion",0,0,40,50,100,100,255,0]},{"code":250,"indent":0,"parameters":[{"name":"Starlight","volume":90,"pitch":100,"pan":0}]},{"code":0,"indent":0,"parameters":[]}],"name":"Hyperion","switchId":1,"trigger":0} 27 | ] -------------------------------------------------------------------------------- /dialogs_translator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import copy 3 | import json 4 | import os 5 | import sys 6 | import time 7 | 8 | from googletrans import Translator # pip install googletrans==4.0.0rc1 9 | 10 | from print_neatly import print_neatly 11 | 12 | 13 | def translate(file_path, tr, src='it', dst='en', verbose=False, max_retries=5): 14 | 15 | def translate_sentence(text): 16 | target = text 17 | translation = tr.translate(target, src=src, dest=dst).text 18 | if target[0].isalpha() and translation[0].isalpha and not target[0].isupper(): 19 | translation = translation[0].lower() + translation[1:] 20 | text = translation 21 | if verbose: 22 | print(target, '->', translation) 23 | return text 24 | 25 | def try_translate_sentence(text): 26 | try: 27 | return (translate_sentence(text), True) 28 | except: 29 | for _ in range(max_retries): 30 | try: 31 | time.sleep(1) 32 | return (translate_sentence(text), True) 33 | except: 34 | pass 35 | return (text, False) 36 | 37 | translations = 0 38 | with open(file_path, 'r', encoding='utf-8-sig') as datafile: 39 | data = json.load(datafile) 40 | num_events = len([e for e in data["events"] if e is not None]) 41 | i = 0 42 | for events in data["events"]: 43 | if events is not None: 44 | print('{}: {}/{}'.format(file_path, i+1, num_events)) 45 | i += 1 46 | for pages in events['pages']: 47 | for list in pages['list']: 48 | 49 | # Plain text (ex: ["plain text"]) 50 | if list['code'] == 401: 51 | # null or empty string check 52 | if not list['parameters'][0]: 53 | continue 54 | # translate 55 | list['parameters'][0], success = try_translate_sentence(list['parameters'][0]) 56 | if not success: 57 | print('Anomaly plain text: {}'.format(list['parameters'][0])) 58 | else: 59 | translations += 1 60 | 61 | # Choices (ex: [["yes", "no"], 1, 0, 2, 0]) 62 | elif list['code'] == 102: 63 | # null or empty list check 64 | if not list['parameters'][0]: 65 | continue 66 | # translate list 67 | for j, choice in enumerate(list['parameters'][0]): 68 | # null or empty string check 69 | if not choice: 70 | continue 71 | # translate 72 | list['parameters'][0][j], success = try_translate_sentence(choice) 73 | if not success: 74 | print('Anomaly choices: {}'.format(choice)) 75 | else: 76 | translations += 1 77 | 78 | # Choices (answer) (ex: [0, "yes"]) 79 | elif list['code'] == 402: 80 | # invalid length null or empty string check 81 | if len(list['parameters']) != 2 or not list['parameters'][1]: 82 | print('Anomaly choices (answer) - Unexpected 402 Code: {}'.format(list['parameters'])) 83 | continue 84 | # translate 85 | list['parameters'][1], success = try_translate_sentence(list['parameters'][1]) 86 | if not success: 87 | print('Anomaly choices (answer): {}'.format(list['parameters'][1])) 88 | else: 89 | translations += 1 90 | return data, translations 91 | 92 | 93 | def translate_neatly(file_path, tr, src='it', dst='en', verbose=False, max_len=40, max_retries=5): 94 | 95 | def translate_sentence(text): 96 | target = text 97 | translation = tr.translate(target, src=src, dest=dst).text 98 | if target[0].isalpha() and translation[0].isalpha and not target[0].isupper(): 99 | translation = translation[0].lower() + translation[1:] 100 | text = translation 101 | return text 102 | 103 | def try_translate_sentence(text): 104 | try: 105 | return (translate_sentence(text), True) 106 | except: 107 | for _ in range(max_retries): 108 | try: 109 | time.sleep(1) 110 | return (translate_sentence(text), True) 111 | except: 112 | pass 113 | return (text, False) 114 | 115 | translations = 0 116 | with open(file_path, 'r', encoding='utf-8-sig') as datafile: 117 | data = json.load(datafile) 118 | num_events = len([e for e in data["events"] if e is not None]) 119 | i = 0 120 | for events in data["events"]: 121 | if events is not None: 122 | print('{}: {}/{}'.format(file_path, i+1, num_events)) 123 | i += 1 124 | for pages in events['pages']: 125 | len_list = len(pages['list']) 126 | list_it = 0 127 | while list_it < len_list: 128 | # 102 Choices (dont nestly translate) (ex: [["yes", "no"], 1, 0, 2, 0]) 129 | if pages['list'][list_it]['code'] == 102: 130 | # null or empty list check 131 | if not pages['list'][list_it]['parameters'][0]: 132 | list_it += 1 133 | continue 134 | # translate list 135 | for j, choice in enumerate(pages['list'][list_it]['parameters'][0]): 136 | # null or empty string check 137 | if not choice: 138 | print('Anomaly choices - Unexpected 102 code: {}'.format(choice)) 139 | continue 140 | # translate 141 | pages['list'][list_it]['parameters'][0][j], success = try_translate_sentence(choice) 142 | if not success: 143 | print('Anomaly choices: {}'.format(choice)) 144 | else: 145 | translations += 1 146 | list_it += 1 147 | 148 | # 402 Choices (answer) (dont nestly translate) (ex: [0, "yes"]) 149 | elif pages['list'][list_it]['code'] == 402: 150 | # invalid length null or empty string check 151 | if len(pages['list'][list_it]['parameters']) != 2 or not pages['list'][list_it]['parameters'][1]: 152 | print('Anomaly choices (answer) - Unexpected 402 Code: {}'.format(pages['list'][list_it]['parameters'])) 153 | list_it += 1 154 | continue 155 | # translate 156 | pages['list'][list_it]['parameters'][1], success = try_translate_sentence(pages['list'][list_it]['parameters'][1]) 157 | if not success: 158 | print('Anomaly choices (answer): {}'.format(pages['list'][list_it]['parameters'][1])) 159 | else: 160 | translations += 1 161 | list_it += 1 162 | 163 | # 401 Plain text (to nestly translate) (ex: ["plain text"]) 164 | elif pages['list'][list_it]['code'] == 401: 165 | list_it_2 = list_it + 1 166 | text = copy.deepcopy(pages['list'][list_it]['parameters']) 167 | while pages['list'][list_it_2]['code'] == 401: 168 | text.append(pages['list'][list_it_2]['parameters'][0]) 169 | list_it_2 += 1 170 | text = ' '.join(text) 171 | # empty string check 172 | if not text: 173 | list_it = list_it_2 174 | continue 175 | # translate 176 | text_tr, success = try_translate_sentence(text) 177 | if (not success) or (text_tr is None): 178 | print('Anomaly: {}'.format(text)) 179 | else: 180 | try: 181 | text_neat = print_neatly(text_tr, max_len) 182 | except: 183 | text_neat = text_tr 184 | for text_it, j in enumerate(range(list_it, list_it_2)): 185 | translations += 1 186 | if text_it >= len(text_neat): # translated text is one row shorter 187 | text_neat.append("") 188 | if verbose: 189 | print(pages['list'][j]['parameters'][0], "->", text_neat[text_it]) 190 | pages['list'][j]['parameters'][0] = text_neat[text_it] 191 | list_it = list_it_2 192 | else: 193 | list_it += 1 194 | return data, translations 195 | 196 | 197 | def translate_neatly_common_events(file_path, tr, src='it', dst='en', verbose=False, max_len=55, max_retries=5): 198 | 199 | def translate_sentence(text): 200 | target = text 201 | translation = tr.translate(target, src=src, dest=dst).text 202 | if target[0].isalpha() and translation[0].isalpha and not target[0].isupper(): 203 | translation = translation[0].lower() + translation[1:] 204 | text = translation 205 | return text 206 | 207 | translations = 0 208 | with open(file_path, 'r', encoding='utf-8-sig') as datafile: 209 | data = json.load(datafile) 210 | num_ids = len([e for e in data if e is not None]) 211 | i = 0 212 | for d in data: 213 | if d is not None: 214 | print('{}: {}/{}'.format(file_path, i+1, num_ids)) 215 | i += 1 216 | list_it = 0 217 | len_list = len(d['list']) 218 | while list_it < len_list: 219 | if 'code' in d['list'][list_it].keys() and d['list'][list_it]['code'] == 401: 220 | list_it_2 = list_it + 1 221 | text = copy.deepcopy(d['list'][list_it]['parameters']) 222 | while 'code' in d['list'][list_it].keys() and d['list'][list_it_2]['code'] == 401: 223 | text.append(d['list'][list_it_2]['parameters'][0]) 224 | list_it_2 += 1 225 | text = ' '.join(text) 226 | # empty string check 227 | if not text: 228 | list_it = list_it_2 229 | continue 230 | text_tr = None 231 | try: 232 | text_tr = translate_sentence(text) 233 | except: 234 | for _ in range(max_retries): 235 | try: 236 | time.sleep(1) 237 | text_tr = translate_sentence(text) 238 | except: 239 | pass 240 | if text_tr is not None: 241 | break 242 | if text_tr is None: 243 | print('Anomaly: {}'.format(text)) 244 | else: 245 | try: 246 | text_neat = print_neatly(text_tr, max_len) 247 | except: 248 | text_neat = text_tr 249 | for text_it, j in enumerate(range(list_it, list_it_2)): 250 | translations += 1 251 | if text_it >= len(text_neat): 252 | text_neat.append("") 253 | if verbose: 254 | print(d['list'][j]['parameters'][0], "->", text_neat[text_it]) 255 | d['list'][j]['parameters'][0] = text_neat[text_it] 256 | list_it = list_it_2 257 | else: 258 | list_it += 1 259 | return data, translations 260 | 261 | 262 | # usage: python dialogs_translator.py --print_neatly --source_lang it --dest_lang en 263 | if __name__ == '__main__': 264 | ap = argparse.ArgumentParser() 265 | ap.add_argument("-i", "--input_folder", type=str, default="dialogs") 266 | ap.add_argument("-sl", "--source_lang", type=str, default="it") 267 | ap.add_argument("-dl", "--dest_lang", type=str, default="en") 268 | ap.add_argument("-v", "--verbose", action="store_true", default=False) 269 | ap.add_argument("-nf", "--no_format", action="store_true", default=False) 270 | ap.add_argument("-pn", "--print_neatly", action="store_true", default=False) 271 | ap.add_argument("-ml", "--max_len", type=int, default=44) 272 | ap.add_argument("-mr", "--max_retries", type=int, default=10) 273 | args = ap.parse_args() 274 | dest_folder = args.input_folder + '_' + args.dest_lang 275 | translations = 0 276 | if not os.path.exists(dest_folder): 277 | os.makedirs(dest_folder) 278 | for file in os.listdir(args.input_folder): 279 | file_path = os.path.join(args.input_folder, file) 280 | if os.path.isfile(os.path.join(dest_folder, file)): 281 | print('skipped file {} because it has already been translated'.format(file_path)) 282 | continue 283 | if file.endswith('.json'): 284 | print('translating file: {}'.format(file_path)) 285 | if file.startswith('Map'): 286 | if args.print_neatly: 287 | new_data, t = translate_neatly(file_path, tr=Translator(), max_len=args.max_len, 288 | src=args.source_lang, dst=args.dest_lang, verbose=args.verbose, 289 | max_retries=args.max_retries) 290 | else: 291 | new_data, t = translate(file_path, tr=Translator(), 292 | src=args.source_lang, dst=args.dest_lang, verbose=args.verbose, 293 | max_retries=args.max_retries) 294 | elif file.startswith('CommonEvents'): 295 | new_data, t = translate_neatly_common_events(file_path, tr=Translator(), max_len=args.max_len, 296 | src=args.source_lang, dst=args.dest_lang, verbose=args.verbose, 297 | max_retries=args.max_retries) 298 | translations += t 299 | new_file = os.path.join(dest_folder, file) 300 | with open(new_file, 'w', encoding='utf-8') as f: 301 | if not args.no_format: 302 | json.dump(new_data, f, indent=4, ensure_ascii=False) 303 | else: 304 | json.dump(new_data, f, ensure_ascii=False) 305 | print('\ndone! translated in total {} dialog windows'.format(translations)) 306 | -------------------------------------------------------------------------------- /images/en1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davide97l/rpgmaker-mv-translator/f4ff2a146b72edbd61ba9259be95e16172c6bbfd/images/en1.jpg -------------------------------------------------------------------------------- /images/en2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davide97l/rpgmaker-mv-translator/f4ff2a146b72edbd61ba9259be95e16172c6bbfd/images/en2.jpg -------------------------------------------------------------------------------- /images/ita1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davide97l/rpgmaker-mv-translator/f4ff2a146b72edbd61ba9259be95e16172c6bbfd/images/ita1.jpg -------------------------------------------------------------------------------- /images/ita2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davide97l/rpgmaker-mv-translator/f4ff2a146b72edbd61ba9259be95e16172c6bbfd/images/ita2.jpg -------------------------------------------------------------------------------- /objects/Actors.json: -------------------------------------------------------------------------------- 1 | [ 2 | null, 3 | {"id":1,"battlerName":"Actor1_1","characterIndex":0,"characterName":"Actor1","classId":1,"equips":[0,0,0,0,0],"faceIndex":0,"faceName":"Actor1","traits":[{"code":51,"dataId":14,"value":1},{"code":51,"dataId":15,"value":1}],"initialLevel":1,"maxLevel":99,"name":"Destiny","nickname":"","note":"","profile":"Un ragazzo misterioso che non ha ricordi del suo\npassato."}, 4 | {"id":2,"battlerName":"Actor2_8","characterIndex":7,"characterName":"Actor2","classId":2,"equips":[0,0,0,0,61],"faceIndex":7,"faceName":"Actor2","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":1,"maxLevel":99,"name":"Nashira","nickname":"","note":"","profile":"Principessa di Pandora. Aumenta del 50% l'effetto il\nrecupero di HP causato da Magia Bianca e Oggetti."}, 5 | {"id":3,"battlerName":"Actor3_4","characterIndex":3,"characterName":"Actor3","classId":3,"equips":[45,0,0,0,60],"faceIndex":3,"faceName":"Actor3","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":6,"maxLevel":99,"name":"Bellatrix","nickname":"","note":"","profile":"Paladina di Pandora. Resiste contro Buio, rigenera il\n5% degli HP e degli AP ogni turno."}, 6 | {"id":4,"battlerName":"Actor1_7","characterIndex":6,"characterName":"Actor1","classId":4,"equips":[63,0,0,0,47],"faceIndex":6,"faceName":"Actor1","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":3,"maxLevel":99,"name":"Sirius","nickname":"","note":"\n","profile":"Capo dei ribelli del gruppo HOPE. 10% possibilità di\ncontrattacco e 150% effetto Guardia."}, 7 | {"id":5,"battlerName":"Actor3_8","characterIndex":7,"characterName":"Actor3","classId":5,"equips":[39,0,0,0,0],"faceIndex":7,"faceName":"Actor3","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":5,"maxLevel":99,"name":"Dubhe","nickname":"","note":"","profile":"Una bambina che ha perso entrambi i genitori. Riduzione\ncossto AP del 20%."}, 8 | {"id":6,"battlerName":"Actor2_1","characterIndex":0,"characterName":"Actor2","classId":6,"equips":[28,0,0,0,51],"faceIndex":0,"faceName":"Actor2","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":10,"maxLevel":99,"name":"Kuma","nickname":"","note":"","profile":"Cacciatore di taglie errante. 10% possibilità di colpo\ncritico."}, 9 | {"id":7,"battlerName":"Shenlong","characterIndex":3,"characterName":"People8","classId":14,"equips":[87,127,20,127,74],"faceIndex":1,"faceName":"People6","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1},{"code":61,"dataId":0,"value":1},{"code":43,"dataId":200,"value":1}],"initialLevel":50,"maxLevel":99,"name":"Shenlong","nickname":"","note":"","profile":"Guerriero leggendario considerato il migliore tra gli\nallievi di Guan Yu."}, 10 | {"id":8,"battlerName":"Actor1_3","characterIndex":2,"characterName":"Actor1","classId":8,"equips":[76,0,0,0,49],"faceIndex":2,"faceName":"Actor1","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":25,"maxLevel":99,"name":"Cesare","nickname":"","note":"","profile":"Non utilizza gli AP ma le sue abilità hanno un tempo di\nrecupero. Immune a Confusione e Possessione."}, 11 | {"id":9,"battlerName":"Actor1_5","characterIndex":4,"characterName":"Actor1","classId":7,"equips":[69,0,0,0,47],"faceIndex":4,"faceName":"Actor1","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1}],"initialLevel":3,"maxLevel":99,"name":"Augusto","nickname":"","note":"","profile":"Esperto di armi del gruppo HOPE, 50% possibilità di\neffettuare due azioni per turno."}, 12 | {"id":10,"battlerName":"SF_Actor3_8","characterIndex":7,"characterName":"SF_Actor3","classId":9,"equips":[23,0,0,23,56],"faceIndex":7,"faceName":"SF_Actor3","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":15,"maxLevel":99,"name":"Yuan","nickname":"","note":"","profile":"Misteriosa assassina dei Coltelli volanti. \nEffettua 2 attacchi normali extra."}, 13 | {"id":11,"battlerName":"","characterIndex":0,"characterName":"","classId":11,"equips":[0,0,0,0,0],"faceIndex":0,"faceName":"Guardians","traits":[{"code":11,"dataId":8,"value":0},{"code":11,"dataId":1,"value":0},{"code":14,"dataId":9,"value":1},{"code":14,"dataId":8,"value":1},{"code":14,"dataId":7,"value":1},{"code":14,"dataId":5,"value":1},{"code":14,"dataId":6,"value":1},{"code":14,"dataId":10,"value":1}],"initialLevel":10,"maxLevel":99,"name":"Astreo","nickname":"","note":"\n\n","profile":""}, 14 | {"id":12,"battlerName":"","characterIndex":0,"characterName":"","classId":11,"equips":[0,0,0,0,0],"faceIndex":1,"faceName":"Guardians","traits":[{"code":11,"dataId":2,"value":0},{"code":11,"dataId":6,"value":0},{"code":14,"dataId":9,"value":1},{"code":14,"dataId":8,"value":1},{"code":14,"dataId":7,"value":1},{"code":14,"dataId":5,"value":1},{"code":14,"dataId":6,"value":1},{"code":14,"dataId":10,"value":1}],"initialLevel":15,"maxLevel":99,"name":"Crono","nickname":"","note":"\n\n","profile":""}, 15 | {"id":13,"battlerName":"Rea","characterIndex":5,"characterName":"altri","classId":12,"equips":[47,0,0,0,73],"faceIndex":1,"faceName":"Actor4","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":15,"maxLevel":99,"name":"Rea","nickname":"","note":"","profile":"Mutaforma capace di assumere varie sembianze."}, 16 | {"id":14,"battlerName":"Genio","characterIndex":4,"characterName":"altri","classId":11,"equips":[0,0,0,0,0],"faceIndex":4,"faceName":"Actor4","traits":[{"code":11,"dataId":2,"value":0},{"code":11,"dataId":6,"value":0},{"code":11,"dataId":3,"value":0},{"code":11,"dataId":7,"value":0},{"code":11,"dataId":5,"value":0},{"code":11,"dataId":4,"value":0},{"code":14,"dataId":9,"value":1},{"code":14,"dataId":8,"value":1},{"code":14,"dataId":7,"value":1},{"code":14,"dataId":5,"value":1},{"code":14,"dataId":6,"value":1},{"code":14,"dataId":10,"value":1},{"code":14,"dataId":4,"value":1},{"code":14,"dataId":19,"value":1},{"code":14,"dataId":18,"value":1},{"code":14,"dataId":20,"value":1}],"initialLevel":80,"maxLevel":99,"name":"Meng","nickname":"","note":"\n","profile":""}, 17 | {"id":15,"battlerName":"Tianlong","characterIndex":6,"characterName":"altri","classId":10,"equips":[56,119,15,26,58],"faceIndex":5,"faceName":"Actor4","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1},{"code":22,"dataId":7,"value":0.05}],"initialLevel":30,"maxLevel":99,"name":"Tian","nickname":"","note":"","profile":"Molte abilità di questo personaggio consumano HP invece \ndegli AP. Rigenera il 10% degli HP ogni turno."}, 18 | {"id":16,"battlerName":"Lesath","characterIndex":0,"characterName":"People3","classId":5,"equips":[42,116,20,46,69],"faceIndex":0,"faceName":"People3","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1},{"code":43,"dataId":172,"value":1},{"code":41,"dataId":5,"value":1}],"initialLevel":75,"maxLevel":99,"name":"Lesath","nickname":"","note":"","profile":"Re Lesath di Asteria."}, 19 | {"id":17,"battlerName":"SF_Actor3_1","characterIndex":2,"characterName":"Actor3","classId":13,"equips":[0,0,21,41,56],"faceIndex":2,"faceName":"Actor3","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1}],"initialLevel":40,"maxLevel":99,"name":"Guan Yu","nickname":"","note":"","profile":"Grande Maestro Guan Yu del villaggio di Chiyou, non\nutilizza alcuna arma in combattimento."}, 20 | {"id":18,"battlerName":"Actor1_8","characterIndex":7,"characterName":"Actor1","classId":8,"equips":[80,0,19,35,85],"faceIndex":7,"faceName":"Actor1","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1}],"initialLevel":50,"maxLevel":99,"name":"Minerva","nickname":"","note":"","profile":"Esperto di armi del gruppo HOPE"}, 21 | {"id":19,"battlerName":"Yomi","characterIndex":5,"characterName":"People8","classId":15,"equips":[91,127,19,127,75],"faceIndex":3,"faceName":"People6","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1},{"code":61,"dataId":0,"value":1}],"initialLevel":99,"maxLevel":99,"name":"Yomi","nickname":"","note":"","profile":"Capo del Clan dei Coltelli Volanti"}, 22 | {"id":20,"battlerName":"Actor2_8","characterIndex":7,"characterName":"Actor2","classId":2,"equips":[0,0,0,0,0],"faceIndex":7,"faceName":"Actor2","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":99,"maxLevel":99,"name":"Nashira","nickname":"","note":"","profile":"Principessa di Pandora. Aumenta del 50% l'effetto il\nrecupero di HP causato da Magia Bianca e Oggetti."}, 23 | {"id":21,"battlerName":"Actor3_4","characterIndex":3,"characterName":"Actor3","classId":3,"equips":[0,0,0,0,0],"faceIndex":3,"faceName":"Actor3","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":99,"maxLevel":99,"name":"Bellatrix","nickname":"","note":"","profile":"Paladina di Pandora. Resiste contro Buio, rigenera il\n5% degli HP e degli AP ogni turno."}, 24 | {"id":22,"battlerName":"Actor1_7","characterIndex":6,"characterName":"Actor1","classId":4,"equips":[0,0,0,0,0],"faceIndex":6,"faceName":"Actor1","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":99,"maxLevel":99,"name":"Sirius","nickname":"","note":"\n","profile":"Capo dei ribelli del gruppo HOPE. 10% possibilità di\ncontrattacco e 150% effetto Guardia."}, 25 | {"id":23,"battlerName":"Actor3_8","characterIndex":7,"characterName":"Actor3","classId":5,"equips":[0,0,0,0,0],"faceIndex":7,"faceName":"Actor3","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":99,"maxLevel":99,"name":"Dubhe","nickname":"","note":"","profile":"Una bambina che ha perso entrambi i genitori. Riduzione\ncossto AP del 20%."}, 26 | {"id":24,"battlerName":"Actor2_1","characterIndex":0,"characterName":"Actor2","classId":6,"equips":[0,0,0,0,0],"faceIndex":0,"faceName":"Actor2","traits":[{"code":51,"dataId":14,"value":1}],"initialLevel":99,"maxLevel":99,"name":"Kuma","nickname":"","note":"","profile":"Cacciatore di taglie errante. 10% possibilità di colpo\ncritico."}, 27 | {"id":25,"battlerName":"","characterIndex":0,"characterName":"","classId":11,"equips":[0,0,0,0,0],"faceIndex":3,"faceName":"Guardians","traits":[{"code":14,"dataId":9,"value":1},{"code":14,"dataId":8,"value":1},{"code":14,"dataId":7,"value":1},{"code":14,"dataId":5,"value":1},{"code":14,"dataId":6,"value":1},{"code":14,"dataId":10,"value":1}],"initialLevel":70,"maxLevel":99,"name":"Mech HOPE","nickname":"","note":"\n\n","profile":""}, 28 | {"id":26,"battlerName":"","characterIndex":0,"characterName":"","classId":11,"equips":[0,0,0,0,0],"faceIndex":5,"faceName":"Guardians","traits":[{"code":11,"dataId":1,"value":0},{"code":14,"dataId":9,"value":1},{"code":14,"dataId":8,"value":1},{"code":14,"dataId":7,"value":1},{"code":14,"dataId":5,"value":1},{"code":14,"dataId":6,"value":1},{"code":14,"dataId":10,"value":1}],"initialLevel":40,"maxLevel":99,"name":"Nemesi","nickname":"","note":"\n\n","profile":""}, 29 | {"id":27,"battlerName":"","characterIndex":0,"characterName":"","classId":11,"equips":[0,0,0,0,0],"faceIndex":6,"faceName":"Guardians","traits":[{"code":11,"dataId":1,"value":0},{"code":11,"dataId":8,"value":0},{"code":14,"dataId":9,"value":1},{"code":14,"dataId":8,"value":1},{"code":14,"dataId":7,"value":1},{"code":14,"dataId":5,"value":1},{"code":14,"dataId":6,"value":1},{"code":14,"dataId":10,"value":1}],"initialLevel":75,"maxLevel":99,"name":"Hades","nickname":"","note":"\n\n","profile":""}, 30 | {"id":28,"battlerName":"","characterIndex":0,"characterName":"","classId":16,"equips":[0,0,0,0,0],"faceIndex":7,"faceName":"Nature","traits":[{"code":11,"dataId":1,"value":0},{"code":11,"dataId":8,"value":0},{"code":14,"dataId":9,"value":1},{"code":14,"dataId":8,"value":1},{"code":14,"dataId":7,"value":1},{"code":14,"dataId":5,"value":1},{"code":14,"dataId":6,"value":1},{"code":14,"dataId":10,"value":1},{"code":21,"dataId":0,"value":10},{"code":21,"dataId":1,"value":10},{"code":22,"dataId":7,"value":10},{"code":22,"dataId":8,"value":10}],"initialLevel":99,"maxLevel":99,"name":"Hyperion","nickname":"","note":"\n\nImmune a qualsiasi danno o status","profile":""}, 31 | {"id":29,"battlerName":"Actor1_1","characterIndex":0,"characterName":"Actor1","classId":16,"equips":[0,0,0,0,0],"faceIndex":0,"faceName":"Actor1","traits":[{"code":22,"dataId":1,"value":10}],"initialLevel":99,"maxLevel":99,"name":"Destiny (Hyperion)","nickname":"","note":"\n","profile":""}, 32 | {"id":30,"battlerName":"","characterIndex":0,"characterName":"Actor3","classId":1,"equips":[0,0,0,0,0],"faceIndex":0,"faceName":"Actor3","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1}],"initialLevel":1,"maxLevel":99,"name":"Mira","nickname":"","note":"","profile":"Capo di VITA."}, 33 | {"id":31,"battlerName":"Yinglong","characterIndex":2,"characterName":"People8","classId":9,"equips":[19,0,20,76,70],"faceIndex":7,"faceName":"Actor4","traits":[{"code":53,"dataId":1,"value":10},{"code":53,"dataId":5,"value":10},{"code":53,"dataId":2,"value":1},{"code":53,"dataId":4,"value":1},{"code":53,"dataId":3,"value":1},{"code":22,"dataId":8,"value":0.05}],"initialLevel":50,"maxLevel":99,"name":"Yinglong","nickname":"","note":"MSHARDS LOCK 31 1\nMSHARDS LOCK 31 2\nMSHARDS LOCK 31 3\nMSHARDS LOCK 31 4\nMSHARDS LOCK 31 5\nMSHARDS LOCK 31 6","profile":""} 34 | ] -------------------------------------------------------------------------------- /objects/README.md: -------------------------------------------------------------------------------- 1 | # Objects folder 2 | 3 | Put the files you want to translate among: 4 | - `Armors.json` 5 | - `Weapons.json` 6 | - `Items.json` 7 | - `Skills.json` 8 | - `Enemies.json` 9 | - `MapInfos.json` 10 | - `Classes.json` 11 | - `States.json` 12 | -------------------------------------------------------------------------------- /objects_translator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import time 5 | 6 | from googletrans import Translator # pip install googletrans==4.0.0rc1 7 | 8 | from print_neatly import print_neatly 9 | 10 | 11 | def translate(file_path, tr, src='it', dst='en', verbose=False, max_retries=5, max_len=55): 12 | 13 | def translate_sentence(text): 14 | target = text 15 | translation = tr.translate(target, src=src, dest=dst).text 16 | if target[0].isalpha() and translation[0].isalpha and not target[0].isupper(): 17 | translation = translation[0].lower() + translation[1:] 18 | text = translation 19 | if verbose: 20 | print(target, '->', translation) 21 | return text 22 | 23 | def translate_and_check(text, remove_escape=True, neatly=False, keep_space=True): 24 | text_tr = None 25 | if remove_escape: 26 | text = text.replace('\n', ' ') 27 | try: 28 | text_tr = translate_sentence(text) 29 | except: 30 | for _ in range(max_retries): 31 | try: 32 | time.sleep(1) 33 | text_tr = translate_sentence(text) 34 | except: 35 | pass 36 | if text_tr is not None: 37 | break 38 | if text_tr is None: 39 | print('Anomaly: {}'.format(text)) 40 | return None, 0 41 | if neatly: 42 | try: 43 | text_neat = print_neatly(text_tr, max_len) 44 | if len(text_neat) > 1: 45 | text_tr = text_neat[0] + '\n' + text_neat[1] 46 | else: 47 | text_tr = text_neat[0] 48 | except: 49 | pass 50 | if keep_space: 51 | if text[0] == ' ' and text_tr[0] != ' ': 52 | text_tr = ' ' + text_tr 53 | return text_tr, 1 54 | 55 | def translate_based_on_keys(dict_or_list, keys, translations=0, remove_escape=True, neatly=False, array_translate=False): 56 | if isinstance(dict_or_list, dict): 57 | for d in dict_or_list: 58 | if isinstance(dict_or_list[d], dict) or isinstance(dict_or_list[d], list): 59 | translations += translate_based_on_keys(dict_or_list[d], keys, translations, remove_escape, neatly, array_translate) 60 | elif d in keys and len(dict_or_list[d]) > 0: 61 | tr, success = translate_and_check(dict_or_list[d], remove_escape, neatly) 62 | dict_or_list[d] = tr 63 | translations += success 64 | elif isinstance(dict_or_list, list): 65 | for i in range(len(dict_or_list)): 66 | if isinstance(dict_or_list[i], dict) or isinstance(dict_or_list[i], list): 67 | translations += translate_based_on_keys(dict_or_list[i], keys, translations, remove_escape, neatly, array_translate) 68 | elif array_translate and isinstance(dict_or_list[i], str) and len(dict_or_list[i]) > 0: 69 | tr, success = translate_and_check(dict_or_list[i], remove_escape, neatly) 70 | dict_or_list[i] = tr 71 | translations += success 72 | return translations 73 | 74 | translations = 0 75 | with open(file_path, 'r', encoding='utf-8-sig') as datafile: 76 | data = json.load(datafile) 77 | num_ids = len([e for e in data if e is not None]) 78 | i = 0 79 | 80 | if file_path.endswith('GalleryList.json'): 81 | translations += translate_based_on_keys(data, ['displayName', 'hint', 'stageText', 'sceneText', 'text'], translations) 82 | 83 | elif file_path.endswith('RubiList.json'): 84 | translations += translate_based_on_keys(data, [], translations, array_translate=True) 85 | 86 | else: 87 | for d in data: 88 | if d is not None: 89 | print('{}: {}/{}'.format(file_path, i+1, num_ids)) 90 | i += 1 91 | if 'name' in d.keys() and len(d['name']) > 0: 92 | name_tr, success = translate_and_check(d['name'], remove_escape=True, neatly=False) 93 | d['name'] = name_tr 94 | translations += success 95 | if 'description' in d.keys() and len(d['description']) > 0: 96 | desc_tr, success = translate_and_check(d['description'], remove_escape=True, neatly=True) 97 | d['description'] = desc_tr 98 | translations += success 99 | if 'profile' in d.keys() and len(d['profile']) > 0: 100 | prf_tr, success = translate_and_check(d['profile'], remove_escape=True, neatly=True) 101 | d['profile'] = prf_tr 102 | translations += success 103 | for m in range(1, 5): 104 | message = 'message' + str(m) 105 | if message in d.keys() and len(d[message]) > 0: 106 | message_tr, success = translate_and_check(d[message], remove_escape=False, neatly=False) 107 | d[message] = message_tr 108 | translations += success 109 | 110 | return data, translations 111 | 112 | 113 | # usage: python objects_translator.py --source_lang it --dest_lang en 114 | if __name__ == '__main__': 115 | ap = argparse.ArgumentParser() 116 | ap.add_argument("-i", "--input_folder", type=str, default="objects") 117 | ap.add_argument("-sl", "--source_lang", type=str, default="it") 118 | ap.add_argument("-dl", "--dest_lang", type=str, default="en") 119 | ap.add_argument("-v", "--verbose", action="store_true", default=False) 120 | ap.add_argument("-nf", "--no_format", action="store_true", default=False) 121 | ap.add_argument("-ml", "--max_len", type=int, default=55) 122 | ap.add_argument("-mr", "--max_retries", type=int, default=10) 123 | args = ap.parse_args() 124 | dest_folder = args.input_folder + '_' + args.dest_lang 125 | translations = 0 126 | if not os.path.exists(dest_folder): 127 | os.makedirs(dest_folder) 128 | for file in os.listdir(args.input_folder): 129 | file_path = os.path.join(args.input_folder, file) 130 | if os.path.isfile(os.path.join(dest_folder, file)): 131 | print('skipped file {} because it has already been translated'.format(file_path)) 132 | continue 133 | if file.endswith('.json'): 134 | print('translating file: {}'.format(file_path)) 135 | new_data, t = translate(file_path, tr=Translator(), max_len=args.max_len, 136 | src=args.source_lang, dst=args.dest_lang, verbose=args.verbose, 137 | max_retries=args.max_retries) 138 | translations += t 139 | new_file = os.path.join(dest_folder, file) 140 | with open(new_file, 'w', encoding='utf-8') as f: 141 | if not args.no_format: 142 | json.dump(new_data, f, indent=4, ensure_ascii=False) 143 | else: 144 | json.dump(new_data, f, ensure_ascii=False) 145 | print('\ndone! translated in total {} strings'.format(translations)) 146 | -------------------------------------------------------------------------------- /print_neatly.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def print_neatly_optimizer(words, n, M): 5 | """ 6 | Function prints a paragraph neatly 7 | @param words : an array of words 8 | @param n : number of words in the array 9 | @param M : maximum line length 10 | """ 11 | minpenalty = [float('inf')]*(n+1) 12 | break_points = [None]*(n+1) 13 | 14 | # initialize base case 15 | minpenalty[0] = 0 16 | 17 | def compute_line_cost(extra_space, j, n): 18 | if extra_space < 0: 19 | return float('inf') 20 | elif j == n and extra_space >= 0: 21 | return 0 22 | else: 23 | return extra_space**3 24 | 25 | for j in range(1, n+1): 26 | extra_space = M + 1 27 | for i in range(j, int(max(1, j + 1 - math.ceil(M/2)))-1, -1): 28 | extra_space = extra_space - len(words[i]) - 1 29 | cur_penalty = minpenalty[i-1] + compute_line_cost(extra_space, j, n) 30 | if minpenalty[j] > cur_penalty: 31 | minpenalty[j] = cur_penalty 32 | break_points[j] = i 33 | 34 | return minpenalty, break_points 35 | 36 | 37 | def reconstruct_lines(text, j, break_points): 38 | i = break_points[j] 39 | neat_text = [] 40 | if i != 1: 41 | neat_text = reconstruct_lines(text, i-1, break_points) 42 | neat_text.append(' '.join(text[i:(j+1)])) 43 | return neat_text 44 | 45 | 46 | def print_neatly(text, M): 47 | n = len(text.split(' ')) 48 | text = ['BLANK'] + text.split(' ') 49 | min_p, p_list = print_neatly_optimizer(text, n, M) 50 | neat_text = reconstruct_lines(text, n, p_list) 51 | return neat_text 52 | 53 | 54 | # adapted from: https://github.com/samuelklam/print-neatly/blob/master/print-neatly.py 55 | if __name__ == '__main__': 56 | text = "Buffy the Vampire Slayer fans are sure to get their fix with the DVD release of the show's first season. The three-disc collection includes all 12 episodes as well as many extras. There is a collection of interviews by the show's creator Joss Whedon in which he explains his inspiration for the show as well as comments on the various cast members. Much of the same material is covered in more depth with Whedon's commentary track for the show's first two episodes that make up the Buffy the Vampire Slayer pilot. The most interesting points of Whedon's commentary come from his explanation of the learning curve he encountered shifting from blockbuster films like Toy Story to a much lower-budget television series. The first disc also includes a short interview with David Boreanaz who plays the role of Angel. Other features include the script for the pilot episodes, a trailer, a large photo gallery of publicity shots and in-depth biographies of Whedon and several of the show's stars, including Sarah Michelle Gellar, Alyson Hannigan and Nicholas Brendon." 57 | M = 40 58 | neat_text = print_neatly(text, M) 59 | print(neat_text) 60 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | googletrans~=4.0.0rc1 --------------------------------------------------------------------------------