mirror of
https://github.com/minetest-mods/intllib.git
synced 2025-07-04 09:00:21 +02:00
Add support for gettext message catalogs.
This commit is contained in:
288
gettext.lua
Normal file
288
gettext.lua
Normal file
@ -0,0 +1,288 @@
|
||||
|
||||
local strfind, strsub, strrep = string.find, string.sub, string.rep
|
||||
local strmatch, strgsub = string.match, string.gsub
|
||||
local floor = math.floor
|
||||
|
||||
local function split(str, sep)
|
||||
local pos, endp = 1, #str+1
|
||||
return function()
|
||||
if (not pos) or pos > endp then return end
|
||||
local s, e = strfind(str, sep, pos, true)
|
||||
local part = strsub(str, pos, s and s-1)
|
||||
pos = e and e + 1
|
||||
return part
|
||||
end
|
||||
end
|
||||
|
||||
local function trim(str)
|
||||
return strmatch(str, "^%s*(.-)%s*$")
|
||||
end
|
||||
|
||||
local escapes = { n="\n", r="\r", t="\t" }
|
||||
|
||||
local function unescape(str)
|
||||
return (strgsub(str, "(\\+)([nrt]?)", function(bs, c)
|
||||
local bsl = #bs
|
||||
local realbs = strrep("\\", bsl/2)
|
||||
if bsl%2 == 1 then
|
||||
c = escapes[c]
|
||||
end
|
||||
return realbs..c
|
||||
end))
|
||||
end
|
||||
|
||||
local function parse_po(str)
|
||||
local state, msgid, msgid_plural, msgstrind
|
||||
local texts = { }
|
||||
local lineno = 0
|
||||
local function perror(msg)
|
||||
return error(msg.." at line "..lineno)
|
||||
end
|
||||
for line in split(str, "\n") do repeat
|
||||
lineno = lineno + 1
|
||||
line = trim(line)
|
||||
|
||||
if line == "" or strmatch(line, "^#") then
|
||||
state, msgid, msgid_plural = nil, nil, nil
|
||||
break -- continue
|
||||
end
|
||||
|
||||
local mid = strmatch(line, "^%s*msgid%s*\"(.*)\"%s*$")
|
||||
if mid then
|
||||
if state == "id" then
|
||||
return perror("unexpected msgid")
|
||||
end
|
||||
state, msgid = "id", unescape(mid)
|
||||
break -- continue
|
||||
end
|
||||
|
||||
mid = strmatch(line, "^%s*msgid_plural%s*\"(.*)\"%s*$")
|
||||
if mid then
|
||||
if state ~= "id" then
|
||||
return perror("unexpected msgid_plural")
|
||||
end
|
||||
state, msgid_plural = "idp", unescape(mid)
|
||||
break -- continue
|
||||
end
|
||||
|
||||
local ind, mstr = strmatch(line,
|
||||
"^%s*msgstr([0-9%[%]]*)%s*\"(.*)\"%s*$")
|
||||
if ind then
|
||||
if not msgid then
|
||||
return perror("missing msgid")
|
||||
elseif ind == "" then
|
||||
msgstrind = 0
|
||||
elseif strmatch(ind, "%[[0-9]+%]") then
|
||||
msgstrind = tonumber(strsub(ind, 2, -2))
|
||||
else
|
||||
return perror("malformed msgstr")
|
||||
end
|
||||
texts[msgid] = texts[msgid] or { }
|
||||
if msgid_plural then
|
||||
texts[msgid_plural] = texts[msgid]
|
||||
end
|
||||
texts[msgid][msgstrind] = unescape(mstr)
|
||||
state = "str"
|
||||
break -- continue
|
||||
end
|
||||
|
||||
mstr = strmatch(line, "^%s*\"(.*)\"%s*$")
|
||||
if mstr then
|
||||
if state == "id" then
|
||||
msgid = msgid..unescape(mstr)
|
||||
break -- continue
|
||||
elseif state == "idp" then
|
||||
msgid_plural = msgid_plural..unescape(mstr)
|
||||
break -- continue
|
||||
elseif state == "str" then
|
||||
local text = texts[msgid][msgstrind]
|
||||
texts[msgid][msgstrind] = text..unescape(mstr)
|
||||
break -- continue
|
||||
end
|
||||
end
|
||||
|
||||
return perror("malformed line")
|
||||
|
||||
until true end -- end for
|
||||
|
||||
return texts
|
||||
end
|
||||
|
||||
local M = { }
|
||||
|
||||
local domains = { }
|
||||
local dgettext_cache = { }
|
||||
local dngettext_cache = { }
|
||||
local langs
|
||||
|
||||
local function detect_languages()
|
||||
if langs then return langs end
|
||||
|
||||
langs = { }
|
||||
|
||||
local function addlang(l)
|
||||
local sep
|
||||
langs[#langs+1] = l
|
||||
sep = strfind(l, ".", 1, true)
|
||||
if sep then
|
||||
l = strsub(l, 1, sep-1)
|
||||
langs[#langs+1] = l
|
||||
end
|
||||
sep = strfind(l, "_", 1, true)
|
||||
if sep then
|
||||
langs[#langs+1] = strsub(l, 1, sep-1)
|
||||
end
|
||||
end
|
||||
|
||||
local v
|
||||
|
||||
v = rawget(_G, "minetest") and minetest.setting_get("language")
|
||||
if v and v~="" then
|
||||
addlang(v)
|
||||
end
|
||||
|
||||
v = os.getenv("LANGUAGE")
|
||||
if v then
|
||||
for item in split(v, ":") do
|
||||
addlang(item)
|
||||
end
|
||||
end
|
||||
|
||||
v = os.getenv("LANG")
|
||||
if v then
|
||||
addlang(v)
|
||||
end
|
||||
|
||||
return langs
|
||||
end
|
||||
|
||||
local function warn(msg)
|
||||
if rawget(_G, "minetest") then
|
||||
minetest.log("warning", msg)
|
||||
else
|
||||
io.stderr:write("WARNING: ", msg, "\n")
|
||||
end
|
||||
end
|
||||
|
||||
-- hax!
|
||||
-- This function converts a C expression to an equivalent Lua expression.
|
||||
-- It handles enough stuff to parse the `Plural-Forms` header correctly.
|
||||
-- Note that it assumes the C expression is valid to begin with.
|
||||
local function compile_plural_forms(str)
|
||||
local plural = strmatch(str, "plural=([^;]+);?$")
|
||||
local function replace_ternary(str)
|
||||
local c, t, f = strmatch(str, "^(.-)%?(.-):(.*)")
|
||||
if c then
|
||||
return ("__if("
|
||||
..replace_ternary(c)
|
||||
..","..replace_ternary(t)
|
||||
..","..replace_ternary(f)
|
||||
..")")
|
||||
end
|
||||
return str
|
||||
end
|
||||
plural = replace_ternary(plural)
|
||||
plural = strgsub(plural, "&&", " and ")
|
||||
plural = strgsub(plural, "||", " or ")
|
||||
plural = strgsub(plural, "!=", "~=")
|
||||
plural = strgsub(plural, "!", " not ")
|
||||
local f, err = loadstring([[
|
||||
local function __if(c, t, f)
|
||||
if c and c~=0 then return t else return f end
|
||||
end
|
||||
local function __f(n)
|
||||
return (]]..plural..[[)
|
||||
end
|
||||
return (__f(...))
|
||||
]])
|
||||
if not f then return nil, err end
|
||||
local env = { }
|
||||
env._ENV, env._G = env, env
|
||||
setfenv(f, env)
|
||||
return function(n)
|
||||
local v = f(n)
|
||||
if type(v) == "boolean" then
|
||||
-- Handle things like a plain `n != 1`
|
||||
v = v and 1 or 0
|
||||
end
|
||||
return v
|
||||
end
|
||||
end
|
||||
|
||||
local function parse_headers(str)
|
||||
local headers = { }
|
||||
for line in split(str, "\n") do
|
||||
local k, v = strmatch(line, "^([^:]+):%s*(.*)")
|
||||
if k then
|
||||
headers[k] = v
|
||||
end
|
||||
end
|
||||
return headers
|
||||
end
|
||||
|
||||
local function load_catalog(filename)
|
||||
local f, data, err
|
||||
|
||||
local function bail(msg)
|
||||
warn(msg..(err and ": ")..(err or ""))
|
||||
return nil
|
||||
end
|
||||
|
||||
f, err = io.open(filename, "rb")
|
||||
if not f then
|
||||
return --bail("failed to open catalog")
|
||||
end
|
||||
|
||||
data, err = f:read("*a")
|
||||
|
||||
f:close()
|
||||
|
||||
if not data then
|
||||
return bail("failed to read catalog")
|
||||
end
|
||||
|
||||
data, err = parse_po(data)
|
||||
if not data then
|
||||
return bail("failed to parse catalog")
|
||||
end
|
||||
|
||||
err = nil
|
||||
local hdrs = data[""]
|
||||
if not (hdrs and hdrs[0]) then
|
||||
print(dump(hdrs))
|
||||
return bail("catalog has no headers")
|
||||
end
|
||||
|
||||
hdrs = parse_headers(hdrs[0])
|
||||
|
||||
local pf = hdrs["Plural-Forms"]
|
||||
if not pf then
|
||||
return bail("failed to load catalog:"
|
||||
.." catalog has no Plural-Forms header")
|
||||
end
|
||||
|
||||
data.plural_index, err = compile_plural_forms(pf)
|
||||
if not data.plural_index then
|
||||
return bail("failed to compile plural forms")
|
||||
end
|
||||
|
||||
--warn("loaded: "..filename)
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
function M.load_catalogs(path)
|
||||
detect_languages()
|
||||
|
||||
local cats = { }
|
||||
for _, lang in ipairs(langs) do
|
||||
local cat = load_catalog(path.."/"..lang..".po")
|
||||
if cat then
|
||||
cats[#cats+1] = cat
|
||||
end
|
||||
end
|
||||
|
||||
return cats
|
||||
end
|
||||
|
||||
return M
|
Reference in New Issue
Block a user