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

915 lines
24 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
import hashlib
import re
import collections
import os
import time
import io
import codecs
################################################################################
# Script
################################################################################
class ScriptTranslator(object):
def __init__(self):
# All languages we know about.
self.languages = set()
# A map from the translate identifier to the translate object used when the
# language is None.
self.default_translates = { }
# A map from (identifier, language) to the translate object used for that
# language.
self.language_translates = { }
# A list of (identifier, language) tuples that we need to chain together.
self.chain_worklist = [ ]
# A map from filename to a list of (label, translate) pairs found in
# that file.
self.file_translates = collections.defaultdict(list)
# A map from language to the StringTranslator for that language.
self.strings = collections.defaultdict(StringTranslator)
# A map from language to a list of TranslateBlock objects for
# that language.
self.block = collections.defaultdict(list)
# A map from language to a list of TranslateEarlyBlock objects for
# that language.
self.early_block = collections.defaultdict(list)
# A map from language to a list of TranslatePython objects for
# that language.
self.python = collections.defaultdict(list)
# A map from filename to a list of additional strings we've found
# in that file.
self.additional_strings = collections.defaultdict(list)
def count_translates(self):
"""
Return the number of dialogue blocks in the game.
"""
return len(self.default_translates)
def take_translates(self, nodes):
"""
Takes the translates out of the flattened list of statements, and stores
them into the dicts above.
"""
label = None
if not nodes:
return
TranslatePython = renpy.ast.TranslatePython
TranslateBlock = renpy.ast.TranslateBlock
TranslateEarlyBlock = renpy.ast.TranslateEarlyBlock
Menu = renpy.ast.Menu
UserStatement = renpy.ast.UserStatement
Translate = renpy.ast.Translate
filename = renpy.exports.unelide_filename(nodes[0].filename)
filename = os.path.normpath(os.path.abspath(filename))
for n in nodes:
if not n.translation_relevant:
continue
if n.name.__class__ is not tuple:
if isinstance(n.name, basestring):
label = n.name
type_n = n.__class__
if type_n is TranslatePython:
if n.language is not None:
self.languages.add(n.language)
self.python[n.language].append(n)
elif type_n is TranslateEarlyBlock:
if n.language is not None:
self.languages.add(n.language)
self.early_block[n.language].append(n)
elif type_n is TranslateBlock:
if n.language is not None:
self.languages.add(n.language)
self.block[n.language].append(n)
elif type_n is Menu:
for i in n.items:
s = i[0]
if s is None:
continue
self.additional_strings[filename].append((n.linenumber, s))
elif type_n is UserStatement:
strings = n.call("translation_strings")
if strings is None:
continue
for s in strings:
self.additional_strings[filename].append((n.linenumber, s))
elif type_n is Translate:
if n.language is None:
self.default_translates[n.identifier] = n
self.file_translates[filename].append((label, n))
else:
self.languages.add(n.language)
self.language_translates[n.identifier, n.language] = n
self.chain_worklist.append((n.identifier, n.language))
def chain_translates(self):
"""
Chains nodes in non-default translates together.
"""
unchained = [ ]
for identifier, language in self.chain_worklist:
if identifier not in self.default_translates:
unchained.append((identifier, language))
continue
translate = self.language_translates[identifier, language]
next_node = self.default_translates[identifier].after
renpy.ast.chain_block(translate.block, next_node)
self.chain_worklist = unchained
def lookup_translate(self, identifier, alternate=None):
identifier = identifier.replace('.', '_')
language = renpy.game.preferences.language
if language is not None:
tl = self.language_translates.get((identifier, language), None)
if (tl is None) and alternate:
tl = self.language_translates.get((identifier, language), None)
else:
tl = None
if tl is None:
tl = self.default_translates[identifier]
return tl.block[0]
def encode_say_string(s):
"""
Encodes a string in the format used by Ren'Py say statements.
"""
s = s.replace("\\", "\\\\")
s = s.replace("\n", "\\n")
s = s.replace("\"", "\\\"")
s = re.sub(r'(?<= ) ', '\\ ', s)
return "\"" + s + "\""
class Restructurer(object):
def __init__(self, children):
self.label = None
self.alternate = None
self.identifiers = set()
self.callback(children)
def id_exists(self, identifier):
if identifier in self.identifiers:
return True
if identifier in renpy.game.script.translator.default_translates: # @UndefinedVariable
return True
return False
def unique_identifier(self, label, digest):
if label is None:
base = digest
else:
base = label.replace(".", "_") + "_" + digest
i = 0
suffix = ""
while True:
identifier = base + suffix
if not self.id_exists(identifier):
break
i += 1
suffix = "_{0}".format(i)
return identifier
def create_translate(self, block):
"""
Creates an ast.Translate that wraps `block`. The block may only contain
translatable statements.
"""
md5 = hashlib.md5()
for i in block:
code = i.get_code()
md5.update(code.encode("utf-8") + "\r\n")
digest = md5.hexdigest()[:8]
identifier = self.unique_identifier(self.label, digest)
self.identifiers.add(identifier)
if self.alternate is not None:
alternate = self.unique_identifier(self.alternate, digest)
self.identifiers.add(alternate)
else:
alternate = None
loc = (block[0].filename, block[0].linenumber)
tl = renpy.ast.Translate(loc, identifier, None, block, alternate=alternate)
tl.name = block[0].name + ("translate",)
ed = renpy.ast.EndTranslate(loc)
ed.name = block[0].name + ("end_translate",)
return [ tl, ed ]
def callback(self, children):
"""
This should be called with a list of statements. It restructures the statements
in the list so that translatable statements are contained within translation blocks.
"""
new_children = [ ]
group = [ ]
for i in children:
if isinstance(i, renpy.ast.Label):
if not i.hide:
if i.name.startswith("_"):
self.alternate = i.name
else:
self.label = i.name
self.alternate = None
if not isinstance(i, renpy.ast.Translate):
i.restructure(self.callback)
if isinstance(i, renpy.ast.Say):
group.append(i)
tl = self.create_translate(group)
new_children.extend(tl)
group = [ ]
elif i.translatable:
group.append(i)
else:
if group:
tl = self.create_translate(group)
new_children.extend(tl)
group = [ ]
new_children.append(i)
if group:
nodes = self.create_translate(group)
new_children.extend(nodes)
group = [ ]
children[:] = new_children
def restructure(children):
Restructurer(children)
################################################################################
# String Translation
################################################################################
update_translations = ("RENPY_UPDATE_STRINGS" in os.environ)
def quote_unicode(s):
s = s.replace("\\", "\\\\")
s = s.replace("\"", "\\\"")
s = s.replace("\a", "\\a")
s = s.replace("\b", "\\b")
s = s.replace("\f", "\\f")
s = s.replace("\n", "\\n")
s = s.replace("\r", "\\r")
s = s.replace("\t", "\\t")
s = s.replace("\v", "\\v")
return s
class StringTranslator(object):
"""
This object stores the translations for a single language. It can also
buffer unknown translations, and write them to a file at game's end, if
we want that to happen.
"""
def __init__(self):
# A map from translation to translated string.
self.translations = { }
# A map from translation to the location of the translated string.
self.translation_loc = { }
# A list of unknown translations.
self.unknown = [ ]
def add(self, old, new, newloc):
if old in self.translations:
if old in self.translation_loc:
print(newloc, self.translation_loc[old])
fn, line = self.translation_loc[old]
raise Exception("A translation for \"{}\" already exists at {}:{}.".format(
quote_unicode(old), fn, line))
else:
raise Exception("A translation for \"{}\" already exists.".format(
quote_unicode(old)))
self.translations[old] = new
if newloc is not None:
self.translation_loc[old] = newloc
def translate(self, old):
new = self.translations.get(old, None)
if new is not None:
return new
if update_translations:
self.translations[old] = old
self.unknown.append(old)
# Remove {#...} tags.
if new is None:
notags = re.sub(r"\{\#.*?\}", "", old)
new = self.translations.get(notags, None)
if new is not None:
return new
return old
def write_updated_strings(self, language):
if not self.unknown:
return
if language is None:
fn = os.path.join(renpy.config.gamedir, "strings.rpy")
else:
fn = os.path.join(renpy.config.gamedir, renpy.config.tl_directory, language, "strings.rpy")
f = renpy.translation.generation.open_tl_file(fn)
f.write(u"translate {} strings:\n".format(language))
f.write(u"\n")
for i in self.unknown:
i = quote_unicode(i)
f.write(u" old \"{}\"\n".format(i))
f.write(u" new \"{}\"\n".format(i))
f.write(u"\n")
f.close()
def add_string_translation(language, old, new, newloc):
tl = renpy.game.script.translator
stl = tl.strings[language]
tl.languages.add(language)
stl.add(old, new, newloc)
Default = renpy.object.Sentinel("default")
def translate_string(s, language=Default):
"""
:doc: translate_string
:name: renpy.translate_string
Translates interface string `s` to `language`. If `language` is Default,
uses the language set in the preferences. This does not mark `s` to be
translated.
"""
if language is Default:
language = renpy.game.preferences.language
stl = renpy.game.script.translator.strings[language] # @UndefinedVariable
return stl.translate(s)
def write_updated_strings():
stl = renpy.game.script.translator.strings[renpy.game.preferences.language] # @UndefinedVariable
stl.write_updated_strings(renpy.game.preferences.language)
################################################################################
# RPT Support
#
# RPT was the translation format used before 6.15.
################################################################################
def load_rpt(fn):
"""
Loads the .rpt file `fn`.
"""
def unquote(s):
s = s.replace("\\n", "\n")
s = s.replace("\\\\", "\\")
return s
language = os.path.basename(fn).replace(".rpt", "")
f = renpy.loader.load(fn)
old = None
for l in f:
l = l.decode("utf-8")
l = l.rstrip()
if not l:
continue
if l[0] == '#':
continue
s = unquote(l[2:])
if l[0] == '<':
if old:
raise Exception("{0} string {1!r} does not have a translation.".format(language, old))
old = s
if l[0] == ">":
if old is None:
raise Exception("{0} translation {1!r} doesn't belong to a string.".format(language, s))
add_string_translation(language, old, s, None)
old = None
f.close()
if old is not None:
raise Exception("{0} string {1!r} does not have a translation.".format(language, old))
def load_all_rpts():
"""
Loads all .rpt files.
"""
for fn in renpy.exports.list_files():
if fn.endswith(".rpt"):
load_rpt(fn)
################################################################################
# Changing language
################################################################################
style_backup = None
def init_translation():
"""
Called before the game starts.
"""
global style_backup
style_backup = renpy.style.backup() # @UndefinedVariable
load_all_rpts()
renpy.store._init_language() # @UndefinedVariable
old_language = "language never set"
# A list of styles that have beend deferred to right before translate
# styles are run.
deferred_styles = [ ]
def old_change_language(tl, language):
for i in deferred_styles:
i.apply()
def run_blocks():
for i in tl.early_block[language]:
renpy.game.context().run(i.block[0])
for i in tl.block[language]:
renpy.game.context().run(i.block[0])
renpy.game.invoke_in_new_context(run_blocks)
for i in tl.python[language]:
renpy.python.py_exec_bytecode(i.code.bytecode)
for i in renpy.config.language_callbacks[language]:
i()
def new_change_language(tl, language):
for i in tl.python[language]:
renpy.python.py_exec_bytecode(i.code.bytecode)
def run_blocks():
for i in tl.early_block[language]:
renpy.game.context().run(i.block[0])
renpy.game.invoke_in_new_context(run_blocks)
for i in renpy.config.language_callbacks[language]:
i()
for i in deferred_styles:
i.apply()
def run_blocks():
for i in tl.block[language]:
renpy.game.context().run(i.block[0])
renpy.game.invoke_in_new_context(run_blocks)
renpy.config.init_system_styles()
def change_language(language, force=False):
"""
:doc: translation_functions
Changes the current language to `language`, which can be a string or
None to use the default language.
"""
global old_language
renpy.game.preferences.language = language
if old_language == language and not force:
return
tl = renpy.game.script.translator
renpy.style.restore(style_backup) # @UndefinedVariable
renpy.style.rebuild() # @UndefinedVariable
for i in renpy.config.translate_clean_stores:
renpy.python.clean_store(i)
if renpy.config.new_translate_order:
new_change_language(tl, language)
else:
old_change_language(tl, language)
renpy.store._history_list = renpy.store.list()
renpy.store.nvl_list = renpy.store.list()
for i in renpy.config.change_language_callbacks:
i()
# Reset various parts of the system. Most notably, this clears the image
# cache, letting us load translated images.
renpy.exports.free_memory()
# Rebuild the styles.
renpy.style.rebuild() # @UndefinedVariable
for i in renpy.config.translate_clean_stores:
renpy.python.reset_store_changes(i)
# Restart the interaction.
renpy.exports.restart_interaction()
if language != old_language:
renpy.exports.block_rollback()
old_language = language
def check_language():
"""
Checks to see if the language has changed. If it has, jump to the start
of the current translation block.
"""
ctx = renpy.game.contexts[-1]
preferences = renpy.game.preferences
# Deal with a changed language.
if ctx.translate_language != preferences.language:
ctx.translate_language = preferences.language
tid = ctx.translate_identifier
if tid is not None:
node = renpy.game.script.translator.lookup_translate(tid) # @UndefinedVariable
if node is not None:
raise renpy.game.JumpException(node.name)
def known_languages():
"""
:doc: translation_functions
Returns the set of known languages. This does not include the default
language, None.
"""
return { i for i in renpy.game.script.translator.languages if i is not None } # @UndefinedVariable
################################################################################
# Detect language
################################################################################
locales = {
"ab": "abkhazian",
"aa": "afar",
"af": "afrikaans",
"ak": "akan",
"sq": "albanian",
"am": "amharic",
"ar": "arabic",
"an": "aragonese",
"hy": "armenian",
"as": "assamese",
"av": "avaric",
"ae": "avestan",
"ay": "aymara",
"az": "azerbaijani",
"bm": "bambara",
"ba": "bashkir",
"eu": "basque",
"be": "belarusian",
"bn": "bengali",
"bh": "bihari",
"bi": "bislama",
"bs": "bosnian",
"br": "breton",
"bg": "bulgarian",
"my": "burmese",
"ca": "catalan",
"ch": "chamorro",
"ce": "chechen",
"ny": "chewa",
"cv": "chuvash",
"kw": "cornish",
"co": "corsican",
"cr": "cree",
"hr": "croatian",
"cs": "czech",
"da": "danish",
"dv": "maldivian",
"nl": "dutch",
"dz": "dzongkha",
"en": "english",
"et": "estonian",
"ee": "ewe",
"fo": "faroese",
"fj": "fijian",
"fi": "finnish",
"fr": "french",
"ff": "fulah",
"gl": "galician",
"ka": "georgian",
"de": "german",
"el": "greek",
"gn": "guaran",
"gu": "gujarati",
"ht": "haitian",
"ha": "hausa",
"he": "hebrew",
"hz": "herero",
"hi": "hindi",
"ho": "hiri_motu",
"hu": "hungarian",
"id": "indonesian",
"ga": "irish",
"ig": "igbo",
"ik": "inupiaq",
"is": "icelandic",
"it": "italian",
"iu": "inuktitut",
"ja": "japanese",
"jv": "javanese",
"kl": "greenlandic",
"kn": "kannada",
"kr": "kanuri",
"ks": "kashmiri",
"kk": "kazakh",
"km": "khmer",
"ki": "kikuyu",
"rw": "kinyarwanda",
"ky": "kirghiz",
"kv": "komi",
"kg": "kongo",
"ko": "korean",
"ku": "kurdish",
"kj": "kuanyama",
"la": "latin",
"lb": "luxembourgish",
"lg": "ganda",
"li": "limburgan",
"ln": "lingala",
"lo": "lao",
"lt": "lithuanian",
"lv": "latvian",
"gv": "manx",
"mk": "macedonian",
"mg": "malagasy",
"ms": "malay",
"ml": "malayalam",
"mt": "maltese",
"mi": "maori",
"mr": "marathi",
"mh": "marshallese",
"mn": "mongolian",
"na": "nauru",
"nv": "navaho",
"ne": "nepali",
"ng": "ndonga",
"no": "norwegian",
"ii": "nuosu",
"nr": "ndebele",
"oc": "occitan",
"oj": "ojibwa",
"om": "oromo",
"or": "oriya",
"os": "ossetian",
"pa": "panjabi",
"pi": "pali",
"fa": "persian",
"pl": "polish",
"ps": "pashto",
"pt": "portuguese",
"qu": "quechua",
"rm": "romansh",
"rn": "rundi",
"ro": "romanian",
"ru": "russian",
"sa": "sanskrit",
"sc": "sardinian",
"sd": "sindhi",
"se": "sami",
"sm": "samoan",
"sg": "sango",
"sr": "serbian",
"gd": "gaelic",
"sn": "shona",
"si": "sinhala",
"sk": "slovak",
"sl": "slovene",
"so": "somali",
"st": "sotho",
"es": "spanish",
"su": "sundanese",
"sw": "swahili",
"ss": "swati",
"sv": "swedish",
"ta": "tamil",
"te": "telugu",
"tg": "tajik",
"th": "thai",
"ti": "tigrinya",
"bo": "tibetan",
"tk": "turkmen",
"tl": "tagalog",
"tn": "tswana",
"to": "tongan",
"tr": "turkish",
"ts": "tsonga",
"tt": "tatar",
"tw": "twi",
"ty": "tahitian",
"ug": "uighur",
"uk": "ukrainian",
"ur": "urdu",
"uz": "uzbek",
"ve": "venda",
"vi": "vietnamese",
"wa": "walloon",
"cy": "welsh",
"wo": "wolof",
"fy": "frisian",
"xh": "xhosa",
"yi": "yiddish",
"yo": "yoruba",
"za": "zhuang",
"zu": "zulu",
"chs": "simplified_chinese",
"cht": "traditional_chinese",
"zh": "traditional_chinese",
}
def detect_user_locale():
import locale
if renpy.windows:
import ctypes
windll = ctypes.windll.kernel32
locale_name = locale.windows_locale.get(windll.GetUserDefaultUILanguage())
elif renpy.android:
from jnius import autoclass
Locale = autoclass('java.util.Locale')
locale_name = str(Locale.getDefault().getLanguage())
elif renpy.ios:
import pyobjus
NSLocale = pyobjus.autoclass("NSLocale")
languages = NSLocale.preferredLanguages()
locale_name = languages.objectAtIndex_(0).UTF8String().decode("utf-8")
locale_name.replace("-", "_")
else:
locale_name = locale.getdefaultlocale()
if locale_name is not None:
locale_name = locale_name[0]
if locale_name is None:
return None, None
normalize = locale.normalize(locale_name)
if normalize == locale_name:
language = region = locale_name
else:
locale_name = normalize
if '.' in locale_name:
locale_name, _ = locale_name.split('.', 1)
language, region = locale_name.lower().split("_")
return language, region