Skip to content

Commit

Permalink
switch to native translator
Browse files Browse the repository at this point in the history
  • Loading branch information
FaceDeer committed Feb 18, 2020
1 parent 290d65c commit a035f10
Show file tree
Hide file tree
Showing 15 changed files with 292 additions and 425 deletions.
218 changes: 218 additions & 0 deletions i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Script to generate the template file and update the translation files.
# Copy the script into the mod or modpack root folder and run it there.
#
# Copyright (C) 2019 Joachim Stolberg
# LGPLv2.1+

from __future__ import print_function
import os, fnmatch, re, shutil, errno

#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
#TODO: support [[]] delimiters
pattern_lua = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)

# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)

pattern_tr = re.compile(r'(.+?[^@])=(.+)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')

#attempt to read the mod's name from the mod.conf file. Returns None on failure
def get_modname(folder):
try:
with open(folder + "mod.conf", "r", encoding='utf-8') as mod_conf:
for line in mod_conf:
match = pattern_name.match(line)
if match:
return match.group(1)
except FileNotFoundError:
pass
return None

#If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
out = []
for root, dirs, files in os.walk(folder + 'locale/'):
for name in files:
if pattern_tr_filename.search(name):
out.append(name)
return out

# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
# The first three items are for unused matches
text = re.sub(r'#~ msgid "', "", text)
text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
text = re.sub(r'"\n#~ msgstr "', "=", text)
# comment lines
text = re.sub(r'#.*\n', "", text)
# converting msg pairs into "=" pairs
text = re.sub(r'msgid "', "", text)
text = re.sub(r'"\nmsgstr ""\n"', "=", text)
text = re.sub(r'"\nmsgstr "', "=", text)
# various line breaks and escape codes
text = re.sub(r'"\n"', "", text)
text = re.sub(r'"\n', "\n", text)
text = re.sub(r'\\"', '"', text)
text = re.sub(r'\\n', '@n', text)
# remove header text
text = re.sub(r'=Project-Id-Version:.*\n', "", text)
# remove double-spaced lines
text = re.sub(r'\n\n', '\n', text)
return text

# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
for root, dirs, files in os.walk(folder + 'locale/'):
for name in files:
code_match = pattern_po_language_code.match(name)
if code_match == None:
continue
language_code = code_match.group(1)
tr_name = modname + "." + language_code + ".tr"
tr_file = os.path.join(root, tr_name)
if os.path.exists(tr_file):
print(tr_name + " already exists, ignoring " + name)
continue
fname = os.path.join(root, name)
with open(fname, "r", encoding='utf-8') as po_file:
print("Importing translations from " + name)
text = process_po_file(po_file.read())
with open(tr_file, "wt", encoding='utf-8') as tr_out:
tr_out.write(text)

# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise

# Writes a template.txt file
def write_template(templ_file, lkeyStrings):
lOut = []
lkeyStrings.sort()
for s in lkeyStrings:
lOut.append("%s=" % s)
mkdir_p(os.path.dirname(templ_file))
with open(templ_file, "wt", encoding='utf-8') as template_file:
template_file.write("\n".join(lOut))

# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
lOut = []
with open(lua_file, encoding='utf-8') as text_file:
text = text_file.read()
text = re.sub(pattern_concat, "", text)
for s in pattern_lua.findall(text):
s = s[1]
s = re.sub(r'"\.\.\s+"', "", s)
s = re.sub("@[^@=0-9]", "@@", s)
s = s.replace('\\"', '"')
s = s.replace("\\'", "'")
s = s.replace("\n", "@n")
s = s.replace("\\n", "@n")
s = s.replace("=", "@=")
lOut.append(s)
return lOut

# Gets strings from an existing translation file
def import_tr_file(tr_file):
dOut = {}
if os.path.exists(tr_file):
with open(tr_file, "r", encoding='utf-8') as existing_file :
for line in existing_file.readlines():
s = line.strip()
if s == "" or s[0] == "#":
continue
match = pattern_tr.match(s)
if match:
dOut[match.group(1)] = match.group(2)
return dOut

# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
def generate_template(folder):
lOut = []
for root, dirs, files in os.walk(folder):
for name in files:
if fnmatch.fnmatch(name, "*.lua"):
fname = os.path.join(root, name)
found = read_lua_file_strings(fname)
print(fname + ": " + str(len(found)) + " translatable strings")
lOut.extend(found)
lOut = list(set(lOut))
lOut.sort()
if len(lOut) == 0:
return None
templ_file = folder + "locale/template.txt"
write_template(templ_file, lOut)
return lOut

