You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
4.9 KiB
226 lines
4.9 KiB
|
|
local strsub, strrep = string.sub, string.rep |
|
local strmatch, strgsub = string.match, string.gsub |
|
|
|
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] or 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 ipairs(str:split("\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") |
|
|
|
-- luacheck: ignore |
|
until true end -- end for |
|
|
|
return texts |
|
end |
|
|
|
local M = { } |
|
|
|
local function warn(msg) |
|
minetest.log("warning", "[intllib] "..msg) |
|
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(s) |
|
local c, t, f = strmatch(s, "^(.-)%?(.-):(.*)") |
|
if c then |
|
return ("__if(" |
|
..replace_ternary(c) |
|
..","..replace_ternary(t) |
|
..","..replace_ternary(f) |
|
..")") |
|
end |
|
return s |
|
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 ipairs(str:split("\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 ": " or "")..(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 |
|
return bail("catalog has no headers") |
|
end |
|
|
|
hdrs = parse_headers(hdrs[0]) |
|
|
|
local pf = hdrs["Plural-Forms"] |
|
if not pf then |
|
-- XXX: Is this right? Gettext assumes this if header not present. |
|
pf = "nplurals=2; plural=n != 1" |
|
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) |
|
local langs = intllib.get_detected_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
|
|
|