stendhal/bin/locale-template.py
2024-05-17 00:53:17 -07:00

308 lines
9.1 KiB
Python
Executable file

#!/usr/bin/env python
# ***************************************************************************
# * Copyright © 2023-2024 - Stendhal *
# ***************************************************************************
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation; either version 2 of the License, or *
# * (at your option) any later version. *
# * *
# ***************************************************************************
# script for generating data/language/template.txt
import codecs
import errno
import os
import re
import sys
dir_root = os.path.dirname(os.path.dirname(__file__))
dir_config = os.path.normpath(os.path.join(dir_root, "data/conf"))
dir_locale = os.path.normpath(os.path.join(dir_root, "data/languages"))
file_template = os.path.join(dir_locale, "template.txt")
# Helper functions.
def printWarning(msg):
sys.stdout.write("WARNING: {}\n".format(msg))
def printError(msg):
sys.stderr.write("ERROR: {}\n".format(msg))
def exitWithError(err, msg):
printError(msg)
sys.exit(err)
def escapeRegex(st):
return st.replace("*", "\\*")\
.replace("?", "\\?")\
.replace("+", "\\+")\
.replace("(", "\\(")\
.replace(")", "\\)")\
.replace("[", "\\[")\
.replace("]", "\\]")\
.replace("|", "\\|")
# categories, names, & descriptions cache
__cache = {
"items": {},
"creatures": {}
}
## Parses configuration file for names & descriptions.
def parseConfig(_type, category):
print("parsing {} names and descriptions from category '{}'".format(_type, category))
dir_type = os.path.join(dir_config, _type)
if not os.path.isdir(dir_type):
printWarning("'{}' directory not found, excluding from translations template".format(dir_type))
return
file_xml = os.path.join(dir_type, category + ".xml")
if not os.path.isfile(file_xml):
printWarning("'{}' file not found, excluding from translation template".format(file_xml))
return
if category not in __cache[_type]:
__cache[_type][category] = {}
tmp = __cache[_type][category]
try:
fin = codecs.open(file_xml, "r", "utf-8")
# ensure working with LF line endings
content = fin.read().replace("\r\n", "\n").replace("\r", "\n")
fin.close()
except PermissionError:
exitWithError(errno.EACCES, "cannot open '{}' for reading, permission denied".format(file_xml))
# tag to parse from xml
tag = _type
if tag.endswith("s"):
tag = tag[:-1]
item_name = None
item_desc = None
in_comment = False
for line in content.split("\n"):
line = line.strip()
# ignore commented sections
# NOTE: only ignores lines starting with a comment and ignores content on same line after comment
if in_comment and "-->" in line:
in_comment = False
continue
if not in_comment:
in_comment = line.startswith("<!--") and "-->" not in line
if in_comment:
continue
if line.startswith("<{} name=\"".format(tag)):
item_name = re.sub("^<{} name=\"".format(tag), "", line)
item_name = re.sub("\".*$", "", item_name).strip()
elif item_name and line.startswith("<description>"):
item_desc = re.sub("^<description>", "", line)
item_desc = re.sub("</description>$", "", item_desc).strip()
elif item_name and line == "</{}>".format(tag):
# there may be multiple versions of an entity/item with different descriptions
if item_name in tmp and item_desc:
desc_new = item_desc
# preserve previous description(s)
item_desc = tmp[item_name]
# convert to a list for multiple descriptions
if type(item_desc) == str:
item_desc = [item_desc]
elif not item_desc:
item_desc = []
# add new description
item_desc.append(desc_new)
tmp[item_name] = item_desc
# reset
item_name = None
item_desc = None
__cache[_type][category] = tmp
## Exports parsed data to template text file.
def exportTemplate():
if not os.path.isdir(dir_locale):
exitWithError(errno.ENOENT, "'{}' directory not found, cannot create translation template".format(dir_locale))
try:
fout = codecs.open(file_template, "w", "utf-8")
except PermissionError:
exitWithError(errno.EACCES, "cannot open '{}' for writing, permission denied".format(file_template))
# main header
header = ("", "##", "## Language: <name> (<code>)", "## Translators: <translator1>[, <translator2>...]", "##")
fout.write("\n".join(header) + "\n")
for _type in __cache:
# type header
fout.write("\n\n# {} names and descriptions".format(_type))
contents = __cache[_type]
for category in tuple(sorted(contents)):
if not contents[category]:
printWarning("'{}' category '{}' is empty, skipping".format(_type, category))
continue
# category header
fout.write("\n\n## " + category + "\n")
cat_list = contents[category]
for item_name in cat_list:
fout.write("\n" + item_name + "=\n")
item_desc = cat_list[item_name]
if item_desc:
if type(item_desc) == str:
item_desc = [item_desc]
for desc in item_desc:
fout.write(desc + "=\n")
fout.close()
print("template exported to '{}'".format(file_template))
## Builds a localization template for translating.
def buildTemplate():
# categores not translated
cat_excludes = {
"items": ("dummy_weapons", "meta")
}
for _type in __cache:
print("parsing type: {}".format(_type))
category_names = []
dir_type = os.path.join(dir_config, _type)
for basename in sorted(os.listdir(dir_type)):
# include xml configuration files only
if not os.path.isfile(os.path.join(dir_type, basename)) or not basename.endswith(".xml"):
continue
# remove .xml filename suffix
category = basename[:-4]
if _type in cat_excludes and category in cat_excludes[_type]:
continue
category_names.append(category)
if len(category_names) == 0:
printWarning("no '{}' categories found, excluding from translation template".format(_type))
for category in category_names:
parseConfig(_type, category)
exportTemplate()
## Updates existing translations found in data/languages.
def updateExisting():
if not os.path.isdir(dir_locale):
printWarning("directory '{}' not found, no translations available for updating".format(dir_locale))
return
if not os.path.isfile(file_template):
exitWithError(errno.ENOENT, "template '{}' not found, cannot update translations".format(file_template))
try:
fin = codecs.open(file_template, "r", "utf-8")
template = fin.read().replace("\r\n", "\n").replace("\r", "\n")
fin.close()
except PermissionError:
exitWithError(errno.EACCES, "cannot open '{}' for reading, permission denied".format(file_template))
locale_codes = []
for basename in os.listdir(dir_locale):
if not basename.endswith(".txt") or basename == "template.txt":
continue
locale_codes.append(basename[:-4])
if not locale_codes:
printWarning("no translations available for updating")
return
for code in locale_codes:
print("updating locale '{}'".format(code))
filepath = os.path.join(dir_locale, code + ".txt")
try:
fin = codecs.open(filepath, "r", "utf-8")
contents_old = fin.read().replace("\r\n", "\n").replace("\r", "\n")
fin.close()
except PermissionError:
exitWithError(errno.EACCES, "cannot open '{}' for reading, permission denied".format(filepath))
# make a copy of template
contents = template
header_language = None
header_translators = None
unused = []
lidx = 0
for line in contents_old.split("\n"):
lidx += 1
# check for headers
if not header_language and line.startswith("## Language:"):
header_language = re.sub("^## Language:", "", line).strip()
continue
if not header_translators and line.startswith("## Translators:"):
header_translators = re.sub("^## Translators:", "", line).strip()
continue
# ignore comments & empty lines
if not line.strip() or line.startswith("#"):
continue
if "=" not in line:
printWarning("malformed line ({}) in '{}'".format(lidx, filepath))
continue
key = escapeRegex(line.split("=", 1)[0]) + "="
if not re.search("^" + key, contents, re.M):
unused.append(line)
continue
contents = re.sub("^" + key + ".*$", line, contents, flags=re.M)
if header_language:
contents = re.sub("^## Language:.*$", "## Language: {}".format(header_language), contents, 1, re.M)
if header_translators:
contents = re.sub("^## Translators:.*$", "## Translators: {}".format(header_translators), contents, 1, re.M)
contents = contents.rstrip()
if unused:
contents += "\n\n\n# strings not found in template\n"
for line in unused:
contents += "\n" + line
try:
fout = codecs.open(filepath, "w", "utf-8")
fout.write(contents + "\n")
fout.close()
except PermissionError:
exitWithError(errno.EACCES, "cannot open '{}' for writing, permission denied".format(filepath))
print("exported to '{}'".format(filepath))
if __name__ == "__main__":
# work from source root
os.chdir(dir_root)
args = sys.argv[1:]
if "--update" in args:
updateExisting()
else:
buildTemplate()