# Updates an existing .tr file, copying the old one to a ".old" file
def update_tr_file(lNew, mod_name, tr_file):
print("updating " + tr_file)
lOut = ["# textdomain: %s\n" % mod_name]

#TODO only make a .old if there are actual changes from the old file
if os.path.exists(tr_file):
shutil.copyfile(tr_file, tr_file+".old")

dOld = import_tr_file(tr_file)
for key in lNew:
val = dOld.get(key, "")
lOut.append("%s=%s" % (key, val))
lOut.append("##### not used anymore #####")
for key in dOld:
if key not in lNew:
lOut.append("%s=%s" % (key, dOld[key]))
with open(tr_file, "w", encoding='utf-8') as new_tr_file:
new_tr_file.write("\n".join(lOut))

# Updates translation files for the mod in the given folder
def update_mod(folder):
modname = get_modname(folder)
if modname is not None:
process_po_files(folder, modname)
print("Updating translations for " + modname)
data = generate_template(folder)
if data == None:
print("No translatable strings found in " + modname)
else:
for tr_file in get_existing_tr_files(folder):
update_tr_file(data, modname, folder + "locale/" + tr_file)
else:
print("Unable to find modname in folder " + folder)

def update_folder(folder):
is_modpack = os.path.exists(folder+"modpack.txt") or os.path.exists(folder+"modpack.conf")
if is_modpack:
subfolders = [f.path for f in os.scandir(folder) if f.is_dir()]
for subfolder in subfolders:
update_mod(subfolder + "/")
else:
update_mod(folder)
print("Done.")


update_folder("./")

# Runs this script on each sub-folder in the parent folder.
# I'm using this for testing this script on all installed mods.
#for modfolder in [f.path for f in os.scandir("../") if f.is_dir()]:
# update_folder(modfolder + "/")
6 changes: 2 additions & 4 deletions init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ end
anvil.make_unrepairable("technic:water_can")
anvil.make_unrepairable("technic:lava_can")

-- internationalization boilerplate
local MP = minetest.get_modpath(minetest.get_current_modname())
local S, NS = dofile(MP.."/intllib.lua")
local S = minetest.get_translator(minetest.get_current_modname())

-- the hammer for the anvil

