CampBuddy/Camp.Buddy v2.2.1/Camp_Buddy-2.2.1-pc/renpy/translation/generation.py
2025-03-03 23:00:33 +01:00

555 lines
14 KiB
Python

# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# 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 renpy.translation
import re
import os
import time
import io
import codecs
import collections
import shutil
from renpy.translation import quote_unicode
from renpy.parser import elide_filename
################################################################################
# Translation Generation
################################################################################
STRING_RE = r"""(?x)
\b__?\s*\(\s*[uU]?(
\"\"\"(?:\\.|\"{1,2}|[^\\"])*?\"\"\"
|'''(?:\\.|\'{1,2}|[^\\'])*?'''
|"(?:\\.|[^\\"])*"
|'(?:\\.|[^\\'])*'
)\s*\)
"""
def scan_strings(filename):
"""
Scans `filename`, a file containing Ren'Py script, for translatable
strings.
Generates a list of (line, string) tuples.
"""
rv = [ ]
for line, s in renpy.game.script.translator.additional_strings[filename]: # @UndefinedVariable
rv.append((line, s))
line = 1
for _filename, lineno, text in renpy.parser.list_logical_lines(filename):
for m in re.finditer(STRING_RE, text):
s = m.group(1)
if s is not None:
s = s.strip()
s = "u" + s
s = eval(s)
rv.append((lineno, s))
return rv
def scan_comments(filename):
rv = [ ]
if filename not in renpy.config.translate_comments:
return rv
comment = [ ]
start = 0
with codecs.open(filename, "r", "utf-8") as f:
lines = [ i.rstrip() for i in f.read().replace(u"\ufeff", "").split('\n') ]
for i, l in enumerate(lines):
if not comment:
start = i + 1
m = re.match(r'\s*## (.*)', l)
if m:
c = m.group(1)
if comment:
c = c.strip()
comment.append(c)
elif comment:
s = "## " + " ".join(comment)
if s.endswith("#"):
s = s.rstrip("# ")
comment = [ ]
rv.append((start, s))
return rv
tl_file_cache = { }
# Should we write the TODO marker?
todo = True
def open_tl_file(fn):
if fn in tl_file_cache:
return tl_file_cache[fn]
if not os.path.exists(fn):
dn = os.path.dirname(fn)
try:
os.makedirs(dn)
except:
pass
f = io.open(fn, "a", encoding="utf-8")
f.write(u"\ufeff")
else:
f = io.open(fn, "a", encoding="utf-8")
if todo:
f.write(u"# TO" + "DO: Translation updated at {}\n".format(time.strftime("%Y-%m-%d %H:%M")))
f.write(u"\n")
tl_file_cache[fn] = f
return f
def close_tl_files():
for i in tl_file_cache.values():
i.close()
tl_file_cache.clear()
def shorten_filename(filename):
"""
Shortens a file name. Returns the shortened filename, and a flag that says
if the filename is in the common directory.
"""
commondir = os.path.normpath(renpy.config.commondir)
gamedir = os.path.normpath(renpy.config.gamedir)
if filename.startswith(commondir):
fn = os.path.relpath(filename, commondir)
common = True
elif filename.startswith(gamedir):
fn = os.path.relpath(filename, gamedir)
common = False
else:
fn = os.path.basename(filename)
common = False
return fn, common
def write_translates(filename, language, filter): # @ReservedAssignment
fn, common = shorten_filename(filename)
# The common directory should not have dialogue in it.
if common:
return
tl_filename = os.path.join(renpy.config.gamedir, renpy.config.tl_directory, language, fn)
if tl_filename[-1] == "m":
tl_filename = tl_filename[:-1]
if language == "None":
language = None
translator = renpy.game.script.translator
for label, t in translator.file_translates[filename]:
if (t.identifier, language) in translator.language_translates:
continue
if hasattr(t, "alternate"):
if (t.alternate, language) in translator.language_translates:
continue
f = open_tl_file(tl_filename)
if label is None:
label = ""
f.write(u"# {}:{}\n".format(t.filename, t.linenumber))
f.write(u"translate {} {}:\n".format(language, t.identifier.replace('.', '_')))
f.write(u"\n")
for n in t.block:
f.write(u" # " + n.get_code() + "\n")
for n in t.block:
f.write(u" " + n.get_code(filter) + "\n")
f.write(u"\n")
def translation_filename(s):
if renpy.config.translate_launcher:
return s.launcher_file
if s.common:
return "common.rpy"
filename = s.elided
if filename[-1] == "m":
filename = filename[:-1]
return filename
def write_strings(language, filter, min_priority, max_priority, common_only): # @ReservedAssignment
"""
Writes strings to the file.
"""
if language == "None":
stl = renpy.game.script.translator.strings[None] # @UndefinedVariable
else:
stl = renpy.game.script.translator.strings[language] # @UndefinedVariable
# If this function changes, count_missing may also need to
# change.
strings = renpy.translation.scanstrings.scan(min_priority, max_priority, common_only)
stringfiles = collections.defaultdict(list)
for s in strings:
tlfn = translation_filename(s)
if tlfn is None:
continue
# Already seen.
if s.text in stl.translations:
continue
if language == "None" and tlfn == "common.rpy":
tlfn = "common.rpym"
stringfiles[tlfn].append(s)
for tlfn, sl in stringfiles.items():
# sl.sort(key=lambda s : (s.filename, s.line))
tlfn = os.path.join(renpy.config.gamedir, renpy.config.tl_directory, language, tlfn)
f = open_tl_file(tlfn)
f.write(u"translate {} strings:\n".format(language))
f.write(u"\n")
for s in sl:
text = filter(s.text)
f.write(u" # {}:{}\n".format(elide_filename(s.filename), s.line))
f.write(u" old \"{}\"\n".format(quote_unicode(s.text)))
f.write(u" new \"{}\"\n".format(quote_unicode(text)))
f.write(u"\n")
def null_filter(s):
return s
def empty_filter(s):
return ""
def generic_filter(s, function):
"""
:doc: text_utility
Transforms `s`, while leaving text tags and interpolation the same.
`function`
A function that is called with strings corresponding to runs of
text, and should return a second string that replaces that run
of text.
::
init python:
def upper(s):
return s.upper()
$ upper = renpy.transform_text("{b}Not Upper{/b}")
"""
def remove_special(s, start, end, process):
specials = 0
first = False
rv = ""
buf = ""
for i in s:
if i == start:
if first:
specials = 0
else:
rv += process(buf)
buf = ""
if specials == 0:
first = True
specials += 1
rv += start
elif i == end:
first = False
specials -= 1
if specials < 0:
specials += 1
rv += end
else:
if specials:
rv += i
else:
buf += i
if buf:
rv += process(buf)
return rv
def remove_braces(s):
return remove_special(s, "{", "}", function)
return remove_special(s, "[", "]", remove_braces)
def rot13_transform(s):
ROT13 = { }
for i, j in zip("ABCDEFGHIJKLM", "NMOPQRSTUVWYZ"):
ROT13[i] = j
ROT13[j] = i
i = i.lower()
j = j.lower()
ROT13[i] = j
ROT13[j] = i
return "".join(ROT13.get(i, i) for i in s)
def rot13_filter(s):
return generic_filter(s, rot13_transform)
def piglatin_transform(s):
# Based on http://stackoverflow.com/a/23177629/3549890
lst = ['sh', 'gl', 'ch', 'ph', 'tr', 'br', 'fr', 'bl', 'gr', 'st', 'sl', 'cl', 'pl', 'fl']
def replace(m):
i = m.group(0)
if i[0] in ['a', 'e', 'i', 'o', 'u']:
rv = i + 'ay'
elif i[:2] in lst:
rv = i[2:] + i[:2] + 'ay'
else:
rv = i[1:] + i[0] + 'ay'
if i[0].isupper():
rv = rv.capitalize()
return rv
return re.sub(r'\w+', replace, s)
def piglatin_filter(s):
return generic_filter(s, piglatin_transform)
def translate_list_files():
"""
Returns a list of files that exist and should be scanned for translations.
"""
filenames = list(renpy.config.translate_files)
for dirname, filename in renpy.loader.listdirfiles():
if dirname is None:
continue
if filename.startswith("tl/"):
continue
filename = os.path.join(dirname, filename)
if not (filename.endswith(".rpy") or filename.endswith(".rpym")):
continue
filename = os.path.normpath(filename)
if not os.path.exists(filename):
continue
filenames.append(filename)
return filenames
def count_missing(language, min_priority, max_priority, common_only):
"""
Prints a count of missing translations for `language`.
"""
translator = renpy.game.script.translator
missing_translates = 0
for filename in translate_list_files():
for _, t in translator.file_translates[filename]:
if (t.identifier, language) not in translator.language_translates:
missing_translates += 1
missing_strings = 0
stl = renpy.game.script.translator.strings[language] # @UndefinedVariable
strings = renpy.translation.scanstrings.scan(min_priority, max_priority, common_only)
for s in strings:
tlfn = translation_filename(s)
if tlfn is None:
continue
if s.text in stl.translations:
continue
missing_strings += 1
print("{}: {} missing dialogue translations, {} missing string translations.".format(
language,
missing_translates,
missing_strings
))
def translate_command():
"""
The translate command. When called from the command line, this generates
the translations.
"""
ap = renpy.arguments.ArgumentParser(description="Generates or updates translations.")
ap.add_argument("language", help="The language to generate translations for.")
ap.add_argument("--rot13", help="Apply rot13 while generating translations.", dest="rot13", action="store_true")
ap.add_argument("--piglatin", help="Apply pig latin while generating translations.", dest="piglatin", action="store_true")
ap.add_argument("--empty", help="Produce empty strings while generating translations.", dest="empty", action="store_true")
ap.add_argument("--count", help="Instead of generating files, print a count of missing translations.", dest="count", action="store_true")
ap.add_argument("--min-priority", help="Translate strings with more than this priority.", dest="min_priority", default=0, type=int)
ap.add_argument("--max-priority", help="Translate strings with more than this priority.", dest="max_priority", default=0, type=int)
ap.add_argument("--strings-only", help="Only translate strings (not dialogue).", dest="strings_only", default=False, action="store_true")
ap.add_argument("--common-only", help="Only translate string from the common code.", dest="common_only", default=False, action="store_true")
ap.add_argument("--no-todo", help="Do not include the TODO flag.", dest="todo", default=True, action="store_false")
args = ap.parse_args()
global todo
todo = args.todo
if renpy.config.translate_launcher:
max_priority = args.max_priority or 499
else:
max_priority = args.max_priority or 299
if args.count:
count_missing(args.language, args.min_priority, max_priority, args.common_only)
return False
if args.rot13:
filter = rot13_filter # @ReservedAssignment
elif args.piglatin:
filter = piglatin_filter # @ReservedAssignment
elif args.empty:
filter = empty_filter # @ReservedAssignment
else:
filter = null_filter # @ReservedAssignment
if not args.strings_only:
for filename in translate_list_files():
write_translates(filename, args.language, filter)
write_strings(args.language, filter, args.min_priority, max_priority, args.common_only)
close_tl_files()
if renpy.config.translate_launcher and (not args.strings_only):
src = os.path.join(renpy.config.renpy_base, "gui", "game", "script.rpy")
dst = os.path.join(renpy.config.gamedir, "tl", args.language, "script.rpym")
if os.path.exists(src) and not os.path.exists(dst):
shutil.copy(src, dst)
return False
renpy.arguments.register_command("translate", translate_command)