--- /dev/null
+#! /usr/bin/env lua
+
+local me = arg[0]:gsub(".*[/\\](.*)$", "%1")
+
+local function err(fmt, ...)
+ io.stderr:write(("%s: %s\n"):format(me, fmt:format(...)))
+ os.exit(1)
+end
+
+local output
+local inputs = { }
+local lang
+local author
+
+local i = 1
+
+local function usage()
+ print([[
+Usage: ]]..me..[[ [OPTIONS] FILE...
+
+Extract translatable strings from the given FILE(s).
+
+Available options:
+ -h,--help Show this help screen and exit.
+ -o,--output X Set output file (default: stdout).
+ -a,--author X Set author.
+ -l,--lang X Set language name.
+]])
+ os.exit(0)
+end
+
+while i <= #arg do
+ local a = arg[i]
+ if (a == "-h") or (a == "--help") then
+ usage()
+ elseif (a == "-o") or (a == "--output") then
+ i = i + 1
+ if i > #arg then
+ err("missing required argument to `%s'", a)
+ end
+ output = arg[i]
+ elseif (a == "-a") or (a == "--author") then
+ i = i + 1
+ if i > #arg then
+ err("missing required argument to `%s'", a)
+ end
+ author = arg[i]
+ elseif (a == "-l") or (a == "--lang") then
+ i = i + 1
+ if i > #arg then
+ err("missing required argument to `%s'", a)
+ end
+ lang = arg[i]
+ elseif a:sub(1, 1) ~= "-" then
+ table.insert(inputs, a)
+ else
+ err("unrecognized option `%s'", a)
+ end
+ i = i + 1
+end
+
+if #inputs == 0 then
+ err("no input files")
+end
+
+local outfile = io.stdout
+
+local function printf(fmt, ...)
+ outfile:write(fmt:format(...))
+end
+
+if output then
+ local e
+ outfile, e = io.open(output, "w")
+ if not outfile then
+ err("error opening file for writing: %s", e)
+ end
+end
+
+if author or lang then
+ outfile:write("\n")
+end
+
+if lang then
+ printf("# Language: %s\n", lang)
+end
+
+if author then
+ printf("# Author: %s\n", author)
+end
+
+if author or lang then
+ outfile:write("\n")
+end
+
+local c_escapes = {
+ [('a'):byte(1)] = '\a',
+ [('b'):byte(1)] = '\b',
+ [('f'):byte(1)] = '\f',
+ [('r'):byte(1)] = '\r',
+ [('t'):byte(1)] = '\t',
+ [('v'):byte(1)] = '\v',
+ -- \n is handled separately
+}
+
+local function parse_lua_string(s)
+ local esc = false
+ local i = 1
+ local len = #s
+
+ while i <= len do
+ local c = s:byte(i)
+ i = i + 1
+ if esc then
+ esc = false
+ if c >= 0x30 and c <= 0x39 then
+ -- 0x30 = 0
+ -- 0x39 = 9
+ local scode = s:match('%d%d?%d?', i - 1)
+ local ncode = tonumber(scode)
+ s = s:sub(1, i - 3) .. string.char(ncode) .. s:sub(i-1 + #scode)
+ -- Reevaluate the current character only if it isn't \
+ i = i - (ncode == 0x5C and 1 or 2)
+ len = #s
+ elseif c == 0x6E then
+ -- 0x6E = n
+ s = s:sub(1, i - 3) .. "@n" .. s:sub(i)
+ elseif c == 0x78 then
+ -- 0x78 = x
+ s = s:sub(1, i - 3) .. s:sub(i - 1)
+ i = i - 2
+ len = len - 1
+ io.stderr:write("Warning: Hex escape sequence is illegal in Lua 5.1\n")
+ elseif c_escapes[c] ~= nil then
+ s = s:sub(1, i - 3) .. c_escapes[c] .. s:sub(i)
+ len = len - 1
+ i = i - 1
+ else
+ s = s:sub(1, i - 3) .. s:sub(i - 1)
+ len = len - 1
+ -- Reevaluate the current character only if it isn't \
+ i = i - (c == 0x5C and 1 or 2)
+ end
+ elseif c == 0x5C then
+ -- 0x5C = \
+ esc = true
+ elseif c == 0x0A then
+ -- 0x0A = LF
+ s = s:sub(1, i - 2) .. "@n" .. s:sub(i)
+ len = len + 1
+ i = i + 1
+ elseif c == 0x3D then
+ -- 0x3D = =
+ s = s:sub(1, i - 2) .. "@=" .. s:sub(i)
+ len = len + 1
+ i = i + 1
+ elseif c == 0x23 and i == 2 then
+ -- 0x23 = #
+ s = '@' .. s
+ len = len + 1
+ i = i + 1
+ end
+ end
+ return s
+end
+
+local function replace_quote_in_quote(s)
+ --[[
+ state = 0: normal code, starting state
+ state = 1: seen -
+ state = 2: seen " or ' (begin string parsing)
+ state = 3: seen \ within string
+ --]]
+ local state = 0
+ local i = 1
+ local len = #s
+ local end_str
+
+ while i <= len do
+ local c = s:byte(i)
+ i = i + 1
+ if state == 0 then
+ if c == 0x2D then
+ -- 0x2D = -
+ state = 1
+ elseif c == 0x22 or c == 0x27 then
+ -- 0x22 = "
+ -- 0x27 = '
+ end_str = c
+ state = 2
+ -- else remain in state 0
+ end
+ elseif state == 1 then
+ if c == 0x2D then
+ -- 0x2D = -
+ -- Ignore the rest of the line. We don't parse --[[ ... ]].
+ return s:sub(1, i - 3)
+ elseif c == 0x22 or c == 0x27 then
+ -- 0x22 = "
+ -- 0x27 = '
+ end_str = c
+ state = 3
+ else
+ state = 0
+ end
+ elseif state == 2 then
+ if c == 0x5C then
+ -- 0x5C = \
+ state = 3
+ elseif c == end_str then
+ state = 0
+ elseif c == 0x22 or c == 0x27 or c == 0x28 then
+ -- " or ' or open parenthesis
+ s = s:sub(1, i - 2) .. ("\\%03d"):format(c) .. s:sub(i)
+ i = i + 3
+ len = len + 3
+ -- else remain in state 2
+ end
+ elseif state == 3 then
+ if c == 0x22 or c == 0x27 then
+ -- Escaped quote found - replace it
+ s = s:sub(1, i - 2) .. (c == 0x22 and "034" or "039") .. s:sub(i)
+ i = i + 2
+ len = len + 2
+ state = 2
+ else
+ state = 2
+ end
+ end
+ end
+ assert(#s == len)
+ return s
+end
+
+local messages = {}
+
+for _, file in ipairs(inputs) do
+ local infile, e = io.open(file, "r")
+ local textdomains = {}
+ if infile then
+ for line in infile:lines() do
+ for translator_name, textdomain in line:gmatch('local (%w+)%s*=%s*%w+%.get_translator%("([^"]*)"%)') do
+ --print(translator_name, textdomain)
+ messages[textdomain] = messages[textdomain] or {}
+ textdomains[translator_name] = textdomain
+ end
+ line = replace_quote_in_quote(line)
+ for translator, s in line:gmatch('(%w+)%("([^"]*)"') do
+ s = parse_lua_string(s)
+ if textdomains[translator] then
+ local textdomain = textdomains[translator]
+ table.insert(messages[textdomain], s)
+ end
+ end
+ for textdomain, s in line:gmatch('%w+%.translate%("([^"]*)"%s*,%s*"([^"]*)"') do
+ s = parse_lua_string(s)
+ messages[textdomain] = messages[textdomain] or {}
+ table.insert(messages[textdomain], s)
+ end
+ end
+ infile:close()
+ else
+ io.stderr:write(("%s: WARNING: error opening file: %s\n"):format(me, e))
+ end
+end
+
+for textdomain, mtbl in pairs(messages) do
+ table.sort(messages[textdomain])
+
+ local last_msg
+ printf("# textdomain: %s\n", textdomain)
+
+ for _, msg in ipairs(messages[textdomain]) do
+ if msg ~= last_msg then
+ printf("%s=\n", msg)
+ end
+ last_msg = msg
+ end
+end
+
+if output then
+ outfile:close()
+end
+
+--[[
+TESTS:
+local S = minetest.get_translator("domain")
+S("foo") S("bar")
+S("bar")
+S("foo") -- S("doesn't matter")
+print("this is in a string S(") x=0 print(") still text") S('bar baz "this" \"\'that\' foobar')
+minetest.translate("another_domain", "foo")
+S("#foo=@1\n@2", "bar", "baz")
+S("what's this? (oh, an apostrophe)")
+S("\035 is a #")
+S("\092 is a \\")
+S("\\ is a \\")
+S("\# is a #")
+]]
--- /dev/null
+#! /usr/bin/env lua
+
+local me = arg[0]:gsub(".*[/\\](.*)$", "%1")
+
+local function err(fmt, ...)
+ io.stderr:write(("%s: %s\n"):format(me, fmt:format(...)))
+ os.exit(1)
+end
+
+local output, outfile, template
+local catalogs = { }
+
+local function usage()
+ print([[
+Usage: ]]..me..[[ [OPTIONS] TEMPLATE CATALOG...
+
+Update a catalog with new strings from a template.
+
+Available options:
+ -h,--help Show this help screen and exit.
+ -o,--output X Set output file (default: stdout).
+
+Messages in the template that are not on the catalog are added to the
+catalog at the end.
+
+This tool also checks messages that are in the catalog but not in the
+template, and reports such lines. It's up to the user to remove such
+lines, if so desired.
+]])
+ os.exit(0)
+end
+
+local i = 1
+
+while i <= #arg do
+ local a = arg[i]
+ if (a == "-h") or (a == "--help") then
+ usage()
+ elseif (a == "-o") or (a == "--output") then
+ i = i + 1
+ if i > #arg then
+ err("missing required argument to `%s'", a)
+ end
+ output = arg[i]
+ elseif a:sub(1, 1) ~= "-" then
+ if not template then
+ template = a
+ else
+ table.insert(catalogs, a)
+ end
+ else
+ err("unrecognized option `%s'", a)
+ end
+ i = i + 1
+end
+
+if not template then
+ err("no template specified")
+elseif #catalogs == 0 then
+ err("no catalogs specified")
+end
+
+local f, e = io.open(template, "r")
+if not f then
+ err("error opening template: %s", e)
+end
+
+local escapes = {
+ ["\n"] = "@n",
+ ["="] = "@=",
+}
+
+local function escape(s)
+ local r = s:gsub("\\n", "@n"):gsub("[\n=]", escapes)
+ if r == "" or not r:sub(1, 1) == "#" then
+ return r
+ else
+ -- Escape '#' at beginning of line
+ return "@" .. r
+ end
+end
+
+if output then
+ outfile, e = io.open(output, "w")
+ if not outfile then
+ err("error opening file for writing: %s", e)
+ end
+end
+
+local function load_strings(file)
+ local infile, e = io.open(file, "r")
+ local messages = {}
+ local textdomain = ""
+ messages[""] = {}
+ if infile then
+ for line in infile:lines() do
+ for td in line:gmatch('# textdomain:%s*(%S+)') do
+ textdomain = td
+ messages[textdomain] = messages[textdomain] or {}
+ end
+ if not (line == "" or line:sub(1, 1) == "#") then
+ local i = 1
+ while i < line:len() do
+ if line:sub(i, i) == "@" then
+ i = i + 2
+ elseif line:sub(i, i) == "=" then
+ break
+ else
+ i = i + 1
+ end
+ end
+ local untranslated = line:sub(1, i - 1)
+ local translated = line:sub(i + 1)
+ messages[textdomain][untranslated] = translated
+ print(file, textdomain, untranslated, translated)
+ end
+ end
+ infile:close()
+ else
+ io.stderr:write(("%s: WARNING: error opening file: %s\n"):format(me, e))
+ end
+ return messages
+end
+
+
+local template_msgs = load_strings(template)
+for _, file in ipairs(catalogs) do
+ print("Processing: "..file)
+ local catalog_msgs = load_strings(file)
+ local dirty_lines = {}
+ local dirty = false
+ if catalog_msgs then
+ -- Add new entries from template.
+ for textdomain, tm in pairs(template_msgs) do
+ for k in pairs(tm) do
+ if not catalog_msgs[textdomain][k] then
+ print("NEW: "..textdomain.." "..k)
+ dirty_lines[textdomain] = dirty_lines[textdomain] or {}
+ table.insert(dirty_lines[textdomain], k.."=")
+ dirty = true
+ end
+ end
+ end
+ -- Check for old messages.
+ for textdomain, cm in pairs(catalog_msgs) do
+ for k, v in pairs(cm) do
+ if not template_msgs[textdomain][k] then
+ print("OLD: "..textdomain.." "..k)
+ dirty_lines[textdomain] = dirty_lines[textdomain] or {}
+ table.insert(dirty_lines[textdomain], "# OLD: "..k.."="..v)
+ dirty = true
+ end
+ end
+ end
+ if dirty then
+ local outf
+ outf, e = io.open(file, "a+")
+ if outf then
+ for textdomain, dl in pairs(dirty_lines) do
+ for _, line in ipairs(dl) do
+ outf:write(line)
+ outf:write("\n")
+ end
+ end
+ outf:close()
+ else
+ io.stderr:write(("%s: WARNING: cannot write: %s\n"):format(me, e))
+ end
+ end
+ else
+ io.stderr:write(("%s: WARNING: could not load catalog\n"):format(me))
+ end
+end