gettext.lua 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. local strsub, strrep = string.sub, string.rep
  2. local strmatch, strgsub = string.match, string.gsub
  3. local function trim(str)
  4. return strmatch(str, "^%s*(.-)%s*$")
  5. end
  6. local escapes = { n="\n", r="\r", t="\t" }
  7. local function unescape(str)
  8. return (strgsub(str, "(\\+)([nrt]?)", function(bs, c)
  9. local bsl = #bs
  10. local realbs = strrep("\\", bsl/2)
  11. if bsl%2 == 1 then
  12. c = escapes[c] or c
  13. end
  14. return realbs..c
  15. end))
  16. end
  17. local function parse_po(str)
  18. local state, msgid, msgid_plural, msgstrind
  19. local texts = { }
  20. local lineno = 0
  21. local function perror(msg)
  22. return error(msg.." at line "..lineno)
  23. end
  24. for _, line in ipairs(str:split("\n")) do repeat
  25. lineno = lineno + 1
  26. line = trim(line)
  27. if line == "" or strmatch(line, "^#") then
  28. state, msgid, msgid_plural = nil, nil, nil
  29. break -- continue
  30. end
  31. local mid = strmatch(line, "^%s*msgid%s*\"(.*)\"%s*$")
  32. if mid then
  33. if state == "id" then
  34. return perror("unexpected msgid")
  35. end
  36. state, msgid = "id", unescape(mid)
  37. break -- continue
  38. end
  39. mid = strmatch(line, "^%s*msgid_plural%s*\"(.*)\"%s*$")
  40. if mid then
  41. if state ~= "id" then
  42. return perror("unexpected msgid_plural")
  43. end
  44. state, msgid_plural = "idp", unescape(mid)
  45. break -- continue
  46. end
  47. local ind, mstr = strmatch(line,
  48. "^%s*msgstr([0-9%[%]]*)%s*\"(.*)\"%s*$")
  49. if ind then
  50. if not msgid then
  51. return perror("missing msgid")
  52. elseif ind == "" then
  53. msgstrind = 0
  54. elseif strmatch(ind, "%[[0-9]+%]") then
  55. msgstrind = tonumber(strsub(ind, 2, -2))
  56. else
  57. return perror("malformed msgstr")
  58. end
  59. texts[msgid] = texts[msgid] or { }
  60. if msgid_plural then
  61. texts[msgid_plural] = texts[msgid]
  62. end
  63. texts[msgid][msgstrind] = unescape(mstr)
  64. state = "str"
  65. break -- continue
  66. end
  67. mstr = strmatch(line, "^%s*\"(.*)\"%s*$")
  68. if mstr then
  69. if state == "id" then
  70. msgid = msgid..unescape(mstr)
  71. break -- continue
  72. elseif state == "idp" then
  73. msgid_plural = msgid_plural..unescape(mstr)
  74. break -- continue
  75. elseif state == "str" then
  76. local text = texts[msgid][msgstrind]
  77. texts[msgid][msgstrind] = text..unescape(mstr)
  78. break -- continue
  79. end
  80. end
  81. return perror("malformed line")
  82. -- luacheck: ignore
  83. until true end -- end for
  84. return texts
  85. end
  86. local M = { }
  87. local function warn(msg)
  88. minetest.log("warning", "[intllib] "..msg)
  89. end
  90. -- hax!
  91. -- This function converts a C expression to an equivalent Lua expression.
  92. -- It handles enough stuff to parse the `Plural-Forms` header correctly.
  93. -- Note that it assumes the C expression is valid to begin with.
  94. local function compile_plural_forms(str)
  95. local plural = strmatch(str, "plural=([^;]+);?$")
  96. local function replace_ternary(s)
  97. local c, t, f = strmatch(s, "^(.-)%?(.-):(.*)")
  98. if c then
  99. return ("__if("
  100. ..replace_ternary(c)
  101. ..","..replace_ternary(t)
  102. ..","..replace_ternary(f)
  103. ..")")
  104. end
  105. return s
  106. end
  107. plural = replace_ternary(plural)
  108. plural = strgsub(plural, "&&", " and ")
  109. plural = strgsub(plural, "||", " or ")
  110. plural = strgsub(plural, "!=", "~=")
  111. plural = strgsub(plural, "!", " not ")
  112. local f, err = loadstring([[
  113. local function __if(c, t, f)
  114. if c and c~=0 then return t else return f end
  115. end
  116. local function __f(n)
  117. return (]]..plural..[[)
  118. end
  119. return (__f(...))
  120. ]])
  121. if not f then return nil, err end
  122. local env = { }
  123. env._ENV, env._G = env, env
  124. setfenv(f, env)
  125. return function(n)
  126. local v = f(n)
  127. if type(v) == "boolean" then
  128. -- Handle things like a plain `n != 1`
  129. v = v and 1 or 0
  130. end
  131. return v
  132. end
  133. end
  134. local function parse_headers(str)
  135. local headers = { }
  136. for _, line in ipairs(str:split("\n")) do
  137. local k, v = strmatch(line, "^([^:]+):%s*(.*)")
  138. if k then
  139. headers[k] = v
  140. end
  141. end
  142. return headers
  143. end
  144. local function load_catalog(filename)
  145. local f, data, err
  146. local function bail(msg)
  147. warn(msg..(err and ": " or "")..(err or ""))
  148. return nil
  149. end
  150. f, err = io.open(filename, "rb")
  151. if not f then
  152. return --bail("failed to open catalog")
  153. end
  154. data, err = f:read("*a")
  155. f:close()
  156. if not data then
  157. return bail("failed to read catalog")
  158. end
  159. data, err = parse_po(data)
  160. if not data then
  161. return bail("failed to parse catalog")
  162. end
  163. err = nil
  164. local hdrs = data[""]
  165. if not (hdrs and hdrs[0]) then
  166. return bail("catalog has no headers")
  167. end
  168. hdrs = parse_headers(hdrs[0])
  169. local pf = hdrs["Plural-Forms"]
  170. if not pf then
  171. -- XXX: Is this right? Gettext assumes this if header not present.
  172. pf = "nplurals=2; plural=n != 1"
  173. end
  174. data.plural_index, err = compile_plural_forms(pf)
  175. if not data.plural_index then
  176. return bail("failed to compile plural forms")
  177. end
  178. --warn("loaded: "..filename)
  179. return data
  180. end
  181. function M.load_catalogs(path)
  182. local langs = intllib.get_detected_languages()
  183. local cats = { }
  184. for _, lang in ipairs(langs) do
  185. local cat = load_catalog(path.."/"..lang..".po")
  186. if cat then
  187. cats[#cats+1] = cat
  188. end
  189. end
  190. return cats
  191. end
  192. return M