Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Support unicode in original language |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
074ae14186a04fa349ef4ae55df916d5 |
User & Date: | Beuc 2019-05-31 13:16:49 |
Context
2019-05-31
| ||
14:40 | Typo check-in: 56898245f4 user: Beuc tags: trunk | |
13:16 | Support unicode in original language check-in: 074ae14186 user: Beuc tags: trunk | |
2019-05-30
| ||
17:35 | Add contact info check-in: bf7736aaa6 user: Beuc tags: trunk | |
Changes
Changes to README.md.
︙ | ︙ | |||
23 24 25 26 27 28 29 | - Ren'Py forces you to either: display empty texts when there's no translation yet; or prefill all translations using the original language but this makes it hard to see untranslated strings. Now you can have both, as untranslated strings will be empty in your .po but filled with the original language in the Ren'Py translation files. Handle duplicates, so you can translate the same dialog line differently depending on the context. Only duplicates get an additional context marker, so as to avoid tons of fuzzy texts when e.g. you renamed a Ren'Py label. Up-to-date source references (file:line). | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | - Ren'Py forces you to either: display empty texts when there's no translation yet; or prefill all translations using the original language but this makes it hard to see untranslated strings. Now you can have both, as untranslated strings will be empty in your .po but filled with the original language in the Ren'Py translation files. Handle duplicates, so you can translate the same dialog line differently depending on the context. Only duplicates get an additional context marker, so as to avoid tons of fuzzy texts when e.g. you renamed a Ren'Py label. Up-to-date source references (file:line). Support customized Ren'Py translations (WIP): for instance .po doesn't support splitting a translation to several Ren'Py dialogs, but if you did that in Ren'Py with a customized translation block, don't translate it in the PO file, or add a `# renpy-ttk:ignore` comment in the `translate` block before your translations. ## Install and run ### From Ren'Py (GUI) Place this directory along with your other Ren'Py games, so it shows |
︙ | ︙ |
Changes to functest.sh.
1 2 3 4 5 6 | #!/bin/bash -ex projectpath=`mktemp -d` mkdir "$projectpath/game" cp -a test/input/*.rpy "$projectpath/game" | | | | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #!/bin/bash -ex projectpath=`mktemp -d` mkdir "$projectpath/game" cp -a test/input/*.rpy "$projectpath/game" renpy.sh . tl2pot "$projectpath" msgmerge -v test/input/testtl-fr_FR.po game.pot -o "$projectpath/testtl-fr_FR.po" renpy.sh . mo2tl "$projectpath" "$projectpath/testtl-fr_FR.po" french rm -f "$projectpath/game/tl/french/common.rpy" sed -i -e 's/# TODO: Translation updated at .*/# TODO: Translation updated at XXXX-XX-XX XX:XX/' "$projectpath/game/tl/french/"*.rpy diff -ru test/output_expected $projectpath/game/tl/french # TODO: check result but PO editors' reformatting makes it difficult renpy.sh . tl2po "$projectpath" french rm -f game.pot french.po rm -rf "$projectpath" |
Changes to game/cli.rpy.
|
| | | | | > > > > | > > | > > > > > > > | > > > | > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | # Call the scripts with the Ren'Py (limited) Python environment # ~/.../renpy-7.2.2-sdk/renpy.sh ~/.../renpy-ttk tl2pot ... init python: def version(): renpy.arguments.takes_no_arguments("version") print(config.name, config.version) return False renpy.arguments.register_command("version", version) def tl2pot(): ap = renpy.arguments.ArgumentParser(description="tl2pot", require_command=False) args, rest = ap.parse_known_args() import tl2pot tl2pot.tl2pot(*rest) return False renpy.arguments.register_command("tl2pot", tl2pot) def tl2po(): ap = renpy.arguments.ArgumentParser(description="tl2po", require_command=False) args, rest = ap.parse_known_args() import tl2po tl2po.tl2po(*rest) return False renpy.arguments.register_command("tl2po", tl2po) def mo2tl(): ap = renpy.arguments.ArgumentParser(description="mo2tl", require_command=False) args, rest = ap.parse_known_args() import mo2tl mo2tl.mo2tl(*rest) return False renpy.arguments.register_command("mo2tl", mo2tl) |
Changes to mo2tl.py.
︙ | ︙ | |||
20 21 22 23 24 25 26 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | < < < | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function import sys, os, fnmatch, io import re import subprocess, shutil import tempfile import rttk.run, rttk.tlparser, rttk.utf_8_sig import gettext # Doc: manual .mo test: # mkdir -p $LANG/LC_MESSAGES/ # msgfmt xxx.po -o $LANG/LC_MESSAGES/game.mo # TEXTDOMAINDIR=. gettext -s -d game "nointeract" # TEXTDOMAINDIR=. gettext -s -d game "script_abcd1234"$'\x4'"You've created a new Ren'Py game." UNESCAPE_CHARS = { 'a': '\a', 'b': '\b', 'e': '\e', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', 'v': '\v', '\\': '\\', '\'': '\'', '"': '\"', '?': '\?', } ESCAPE_CHARS = { '\a': r'\a', '\b': r'\b', '\e': r'\e', '\f': r'\f', '\n': r'\n', '\r': r'\r', '\t': r'\t', '\v': r'\v', '\\': r'\\', #'\'': r'\'', '\"': r'\"', '\?': r'\?', } def c_unescape(s): r''' Convert Python-style string for gettext look-up Like str.decode('unicode_escape') but actually support unicode characters No support for \xXX or \0xx. ''' ret = '' pos = 0 while pos < len(s): if s[pos] == '\\' and (pos+1) < len(s) and s[pos+1] in UNESCAPE_CHARS.keys(): ret += UNESCAPE_CHARS[s[pos+1]] pos += 1 else: ret += s[pos] pos += 1 return ret def c_escape(s): ''' Convert gettext result back to Python-style string Like str.encode('string_escape') but keeping non-ASCII letters as-is (no \xc3\xa9 everywhere) ''' return ''.join([ESCAPE_CHARS.get(c, c) for c in s]) def mo2tl(projectpath, mofile, renpy_target_language): if not re.match('^[a-z_]+$', renpy_target_language): raise Exception("Invalid language", renpy_target_language) # Refresh strings print("Calling Ren'Py translate to get untranslated strings") try: # Ensure Ren'Py keeps the strings order (rather than append new strings) shutil.rmtree(os.path.join(projectpath,'game','tl','pot')) except OSError: pass # using --compile otherwise Ren'Py sometimes skips half of the files rttk.run.renpy([projectpath, 'translate', 'pot', '--compile']) # Prepare msgid:untranslated_string index originals = [] for curdir, subdirs, filenames in os.walk(os.path.join(projectpath,'game','tl','pot')): for filename in fnmatch.filter(filenames, '*.rpy'): print("Parsing " + os.path.join(curdir,filename)) f = io.open(os.path.join(curdir,filename), 'r', encoding='utf-8-sig') lines = f.readlines() lines.reverse() while len(lines) > 0: originals.extend(rttk.tlparser.parse_next_block(lines)) o_blocks_index = {} o_basestr_index = {} for s in originals: |
︙ | ︙ | |||
104 105 106 107 108 109 110 111 112 113 114 115 | os.environ['LANG'] = 'en_US.UTF-8' msgdir = os.path.join(localedir, os.environ['LANG'], 'LC_MESSAGES') os.makedirs(msgdir) if mofile.endswith('.po'): pofile = mofile ret = subprocess.call(['msgfmt', pofile, '-v', '-o', os.path.join(msgdir, 'game.mo')]) if ret != 0: raise Exception("msgfmt failed") else: shutil.copy2(mofile, os.path.join(msgdir, 'game.mo')) | > | > > | > > | < < | < | 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | os.environ['LANG'] = 'en_US.UTF-8' msgdir = os.path.join(localedir, os.environ['LANG'], 'LC_MESSAGES') os.makedirs(msgdir) if mofile.endswith('.po'): pofile = mofile print(".po ->", os.path.join(msgdir, 'game.mo')) ret = subprocess.call(['msgfmt', pofile, '-v', '-o', os.path.join(msgdir, 'game.mo')]) if ret != 0: raise Exception("msgfmt failed") else: shutil.copy2(mofile, os.path.join(msgdir, 'game.mo')) translations = gettext.translation('game', localedir) class NoneOnMissingTranslation: @staticmethod def ugettext(str): return None translations.add_fallback(NoneOnMissingTranslation) for curdir, subdirs, filenames in os.walk(os.path.join(projectpath,'game','tl',renpy_target_language)): for filename in fnmatch.filter(filenames, '*.rpy'): print("Updating " + os.path.join(curdir,filename)) scriptpath = os.path.join(curdir,filename) f_in = io.open(scriptpath, 'r', encoding='utf-8-sig') lines = f_in.readlines() lines.reverse() # reverse so we can pop/append efficiently f_in.close() out = io.open(scriptpath, 'w', encoding='utf-8-sig') while len(lines) > 0: line = lines.pop() if rttk.tlparser.is_empty(line): out.write(line) elif rttk.tlparser.is_comment(line): out.write(line) elif rttk.tlparser.is_block_start(line): |
︙ | ︙ | |||
151 152 153 154 155 156 157 | msgctxt = line.lstrip().lstrip('#').strip() elif not line.startswith(' '): # end of block lines.append(line) break elif line.lstrip().startswith('old '): msgstr = rttk.tlparser.extract_base_string(line)['text'] | | | | | > | | | 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | msgctxt = line.lstrip().lstrip('#').strip() elif not line.startswith(' '): # end of block lines.append(line) break elif line.lstrip().startswith('old '): msgstr = rttk.tlparser.extract_base_string(line)['text'] lookup = c_unescape(msgstr) lookup = msgctxt+'\x04'+lookup translation = translations.ugettext(lookup) if translation is None: # no match with context, try without lookup = c_unescape(msgstr) translation = translations.ugettext(lookup) if translation is not None: translation = c_escape(translation) msgctxt = '' elif line.lstrip().startswith('new '): if translation is not None: s = rttk.tlparser.extract_base_string(line) line = line[:s['start']]+translation+line[s['end']:] translation = None else: |
︙ | ︙ | |||
198 199 200 201 202 203 204 | pass elif o_blocks_index.get(msgid, None) is None: # obsolete translate block, don't translate pass else: msgstr = o_blocks_index[msgid] msgctxt = msgid | | | | | > | | | | 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | pass elif o_blocks_index.get(msgid, None) is None: # obsolete translate block, don't translate pass else: msgstr = o_blocks_index[msgid] msgctxt = msgid lookup = c_unescape(msgstr) lookup = msgctxt+'\x04'+lookup translation = translations.ugettext(lookup) if translation is None: # no match with context, try without lookup = c_unescape(msgstr) translation = translations.ugettext(lookup) if translation is not None: translation = c_escape(translation) line = line[:s['start']]+translation+line[s['end']:] out.write(line) # Unknown else: print("Warning: format not detected:", line) out.write(line) shutil.rmtree(localedir) |
︙ | ︙ |
Changes to rttk/test_tlparser.py.
︙ | ︙ | |||
25 26 27 28 29 30 31 | import unittest import tlparser class TestTlparser(unittest.TestCase): def test_is_empty(self): | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | import unittest import tlparser class TestTlparser(unittest.TestCase): def test_is_empty(self): self.assertTrue(tlparser.is_empty(u'')) self.assertTrue(tlparser.is_empty(u'\n')) self.assertFalse(tlparser.is_empty(u'translate french start_a170b500\n')) self.assertFalse(tlparser.is_empty(u'# game/script.rpy:27')) def test_is_comment(self): self.assertTrue(tlparser.is_comment(u'#')) self.assertTrue(tlparser.is_comment(u'# game/script.rpy:27\n')) self.assertFalse(tlparser.is_comment(u' ')) self.assertFalse(tlparser.is_comment(u'e "Hello"')) self.assertFalse(tlparser.is_comment(u'translate french start_a170b500 # test\n')) def test_is_block_start(self): self.assertTrue(tlparser.is_block_start(u'translate french start_a170b500 # test\n')) def test_extract_source(self): self.assertEqual(tlparser.extract_source(u'# game/script.rpy:27\n'), u'game/script.rpy:27') def test_extract_dqstrings(self): testcase = ur''' _( 'string " character' ) "Tricky single/double '\" multiple strings 2"''' self.assertEqual(tlparser.extract_dqstrings(testcase), [{'start': 31, 'end': 74, 'text': ur'''Tricky single/double '\" multiple strings 2'''}]) testcase = ur'''_( "string \" character" ) "Tricky double/double \"' multiple strings"''' self.assertEqual(tlparser.extract_dqstrings(testcase), [{'start': 4, 'end': 23, 'text': ur'''string \" character'''}, {'start': 28, 'end': 69, 'text': ur'''Tricky double/double \"' multiple strings'''}]) def test_extract_base_string(self): self.assertEqual( tlparser.extract_base_string(u''' old "menu title"\n'''), {'start': 9, 'end': 19, 'text': u'menu title'}) def test_extract_dialog_string(self): self.assertEqual( tlparser.extract_dialog_string(u'''e "You've created a new Ren'Py game."\n'''), {'start': 3, 'end': 36, 'text': u"You've created a new Ren'Py game."}) testcase = ur''' _( 'string " character' ) "Tricky single/double '\" multiple strings 2"''' self.assertEqual(tlparser.extract_dialog_string(testcase), {'start': 31, 'end': 74, 'text': ur'''Tricky single/double '\" multiple strings 2'''}) def test_parse_next_block(self): # https://www.renpy.org/doc/html/translation.html lines = u""" # TODO: Translation updated at 2019-05-18 19:13 # game/script.rpy:27 translate pot start_a170b500: # e "You've created a new Ren'Py game." e "You've created a new Ren'Py game." """ lines = [l+"\n" for l in lines.split("\n")] lines.reverse() self.assertEqual(tlparser.parse_next_block(lines), [{ 'id': u'start_a170b500', 'source': u'game/script.rpy:27', 'text': ur"You've created a new Ren'Py game.", 'translation': None }]) lines = u""" # game/script.rpy:64 translate pot start_130610c2: # nvl clear # nvle "You use 'nvl clear' to clear the screen when that becomes necessary." nvl clear nvle "You use 'nvl clear' to clear the screen when that becomes necessary." """ lines = [l+"\n" for l in lines.split("\n")] lines.reverse() self.assertEqual(tlparser.parse_next_block(lines), [{ 'id': u'start_130610c2', 'source': u'game/script.rpy:64', 'text': ur"You use 'nvl clear' to clear the screen when that becomes necessary.", 'translation': None }]) lines = u""" translate russian tutorial_nvlmode_76b2fe88: # nvl clear nvl clear """ lines = [l+"\n" for l in lines.split("\n")] lines.reverse() self.assertEqual(tlparser.parse_next_block(lines), []) lines = u""" translate piglatin style default: # comment but not the end of the bloc font "stonecutter.ttf" """ lines = [l+"\n" for l in lines.split("\n")] lines.reverse() self.assertEqual(tlparser.parse_next_block(lines), []) lines = u""" translate piglatin python: style.default.font = "stonecutter.ttf" """ lines = [l+"\n" for l in lines.split("\n")] lines.reverse() self.assertEqual(tlparser.parse_next_block(lines), []) lines = u""" translate pot strings: # script.rpy:14 old "Eileen" new "translation1" # script.rpy:40 old "string ' character" new "translation2" """ lines = [l+"\n" for l in lines.split("\n")] lines.reverse() self.assertEqual(tlparser.parse_next_block(lines), [ {'id':None, 'source':u'script.rpy:14', 'text':u"Eileen", 'translation':u"translation1"}, {'id':None, 'source':u'script.rpy:40', 'text':u"string ' character", 'translation':u"translation2"} ]) lines = u"""\ # game/script.rpy:27 translate pot start_a170b500: # e "You've created a new Ren'Py game." e "You've created a new Ren'Py game." # game/script.rpy:29 translate pot start_a1247ef6: # "Eileen" "Once you add a story, pictures, and music, you can release it to the world!" "Eileen" "Once you add a story, pictures, and music, you can release it to the world!"\ """ lines = [l+"\n" for l in lines.split("\n")] lines.reverse() self.assertEqual(tlparser.parse_next_block(lines), [{ 'id': u'start_a170b500', 'source': u'game/script.rpy:27', 'text': ur"You've created a new Ren'Py game.", 'translation': None }]) self.assertEqual(tlparser.parse_next_block(lines), [{ 'id': u'start_a1247ef6', 'source': u'game/script.rpy:29', 'text': ur"Once you add a story, pictures, and music, you can release it to the world!", 'translation': None }]) lines = u"""\ # game/script.rpy:92 translate french start_06194c6b: # voice "path/to/file" # e "voiced text" voice "path/to/file" e "voiced text" """ lines = [l+"\n" for l in lines.split("\n")] lines.reverse() self.assertEqual(tlparser.parse_next_block(lines), [{ 'id': u'start_06194c6b', 'source': u'game/script.rpy:92', 'text': ur"voiced text", 'translation': None }]) if __name__ == '__main__': unittest.main() |
Added rttk/utf_8_sig.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | """ Python 'utf-8-sig' Codec This work similar to UTF-8 with the following changes: * On encoding/writing a UTF-8 encoded BOM will be prepended/written as the first three bytes. * On decoding/reading if the first three bytes are a UTF-8 encoded BOM, these bytes will be skipped. """ import codecs ### Codec APIs def encode(input, errors='strict'): return (codecs.BOM_UTF8 + codecs.utf_8_encode(input, errors)[0], len(input)) def decode(input, errors='strict'): prefix = 0 if input[:3] == codecs.BOM_UTF8: input = input[3:] prefix = 3 (output, consumed) = codecs.utf_8_decode(input, errors, True) return (output, consumed+prefix) class IncrementalEncoder(codecs.IncrementalEncoder): def __init__(self, errors='strict'): codecs.IncrementalEncoder.__init__(self, errors) self.first = 1 def encode(self, input, final=False): if self.first: self.first = 0 return codecs.BOM_UTF8 + codecs.utf_8_encode(input, self.errors)[0] else: return codecs.utf_8_encode(input, self.errors)[0] def reset(self): codecs.IncrementalEncoder.reset(self) self.first = 1 def getstate(self): return self.first def setstate(self, state): self.first = state class IncrementalDecoder(codecs.BufferedIncrementalDecoder): def __init__(self, errors='strict'): codecs.BufferedIncrementalDecoder.__init__(self, errors) self.first = True def _buffer_decode(self, input, errors, final): if self.first: if len(input) < 3: if codecs.BOM_UTF8.startswith(input): # not enough data to decide if this really is a BOM # => try again on the next call return (u"", 0) else: self.first = None else: self.first = None if input[:3] == codecs.BOM_UTF8: (output, consumed) = codecs.utf_8_decode(input[3:], errors, final) return (output, consumed+3) return codecs.utf_8_decode(input, errors, final) def reset(self): codecs.BufferedIncrementalDecoder.reset(self) self.first = True class StreamWriter(codecs.StreamWriter): def reset(self): codecs.StreamWriter.reset(self) try: del self.encode except AttributeError: pass def encode(self, input, errors='strict'): self.encode = codecs.utf_8_encode return encode(input, errors) class StreamReader(codecs.StreamReader): def reset(self): codecs.StreamReader.reset(self) try: del self.decode except AttributeError: pass def decode(self, input, errors='strict'): if len(input) < 3: if codecs.BOM_UTF8.startswith(input): # not enough data to decide if this is a BOM # => try again on the next call return (u"", 0) elif input[:3] == codecs.BOM_UTF8: self.decode = codecs.utf_8_decode (output, consumed) = codecs.utf_8_decode(input[3:],errors) return (output, consumed+3) # (else) no BOM present self.decode = codecs.utf_8_decode return codecs.utf_8_decode(input, errors) ### encodings module API def getregentry(): return codecs.CodecInfo( name='utf-8-sig', encode=encode, decode=decode, incrementalencoder=IncrementalEncoder, incrementaldecoder=IncrementalDecoder, streamreader=StreamReader, streamwriter=StreamWriter, ) # (The above copied from /usr/lib/python2.7/encodings/utf_8_sig.py) # Polyfill for Ren'Py: def lookup(name): if name != 'utf-8-sig': return None return getregentry() try: codecs.getdecoder('utf-8-sig') except LookupError: codecs.register(lookup) |
Changes to test/input/script.rpy.
︙ | ︙ | |||
87 88 89 90 91 92 93 94 95 96 97 | "dupmenuentry": pass "dupmenuentry": pass voice "path/to/file" e "voiced text" call dup return | > > | 87 88 89 90 91 92 93 94 95 96 97 98 99 | "dupmenuentry": pass "dupmenuentry": pass voice "path/to/file" e "voiced text" b "「unicode characters♪」" call dup return |
Changes to test/input/testtl-fr_FR.po.
1 2 3 4 | msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" | | | 1 2 3 4 5 6 7 8 9 10 11 12 | msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: \n" "PO-Revision-Date: 2019-05-31 13:44+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.2.1\n" |
︙ | ︙ | |||
55 56 57 58 59 60 61 62 63 64 65 66 67 68 | #: renpy/common/00accessibility.rpy:129 msgid "Clipboard" msgstr "" #: renpy/common/00accessibility.rpy:133 msgid "Debug" msgstr "" #: renpy/common/00action_file.rpy:26 msgid "{#weekday}Monday" msgstr "" #: renpy/common/00action_file.rpy:26 msgid "{#weekday}Tuesday" | > > > > | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | #: renpy/common/00accessibility.rpy:129 msgid "Clipboard" msgstr "" #: renpy/common/00accessibility.rpy:133 msgid "Debug" msgstr "" #: renpy/common/00accessibility.rpy:144 msgid "Return" msgstr "" #: renpy/common/00action_file.rpy:26 msgid "{#weekday}Monday" msgstr "" #: renpy/common/00action_file.rpy:26 msgid "{#weekday}Tuesday" |
︙ | ︙ | |||
235 236 237 238 239 240 241 242 243 244 245 246 247 248 | #: renpy/common/00action_file.rpy:571 msgid "File page quick" msgstr "" #: renpy/common/00action_file.rpy:573 msgid "File page [text]" msgstr "" #: renpy/common/00action_file.rpy:772 msgid "Next file page." msgstr "" #: renpy/common/00action_file.rpy:845 msgid "Previous file page." | > > > > > > > > > > > > | 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | #: renpy/common/00action_file.rpy:571 msgid "File page quick" msgstr "" #: renpy/common/00action_file.rpy:573 msgid "File page [text]" msgstr "" #: renpy/common/00action_file.rpy:631 msgid "Page {}" msgstr "" #: renpy/common/00action_file.rpy:631 msgid "Automatic saves" msgstr "" #: renpy/common/00action_file.rpy:631 msgid "Quick saves" msgstr "" #: renpy/common/00action_file.rpy:772 msgid "Next file page." msgstr "" #: renpy/common/00action_file.rpy:845 msgid "Previous file page." |
︙ | ︙ | |||
259 260 261 262 263 264 265 266 267 268 269 270 271 272 | #: renpy/common/00action_file.rpy:943 msgid "Quick load." msgstr "" #: renpy/common/00action_other.rpy:355 msgid "Language [text]" msgstr "" #: renpy/common/00director.rpy:708 msgid "The interactive director is not enabled here." msgstr "" #: renpy/common/00director.rpy:1481 msgid "⬆" | > > > > | 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | #: renpy/common/00action_file.rpy:943 msgid "Quick load." msgstr "" #: renpy/common/00action_other.rpy:355 msgid "Language [text]" msgstr "" #: renpy/common/00compat.rpy:264 msgid "Fullscreen" msgstr "" #: renpy/common/00director.rpy:708 msgid "The interactive director is not enabled here." msgstr "" #: renpy/common/00director.rpy:1481 msgid "⬆" |
︙ | ︙ | |||
423 424 425 426 427 428 429 430 431 432 433 434 435 436 | #: renpy/common/00gltest.rpy:93 msgid "NPOT" msgstr "" #: renpy/common/00gltest.rpy:97 msgid "Enable" msgstr "" #: renpy/common/00gltest.rpy:131 msgid "Powersave" msgstr "" #: renpy/common/00gltest.rpy:145 msgid "Framerate" | > > > > > > > > > > > > | 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 | #: renpy/common/00gltest.rpy:93 msgid "NPOT" msgstr "" #: renpy/common/00gltest.rpy:97 msgid "Enable" msgstr "" #: renpy/common/00gltest.rpy:101 msgid "Disable" msgstr "" #: renpy/common/00gltest.rpy:108 msgid "Gamepad" msgstr "" #: renpy/common/00gltest.rpy:122 msgid "Calibrate" msgstr "" #: renpy/common/00gltest.rpy:131 msgid "Powersave" msgstr "" #: renpy/common/00gltest.rpy:145 msgid "Framerate" |
︙ | ︙ | |||
451 452 453 454 455 456 457 458 459 460 461 462 463 464 | #: renpy/common/00gltest.rpy:163 msgid "Tearing" msgstr "" #: renpy/common/00gltest.rpy:179 msgid "Changes will take effect the next time this program is run." msgstr "" #: renpy/common/00gltest.rpy:213 msgid "Performance Warning" msgstr "" #: renpy/common/00gltest.rpy:218 msgid "This computer is using software rendering." | > > > > | 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 | #: renpy/common/00gltest.rpy:163 msgid "Tearing" msgstr "" #: renpy/common/00gltest.rpy:179 msgid "Changes will take effect the next time this program is run." msgstr "" #: renpy/common/00gltest.rpy:186 msgid "Quit" msgstr "" #: renpy/common/00gltest.rpy:213 msgid "Performance Warning" msgstr "" #: renpy/common/00gltest.rpy:218 msgid "This computer is using software rendering." |
︙ | ︙ | |||
1004 1005 1006 1007 1008 1009 1010 | msgstr "Entrée dans dup.rpy" #: game/dup.rpy:5 msgctxt "dup_c1842022" msgid "Eileen" msgstr "Hélène dialogue dup" | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 | msgstr "Entrée dans dup.rpy" #: game/dup.rpy:5 msgctxt "dup_c1842022" msgid "Eileen" msgstr "Hélène dialogue dup" #: game/script.rpy:7 msgctxt "game/script.rpy:7" msgid "Eileen" msgstr "Hélène personnage dup" #: game/script.rpy:27 msgid "You've created a new Ren'Py game." |
︙ | ︙ | |||
1501 1502 1503 1504 1505 1506 1507 | #: game/script.rpy:85 msgid "dupmenuentry" msgstr "dupmenuentrée" #: game/script.rpy:92 msgid "voiced text" msgstr "texte vocalisé" | > > > > > > > | 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 | #: game/script.rpy:85 msgid "dupmenuentry" msgstr "dupmenuentrée" #: game/script.rpy:92 msgid "voiced text" msgstr "texte vocalisé" #: game/script.rpy:95 msgid "「unicode characters♪」" msgstr "「caractères unicode♪」" #~ msgid "「But if you ever change your mind, feel free to ask me♪」" #~ msgstr "「Mais jamais tu changes d'avis, n'hésite pas à me demander♪」" |
Changes to test/output_expected/script.rpy.
︙ | ︙ | |||
132 133 134 135 136 137 138 139 140 141 142 143 144 145 | translate french start_06194c6b: # voice "path/to/file" # e "voiced text" voice "path/to/file" e "texte vocalisé" translate french strings: # game/script.rpy:7 old "Eileen" new "Hélène personnage dup" # game/script.rpy:33 | > > > > > > | 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | translate french start_06194c6b: # voice "path/to/file" # e "voiced text" voice "path/to/file" e "texte vocalisé" # game/script.rpy:95 translate french start_7e46d471: # b "「unicode characters♪」" b "「caractères unicode♪」" translate french strings: # game/script.rpy:7 old "Eileen" new "Hélène personnage dup" # game/script.rpy:33 |
︙ | ︙ |
Changes to tl2po.py.
︙ | ︙ | |||
24 25 26 27 28 29 30 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Use cases: # - import your game's translation started in Ren'Py format # - import default Ren'Py translated strings from "The Question" from __future__ import print_function | | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Use cases: # - import your game's translation started in Ren'Py format # - import default Ren'Py translated strings from "The Question" from __future__ import print_function import sys, os, fnmatch, operator, io import re import shutil import rttk.run, rttk.tlparser, rttk.utf_8_sig def tl2po(projectpath, language, outfile=None): if not re.match('^[a-z_]+$', language): raise Exception("Invalid language", language) if not os.path.isdir(os.path.join(projectpath,'game','tl',language)): raise Exception("Language not found", os.path.join(projectpath,'game','tl',language)) |
︙ | ︙ | |||
53 54 55 56 57 58 59 | # using --compile otherwise Ren'Py sometimes skips half of the files rttk.run.renpy([projectpath, 'translate', 'pot', '--compile']) originals = [] for curdir, subdirs, filenames in sorted(os.walk(os.path.join(projectpath,'game','tl','pot')), key=operator.itemgetter(0)): for filename in sorted(fnmatch.filter(filenames, '*.rpy')): print("Parsing " + os.path.join(curdir,filename)) | | < < < | < < < | | | | | | | | 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | # using --compile otherwise Ren'Py sometimes skips half of the files rttk.run.renpy([projectpath, 'translate', 'pot', '--compile']) originals = [] for curdir, subdirs, filenames in sorted(os.walk(os.path.join(projectpath,'game','tl','pot')), key=operator.itemgetter(0)): for filename in sorted(fnmatch.filter(filenames, '*.rpy')): print("Parsing " + os.path.join(curdir,filename)) f = io.open(os.path.join(curdir,filename), 'r', encoding='utf-8-sig') lines = f.readlines() lines.reverse() while len(lines) > 0: originals.extend(rttk.tlparser.parse_next_block(lines)) translated = [] for curdir, subdirs, filenames in os.walk(os.path.join(projectpath,'game','tl',language)): for filename in fnmatch.filter(filenames, '*.rpy'): print("Parsing " + os.path.join(curdir,filename)) f = io.open(os.path.join(curdir,filename), 'r', encoding='utf-8-sig') lines = f.readlines() lines.reverse() while len(lines) > 0: translated.extend(rttk.tlparser.parse_next_block(lines)) t_blocks_index = {} t_basestr_index = {} for s in translated: if s['id']: t_blocks_index[s['id']] = s['text'] else: t_basestr_index[s['text']] = s['translation'] occurrences = {} for s in originals: occurrences[s['text']] = occurrences.get(s['text'], 0) + 1 out = io.open(outfile, 'w', encoding='utf-8') out.write(ur"""msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" """) for s in originals: out.write(u'#: ' + s['source'] + u'\n') if occurrences[s['text']] > 1: out.write(u'msgctxt "' + (s['id'] or s['source']) + u'"\n') out.write('msgid "' + s['text'] + '"\n') if s['id'] is not None and t_blocks_index.has_key(s['id']): out.write(u'msgstr "' + t_blocks_index[s['id']] + u'"\n') else: out.write(u'msgstr "' + t_basestr_index.get(s['text'],'') + u'"\n') out.write(u'\n') print("Wrote '" + outfile + "'.") try: # Clean-up shutil.rmtree(os.path.join(projectpath,'game','tl','pot')) except OSError: pass if __name__ == '__main__': tl2po(sys.argv[1], sys.argv[2]) |
Changes to tl2pot.py.
︙ | ︙ | |||
20 21 22 23 24 25 26 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function | | | < | < < < | | | | | | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function import sys, os, fnmatch, operator, io import re import shutil import rttk.run, rttk.tlparser, rttk.utf_8_sig def tl2pot(projectpath, outfile='game.pot'): # Refresh strings try: # Ensure Ren'Py keeps the strings order (rather than append new strings) shutil.rmtree(os.path.join(projectpath,'game','tl','pot')) except OSError: pass print("Calling Ren'Py translate to get the latest strings") # using --compile otherwise Ren'Py sometimes skips half of the files rttk.run.renpy([projectpath, 'translate', 'pot', '--compile']) strings = [] for curdir, subdirs, filenames in sorted(os.walk(os.path.join(projectpath,'game','tl','pot')), key=operator.itemgetter(0)): for filename in sorted(fnmatch.filter(filenames, '*.rpy')): print("Parsing " + os.path.join(curdir,filename)) f = io.open(os.path.join(curdir,filename), 'r', encoding='utf-8-sig') lines = f.readlines() lines.reverse() cur_strings = [] while len(lines) > 0: cur_strings.extend(rttk.tlparser.parse_next_block(lines)) cur_strings.sort(key=lambda s: (s['source'].split(':')[0], int(s['source'].split(':')[1]))) strings.extend(cur_strings) occurrences = {} for s in strings: occurrences[s['text']] = occurrences.get(s['text'], 0) + 1 out = io.open(outfile, 'w', encoding='utf-8') out.write(ur"""msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" """) for s in strings: out.write(u'#: ' + s['source'] + u'\n') if occurrences[s['text']] > 1: out.write(u'msgctxt "' + (s['id'] or s['source']) + u'"\n') out.write(u'msgid "' + s['text'] + u'"\n') out.write(u'msgstr ""\n') out.write(u'\n') print("Wrote '" + outfile + "'.") try: # Clean-up shutil.rmtree(os.path.join(projectpath,'game','tl','pot')) except OSError: pass if __name__ == '__main__': tl2pot(sys.argv[1]) |