We hacked together a small tool to extract translation files for Ren’Py games.


Ren’Py is built to support translations natively, and it comes with a handy-dandy button to easily extract translation files.

Screenshot of Ren'Py's main menu screen
Screenshot of Ren'Py's extract translations screen

The files come out looking something like:

Snippet from YDD's translation file

Note the unique strings underlined in the image. Those are identifiers that allow Ren’Py to easily substitute translations into the correct place. So usually, generating these translation files is a breeze, and the localization process, at least on the textual end, is not too bad.

However, recently, that handy-dandy “Extract Translations” button has been broken for us. We have been unable to use it to generate the files, with Ren’Py complaining with every attempt. Luckily, the “Extract Dialogue” button still works, and it contains the unique identifiers for each string. The format is simply incorrect though.

Extract Dialogue gives us a tab-delimited file.

Original tab-delimited file

Chess of Blades

The same file has been loaded into a table for easy viewing.

Loading extracted dialogue of Chess of Blades into a table for easy viewing

From Chess of Blades

We need to convert that file into something that looked like the translation files. The code snippet below is pretty messy and hard-coded, but it works, at least. We’ll upload a cleaner/better version to a public Git repo with all the random snippets that we create. Hopefully it will save others some hair-pulling.

import pandas as pd
import math

# load the dialogue file into a pandas table
# we're working with Chess of Blades here
cob_df = pd.read_csv("chess_of_blades/dialogue.tab",sep='\t',skiprows=(0),header=(0))

# define the translation folder language we want
lang = "simplified_chinese" 

# iterate through each row in the table
for _, row in cob_df.iterrows():
    filename = row['Filename'].split('/')[-1]
    with open('chess_of_blades/game/tl/'+lang+'/'+filename, 'a', encoding='utf-8') as f:
        # this try clause is for any translation text that doesn't have a character speaking
        # such as a choice menu
        try:
            if math.isnan(row['Character']):
                old_char = 'old'
                new_char = 'new'
                f.write('''
# {}:{}
translate {} strings:

    old "{}"
    new ""
    
                '''.format(row['Filename'], row['Line Number'], lang, row['Dialogue']))
        except TypeError:
            old_char = row['Character']
            new_char = old_char
            
            f.write('''
# {}:{}
translate {} {}:

    # {} "{}"
    {} ""

            '''.format(row['Filename'], row['Line Number'], lang, row['Identifier'], 
                       old_char, row['Dialogue'], new_char))