Expand Down Expand Up @@ -170,7 +168,7 @@ minetest.register_node("anvil:anvil", {
after_place_node = function(pos, placer)
local meta = minetest.get_meta(pos)
meta:set_string("owner", placer:get_player_name() or "")
meta:set_string("infotext",placer:get_player_name().."'s anvil")
meta:set_string("infotext", S("@1's anvil", placer:get_player_name()))
end,

can_dig = function(pos,player)
Expand Down
45 changes: 0 additions & 45 deletions intllib.lua

This file was deleted.

22 changes: 22 additions & 0 deletions locale/anvil.de.tr
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# textdomain: anvil

@1 cannot be repaired with an anvil.=
@1's anvil=
A tool for repairing other tools at a blacksmith's anvil.=Stahlhammer um Werkzeuge auf dem Amboss zu reparieren
A tool for repairing other tools in conjunction with a blacksmith's hammer.=
Anvil=Amboss
Right-click on this anvil with a damaged tool to place the damaged tool upon it. You can then repair the damaged tool by striking it with a blacksmith's hammer. Repeated blows may be necessary to fully repair a badly worn tool. To retrieve the tool either punch or right-click the anvil with an empty hand.=
Steel blacksmithing hammer=
This anvil is for damaged tools only.=Das Werkstueckfeld gilt nur fuer beschaedigtes Werkzeug.
Use this hammer to strike blows upon an anvil bearing a damaged tool and you can repair it. It can also be used for smashing stone, but it is not well suited to this task.=
Your @1 has been repaired successfully.=
##### not used anymore #####
Workpiece:=Werkstueck:
Optional=Moegliche
storage for=Aufbewahrung fuer
your hammer=deinen Hammer
Punch anvil with hammer to=Schlage mit dem Hammer auf den Amboss um
repair tool in workpiece-slot.=das Werkzeug im Werkstueckfeld zu reparieren.
anvil=Amboss
Anvil (owned by %s)=Amboss (gehoert %s)
Owner: %s=Besitzer: %s
13 changes: 13 additions & 0 deletions locale/anvil.es.tr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# textdomain: anvil

@1 cannot be repaired with an anvil.=
@1's anvil=
A tool for repairing other tools at a blacksmith's anvil.=Es una herramienta para reparar otras herramientas en el yunque del herrero
A tool for repairing other tools in conjunction with a blacksmith's hammer.=Es una herramienta para reparar de herramientas dañadas en conjunto con el martillo del herrero.
Anvil=Yunque
Right-click on this anvil with a damaged tool to place the damaged tool upon it. You can then repair the damaged tool by striking it with a blacksmith's hammer. Repeated blows may be necessary to fully repair a badly worn tool. To retrieve the tool either punch or right-click the anvil with an empty hand.=Haga clic derecho sobre este yunque con una herramienta dañada Puede reparar la herramienta dañada golpeándola con el martillo del herrero Para reparar completamente una herramienta puede dar varios golpes Para sacar la herramienta, golpeela con la mano vacia o tambien con un clic derecho
Steel blacksmithing hammer=Martillo de acero para la herrería
This anvil is for damaged tools only.=Este yunque es sólo para herramientas dañadas
Use this hammer to strike blows upon an anvil bearing a damaged tool and you can repair it. It can also be used for smashing stone, but it is not well suited to this task.=Use este martillo para dar golpes sobre el yunque donde puso la herramienta dañada Tambien puede ser usado para romper piedra pero no es muy adecuado para esa tarea.
Your @1 has been repaired successfully.=Su @1 ha sido reparado correctamente.
##### not used anymore #####
13 changes: 13 additions & 0 deletions locale/anvil.fr.tr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# textdomain: anvil

@1 cannot be repaired with an anvil.=
@1's anvil=
A tool for repairing other tools at a blacksmith's anvil.=Un outil pour réparer les autres outils avec une enclume de forgeron.
A tool for repairing other tools in conjunction with a blacksmith's hammer.=Un outil pour réparer les autres outils à utiliser avec un marteau de forgeron.
Anvil=Enclume
Right-click on this anvil with a damaged tool to place the damaged tool upon it. You can then repair the damaged tool by striking it with a blacksmith's hammer. Repeated blows may be necessary to fully repair a badly worn tool. To retrieve the tool either punch or right-click the anvil with an empty hand.=Cliquez-droit sur cette enclume avec un outil endommagé pour le placer dessus. Vous pourrez alors réparer l'outil endommagé en le frappant avec un marteau de forgeron. Des coups successifs seront nécessaires pour réparer l'outil entièrement. Pour récupérer l'outil, frappez dessus ou faites un click-droit en ayant la main vide.
Steel blacksmithing hammer=Marteau de forgeron en acier
This anvil is for damaged tools only.=L'enclume s'utilise sur les outils endommagés.
Use this hammer to strike blows upon an anvil bearing a damaged tool and you can repair it. It can also be used for smashing stone, but it is not well suited to this task.=Utilisez ce marteau pour frapper une enclume contenant un outil endommagé, ainsi vous pourrez le réparer. Il peut être aussi utilisé pour casser de la pierre, mais il n'est pas adapté à cette tâche.
Your @1 has been repaired successfully.=Votre @1 a été réparé avec succès.
##### not used anymore #####
13 changes: 13 additions & 0 deletions locale/anvil.it.tr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# textdomain: anvil

@1 cannot be repaired with an anvil.=
@1's anvil=
A tool for repairing other tools at a blacksmith's anvil.=Un attrezzo per riparare altri attrezzi su di una incudine da fabbro.
A tool for repairing other tools in conjunction with a blacksmith's hammer.=Un attrezzo per riparare altri attrezzi usando un martello da fabbro.
Anvil=Incudine
Right-click on this anvil with a damaged tool to place the damaged tool upon it. You can then repair the damaged tool by striking it with a blacksmith's hammer. Repeated blows may be necessary to fully repair a badly worn tool. To retrieve the tool either punch or right-click the anvil with an empty hand.=Fate click destro su questa incudine con un attrezzo danneggiato per metterlo sull'incudine. Poi potrete ripararlo colpendolo con un martello da fabbro. Potrebbero essere necessari più colpi per riparare un attrezzo gravemente danneggiato. Per riprendere l'attrezzo colpite o fate click destro sull'incudine a mani vuote.
Steel blacksmithing hammer=Martello da fabbro di acciaio
This anvil is for damaged tools only.=Questa incudine è solo per attrezzi danneggiati.
Use this hammer to strike blows upon an anvil bearing a damaged tool and you can repair it. It can also be used for smashing stone, but it is not well suited to this task.=Usate questo martello per colpire una incudine su cui è posto un attrezzo danneggiato e potrete ripararlo. Può anche essere usato per colpire la pietra, ma non è molto adatto a questo compito.
Your @1 has been repaired successfully.=La/il vostr* @1 è stat* riparat* con successo.
##### not used anymore #####
Loading

0 comments on commit a035f10

Please sign in to comment.