gluon-web: add i18n package namespaces

This commit is contained in:
Matthias Schiffer 2018-02-23 06:26:10 +01:00
parent 1a426c3bb9
commit 557565e189
No known key found for this signature in database
GPG Key ID: 16EF3F64CB201D9C
30 changed files with 377 additions and 315 deletions

View File

@ -1,14 +1,16 @@
return function(form, uci)
local pkg_i18n = i18n 'gluon-config-mode-autoupdater'
local enabled = uci:get_bool("autoupdater", "settings", "enabled")
if enabled then
form:section(
Section, nil,
translate('This node will automatically update its firmware when a new version is available.')
pkg_i18n.translate('This node will automatically update its firmware when a new version is available.')
)
else
form:section(
Section, nil,
translate('Automatic updates are disabled. They can be enabled in <em>Advanced settings</em>.')
pkg_i18n.translate('Automatic updates are disabled. They can be enabled in <em>Advanced settings</em>.')
)
end
end

View File

@ -1,16 +1,18 @@
return function(form, uci)
local pkg_i18n = i18n 'gluon-config-mode-contact-info'
local site = require 'gluon.site'
local owner = uci:get_first("gluon-node-info", "owner")
local s = form:section(Section, nil, translate(
local s = form:section(Section, nil, pkg_i18n.translate(
'Please provide your contact information here to '
.. 'allow others to contact you. Note that '
.. 'this information will be visible <em>publicly</em> '
.. 'on the internet together with your node\'s coordinates.'
))
local o = s:option(Value, "contact", translate("Contact info"), translate("e.g. E-mail or phone number"))
local o = s:option(Value, "contact", pkg_i18n.translate("Contact info"), pkg_i18n.translate("e.g. E-mail or phone number"))
o.default = uci:get("gluon-node-info", owner, "contact")
o.optional = not site.config_mode.owner.obligatory(false)
-- without a minimal length, an empty string will be accepted even with "optional = false"

View File

@ -1,7 +1,9 @@
<%-
local site_i18n = i18n 'gluon-site'
local sysconfig = require 'gluon.sysconfig'
local msg = _translate('gluon-config-mode:welcome')
local msg = site_i18n._translate('gluon-config-mode:welcome')
if not msg then return end
-%>
<p>

View File

@ -1,3 +1,5 @@
local site_i18n = i18n 'gluon-site'
local site = require 'gluon.site'
local sysconfig = require 'gluon.sysconfig'
local pretty_hostname = require 'pretty_hostname'
@ -7,7 +9,7 @@ local uci = require("simple-uci").cursor()
local hostname = pretty_hostname.get(uci)
local contact = uci:get_first('gluon-node-info', 'owner', 'contact')
local msg = _translate('gluon-config-mode:reboot')
local msg = site_i18n._translate('gluon-config-mode:reboot')
if not msg then return end
renderer.render_string(msg, {

View File

@ -1,2 +1,4 @@
package 'gluon-config-mode-core'
entry({}, alias("wizard"))
entry({"wizard"}, model("gluon-config-mode/wizard"), _("Wizard"), 5)

View File

@ -26,6 +26,7 @@ f.reset = false
local s = f:section(Section)
s.template = "gluon/config-mode/welcome"
s.package = "gluon-config-mode-core"
local commit = {'gluon-setup-mode'}
local run = {}
@ -57,6 +58,7 @@ function f:write()
end
f.template = "gluon/config-mode/reboot"
f.package = "gluon-config-mode-core"
f.hidenav = true
if nixio.fork() == 0 then

View File

@ -1,4 +1,6 @@
return function(form, uci)
local site_i18n = i18n 'gluon-site'
local fs = require 'nixio.fs'
local json = require 'jsonc'
local site = require 'gluon.site'
@ -24,8 +26,8 @@ return function(form, uci)
return list
end
local s = form:section(Section, nil, translate('gluon-config-mode:domain-select'))
local o = s:option(ListValue, 'domain', translate('gluon-config-mode:domain'))
local s = form:section(Section, nil, site_i18n.translate('gluon-config-mode:domain-select'))
local o = s:option(ListValue, 'domain', site_i18n.translate('gluon-config-mode:domain'))
if configured then
o.default = selected_domain

View File

@ -1,4 +1,7 @@
return function(form, uci)
local pkg_i18n = i18n 'gluon-config-mode-geo-location'
local site_i18n = i18n 'gluon-site'
local site = require 'gluon.site'
local location = uci:get_first("gluon-node-info", "location")
@ -11,25 +14,25 @@ return function(form, uci)
return uci:get_bool("gluon-node-info", location, "altitude")
end
local text = translate(
local text = pkg_i18n.translate(
'If you want the location of your node to ' ..
'be displayed on the map, you can enter its coordinates here.'
)
if show_altitude() then
text = text .. ' ' .. translate("gluon-config-mode:altitude-help")
text = text .. ' ' .. site_i18n.translate("gluon-config-mode:altitude-help")
end
local s = form:section(Section, nil, text)
local o
local share_location = s:option(Flag, "location", translate("Show node on the map"))
local share_location = s:option(Flag, "location", pkg_i18n.translate("Show node on the map"))
share_location.default = uci:get_bool("gluon-node-info", location, "share_location")
function share_location:write(data)
uci:set("gluon-node-info", location, "share_location", data)
end
o = s:option(Value, "latitude", translate("Latitude"), translatef("e.g. %s", "53.873621"))
o = s:option(Value, "latitude", pkg_i18n.translate("Latitude"), pkg_i18n.translatef("e.g. %s", "53.873621"))
o.default = uci:get("gluon-node-info", location, "latitude")
o:depends(share_location, true)
o.datatype = "float"
@ -37,7 +40,7 @@ return function(form, uci)
uci:set("gluon-node-info", location, "latitude", data)
end
o = s:option(Value, "longitude", translate("Longitude"), translatef("e.g. %s", "10.689901"))
o = s:option(Value, "longitude", pkg_i18n.translate("Longitude"), pkg_i18n.translatef("e.g. %s", "10.689901"))
o.default = uci:get("gluon-node-info", location, "longitude")
o:depends(share_location, true)
o.datatype = "float"
@ -46,7 +49,7 @@ return function(form, uci)
end
if show_altitude() then
o = s:option(Value, "altitude", translate("gluon-config-mode:altitude-label"), translatef("e.g. %s", "11.51"))
o = s:option(Value, "altitude", site_i18n.translate("gluon-config-mode:altitude-label"), pkg_i18n.translatef("e.g. %s", "11.51"))
o.default = uci:get("gluon-node-info", location, "altitude")
o:depends(share_location, true)
o.datatype = "float"

View File

@ -1,8 +1,10 @@
return function(form, uci)
local pkg_i18n = i18n 'gluon-config-mode-hostname'
local pretty_hostname = require "pretty_hostname"
local s = form:section(Section)
local o = s:option(Value, "hostname", translate("Node name"))
local o = s:option(Value, "hostname", pkg_i18n.translate("Node name"))
o.default = pretty_hostname.get(uci)
function o:write(data)

View File

@ -1,3 +1,5 @@
local site_i18n = i18n 'gluon-site'
local uci = require("simple-uci").cursor()
local lutil = require "gluon.web.util"
local fs = require "nixio.fs"
@ -23,15 +25,15 @@ local msg
if has_tunneldigger then
local tunneldigger_enabled = uci:get_bool("tunneldigger", "mesh_vpn", "enabled")
if not tunneldigger_enabled then
msg = _translate('gluon-config-mode:novpn')
msg = site_i18n._translate('gluon-config-mode:novpn')
end
elseif has_fastd then
local fastd_enabled = uci:get_bool("fastd", "mesh_vpn", "enabled")
if fastd_enabled then
pubkey = util.trim(lutil.exec("/etc/init.d/fastd show_key mesh_vpn"))
msg = _translate('gluon-config-mode:pubkey')
msg = site_i18n._translate('gluon-config-mode:pubkey')
else
msg = _translate('gluon-config-mode:novpn')
msg = site_i18n._translate('gluon-config-mode:novpn')
end
end

View File

@ -8,7 +8,9 @@ return function(form, uci)
return
end
local msg = translate(
local pkg_i18n = i18n 'gluon-config-mode-mesh-vpn'
local msg = pkg_i18n.translate(
'Your internet connection can be used to establish a ' ..
'VPN connection with other nodes. ' ..
'Enable this option if there are no other nodes reachable ' ..
@ -21,7 +23,7 @@ return function(form, uci)
local o
local meshvpn = s:option(Flag, "meshvpn", translate("Use internet connection (mesh VPN)"))
local meshvpn = s:option(Flag, "meshvpn", pkg_i18n.translate("Use internet connection (mesh VPN)"))
meshvpn.default = uci:get_bool("fastd", "mesh_vpn", "enabled") or uci:get_bool("tunneldigger", "mesh_vpn", "enabled")
function meshvpn:write(data)
if has_fastd then
@ -32,7 +34,7 @@ return function(form, uci)
end
end
local limit = s:option(Flag, "limit_enabled", translate("Limit bandwidth"))
local limit = s:option(Flag, "limit_enabled", pkg_i18n.translate("Limit bandwidth"))
limit:depends(meshvpn, true)
limit.default = uci:get_bool("simple-tc", "mesh_vpn", "enabled")
function limit:write(data)
@ -41,7 +43,7 @@ return function(form, uci)
uci:set("simple-tc", "mesh_vpn", "ifname", "mesh-vpn")
end
o = s:option(Value, "limit_ingress", translate("Downstream (kbit/s)"))
o = s:option(Value, "limit_ingress", pkg_i18n.translate("Downstream (kbit/s)"))
o:depends(limit, true)
o.default = uci:get("simple-tc", "mesh_vpn", "limit_ingress")
o.datatype = "uinteger"
@ -49,7 +51,7 @@ return function(form, uci)
uci:set("simple-tc", "mesh_vpn", "limit_ingress", data)
end
o = s:option(Value, "limit_egress", translate("Upstream (kbit/s)"))
o = s:option(Value, "limit_egress", pkg_i18n.translate("Upstream (kbit/s)"))
o:depends(limit, true)
o.default = uci:get("simple-tc", "mesh_vpn", "limit_egress")
o.datatype = "uinteger"

View File

@ -1,3 +1,6 @@
package 'gluon-web-admin'
local root = node()
if not root.target then
root.target = alias("admin")

View File

@ -9,6 +9,9 @@ You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
]]--
package 'gluon-web-admin'
local fs = require 'nixio.fs'
local tmpfile = "/tmp/firmware.img"
@ -106,17 +109,23 @@ local function action_upgrade(http, renderer)
renderer.render("layout", {
content = "admin/upgrade",
env = {
bad_image = has_image and not has_support,
},
pkg = 'gluon-web-admin',
})
-- Step 2: present uploaded file, show checksum, confirmation
elseif step == 2 then
renderer.render("layout", {
content = "admin/upgrade_confirm",
env = {
checksum = image_checksum(tmpfile),
filesize = fs.stat(tmpfile).size,
flashsize = storage_size(),
keepconfig = (http:formvalue("keepcfg") == "1"),
},
pkg = 'gluon-web-admin',
})
elseif step == 3 then
if http:formvalue("keepcfg") == "1" then
@ -127,6 +136,7 @@ local function action_upgrade(http, renderer)
renderer.render("layout", {
content = "admin/upgrade_reboot",
hidenav = true,
pkg = 'gluon-web-admin',
})
end
end

View File

@ -1 +1,3 @@
package 'gluon-web-autoupdater'
entry({"admin", "autoupdater"}, model("admin/autoupdater"), _("Automatic updates"), 80)

View File

@ -1 +1,3 @@
package 'gluon-web-logging'
entry({"admin", "logging"}, model("admin/logging"), _("Logging"), 85)

View File

@ -26,14 +26,14 @@ define Build/Configure
endef
define Build/Compile
$(call GluonBuildI18N,gluon-mesh-vpn-fastd,i18n)
$(call GluonBuildI18N,gluon-web-mesh-vpn-fastd,i18n)
$(call GluonSrcDiet,./luasrc,$(PKG_BUILD_DIR)/luadest/)
endef
define Package/gluon-web-mesh-vpn-fastd/install
$(CP) ./files/* $(1)/
$(CP) $(PKG_BUILD_DIR)/luadest/* $(1)/
$(call GluonInstallI18N,gluon-mesh-vpn-fastd,$(1))
$(call GluonInstallI18N,gluon-web-mesh-vpn-fastd,$(1))
endef
define Package/gluon-web-mesh-vpn-fastd/postinst

View File

@ -1 +1,3 @@
package 'gluon-web-mesh-vpn-fastd'
entry({"admin", "mesh_vpn_fastd"}, model("admin/mesh_vpn_fastd"), _("Mesh VPN"), 50)

View File

@ -6,6 +6,7 @@ local f = Form(translate('Mesh VPN'))
local s = f:section(Section)
local mode = s:option(Value, 'mode')
mode.package = "gluon-web-mesh-vpn-fastd"
mode.template = "gluon/model/mesh-vpn-fastd"
local methods = uci:get('fastd', 'mesh_vpn', 'method')

View File

@ -1 +1,3 @@
package 'gluon-web-network'
entry({"admin", "network"}, model("admin/network"), _("Network"), 40)

View File

@ -1 +1,3 @@
package 'gluon-web-node-role'
entry({"admin", "noderole"}, model("admin/noderole"), "Node role", 60)

View File

@ -1 +1,3 @@
package 'gluon-web-private-wifi'
entry({"admin", "privatewifi"}, model("admin/privatewifi"), _("Private WLAN"), 30)

View File

@ -33,6 +33,10 @@ You may obtain a copy of the License at
return r
end
local function title(node)
return i18n(node.pkg).translate(node.title)
end
local function subtree(prefix, node, name, ...)
if not node then return end
@ -48,7 +52,7 @@ You may obtain a copy of the License at
local active = (v == name)
%>
<li class="tabmenu-item-<%=v%><% if active then %> active<% end %>">
<a href="<%=url(append(prefix, v))%>"><%=pcdata(translate(child.title))%></a>
<a href="<%=url(append(prefix, v))%>"><%=pcdata(title(child))%></a>
</li>
<%
end
@ -71,7 +75,7 @@ You may obtain a copy of the License at
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" media="screen" href="<%=media%>/cascade.css" />
<title><%=pcdata( hostname .. ( (rnode and rnode.title) and ' - ' .. translate(rnode.title) or '')) %></title>
<title><%=pcdata( hostname .. ((rnode and rnode.title) and ' - ' .. title(rnode) or '')) %></title>
</head>
<body>
@ -88,7 +92,7 @@ You may obtain a copy of the License at
<% if #categories > 1 and not hidenav then %>
<ul id="topmenu">
<% for i, r in ipairs(categories) do %>
<li><a class="topcat<% if request[1] == r then %> active<%end%>" href="<%=url({r})%>"><%=pcdata(translate(root.nodes[r].title))%></a></li>
<li><a class="topcat<% if request[1] == r then %> active<%end%>" href="<%=url({r})%>"><%=pcdata(title(root.nodes[r]))%></a></li>
<% end %>
</ul>
<% end %>
@ -110,9 +114,9 @@ You may obtain a copy of the License at
</noscript>
<%
ok, err = pcall(include, content)
ok, err = pcall(renderer.render, content, env, pkg)
if not ok then
renderer.render('error500', {message = err})
renderer.render('error500', {message = err}, 'gluon-web')
end
%>

View File

@ -1 +1,3 @@
package 'gluon-web-wifi-config'
entry({"admin", "wifi-config"}, model("admin/wifi-config"), _("WLAN"), 20)

View File

@ -1,6 +1,6 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
-- Copyright 2017-2018 Matthias Schiffer <mschiffer@universe-factory.net>
-- Licensed to the public under the Apache License 2.0.
local fs = require "nixio.fs"
@ -77,7 +77,7 @@ local function set_language(renderer, accept)
end
for match in accept:gmatch("[^,]+") do
local lang = match:match('^%s*([^%s;-_]+)')
local lang = match:match('^%s*([^%s;_-]+)')
local q = tonumber(match:match(';q=(%S+)%s*$') or 1)
if lang == '*' then
@ -93,11 +93,7 @@ local function set_language(renderer, accept)
return (weights[a] or 0) > (weights[b] or 0)
end)
for _, lang in ipairs(langs) do
if renderer.setlanguage(lang) then
return
end
end
renderer.set_language(langs)
end
@ -147,7 +143,20 @@ function dispatch(http, request)
url = function(path) return build_url(http, path) end,
}, { __index = _G }))
local function createtree()
local base = util.libpath() .. "/controller/"
local function load_ctl(path)
local ctl = assert(loadfile(path))
local _pkg
local subdisp = setmetatable({
package = function(name)
_pkg = name
end,
node = function(...)
return _node({...})
end,
@ -158,6 +167,7 @@ function dispatch(http, request)
c.target = target
c.title = title
c.order = order
c.pkg = _pkg
return c
end,
@ -177,17 +187,19 @@ function dispatch(http, request)
end,
template = function(view)
local pkg = _pkg
return function()
renderer.render("layout", {content = view})
renderer.render("layout", {content = view, pkg = pkg})
end
end,
model = function(name)
local pkg = _pkg
return function()
local hidenav = false
local model = require "gluon.web.model"
local maps = model.load(name, renderer)
local maps = model.load(name, renderer, pkg)
for _, map in ipairs(maps) do
map:parse(http)
@ -199,7 +211,9 @@ function dispatch(http, request)
renderer.render("layout", {
content = "model/wrapper",
env = {
maps = maps,
},
hidenav = hidenav,
})
end
@ -210,12 +224,6 @@ function dispatch(http, request)
end,
}, { __index = _G })
local function createtree()
local base = util.libpath() .. "/controller/"
local function load_ctl(path)
local ctl = assert(loadfile(path))
local env = setmetatable({}, { __index = subdisp })
setfenv(ctl, env)
@ -239,9 +247,14 @@ function dispatch(http, request)
if not node or not node.target then
http:status(404, "Not Found")
renderer.render("layout", { content = "error404", message =
renderer.render("layout", {
content = "error404",
env = {
message =
"No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
"If this URL belongs to an extension, make sure it is properly installed.\n"
"If this URL belongs to an extension, make sure it is properly installed.\n",
},
pkg = 'gluon-web',
})
return
end
@ -251,9 +264,14 @@ function dispatch(http, request)
local ok, err = pcall(node.target)
if not ok then
http:status(500, "Internal Server Error")
renderer.render("layout", { content = "error500", message =
renderer.render("layout", {
content = "error500",
env = {
message =
"Failed to execute dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
"The called action terminated with an exception:\n" .. tostring(err or "(unknown)")
"The called action terminated with an exception:\n" .. tostring(err or "(unknown)"),
},
pkg = 'gluon-web',
})
end
end

View File

@ -0,0 +1,54 @@
-- Copyright 2018 Matthias Schiffer <mschiffer@universe-factory.net>
-- Licensed to the public under the Apache License 2.0.
local tparser = require "gluon.web.template.parser"
local util = require "gluon.web.util"
local fs = require "nixio.fs"
local i18ndir = util.libpath() .. "/i18n"
local function i18n_file(lang, pkg)
return string.format('%s/%s.%s.lmo', i18ndir, pkg, lang)
end
local function no_translation(key)
return nil
end
local function load_catalog(lang, pkg)
if pkg then
local file = i18n_file(lang, pkg)
local cat = fs.access(file) and tparser.load_catalog(file)
if cat then return cat end
end
return no_translation
end
module "gluon.web.i18n"
function supported(lang)
return lang == 'en' or fs.access(i18n_file(lang, 'gluon-web'))
end
function load(lang, pkg)
local _translate = load_catalog(lang, pkg)
local function translate(key)
return _translate(key) or key
end
local function translatef(key, ...)
return translate(key):format(...)
end
return {
_translate = _translate,
translate = translate,
translatef = translatef,
}
end

View File

@ -4,11 +4,11 @@
module("gluon.web.model", package.seeall)
local util = require("gluon.web.util")
local util = require "gluon.web.util"
local fs = require("nixio.fs")
local datatypes = require("gluon.web.model.datatypes")
local dispatcher = require("gluon.web.dispatcher")
local fs = require "nixio.fs"
local datatypes = require "gluon.web.model.datatypes"
local dispatcher = require "gluon.web.dispatcher"
local class = util.class
local instanceof = util.instanceof
@ -17,7 +17,7 @@ FORM_VALID = 1
FORM_INVALID = -1
-- Loads a model from given file, creating an environment and returns it
function load(name, renderer)
function load(name, renderer, pkg)
local modeldir = util.libpath() .. "/model/"
if not fs.access(modeldir..name..".lua") then
@ -26,14 +26,16 @@ function load(name, renderer)
local func = assert(loadfile(modeldir..name..".lua"))
local env = {
translate=renderer.translate,
translatef=renderer.translatef,
}
local i18n = setmetatable({
i18n = renderer.i18n
}, {
__index = renderer.i18n(pkg)
})
setfenv(func, setmetatable(env, {__index =
setfenv(func, setmetatable({}, {__index =
function(tbl, key)
return _M[key] or _G[key]
return _M[key] or i18n[key] or _G[key]
end
}))
@ -85,6 +87,7 @@ function Node:__init__(title, description, name)
self.name = name
self.index = nil
self.parent = nil
self.package = 'gluon-web'
end
function Node:append(obj)
@ -116,7 +119,7 @@ function Node:render(renderer, scope)
id = self:id(),
scope = scope,
}, {__index = scope})
renderer.render(self.template, env)
renderer.render(self.template, env, self.package)
end
end

View File

@ -1,39 +1,59 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
-- Copyright 2017-2018 Matthias Schiffer <mschiffer@universe-factory.net>
-- Licensed to the public under the Apache License 2.0.
local tparser = require "gluon.web.template.parser"
local i18n = require "gluon.web.i18n"
local util = require "gluon.web.util"
local fs = require "nixio.fs"
local tostring, setmetatable, setfenv, pcall, assert = tostring, setmetatable, setfenv, pcall, assert
local tostring, ipairs, setmetatable, setfenv = tostring, ipairs, setmetatable, setfenv
local pcall, assert = pcall, assert
module "gluon.web.template"
local viewdir = util.libpath() .. "/view/"
local i18ndir = util.libpath() .. "/i18n/"
function renderer(env)
local ctx = {}
local language = 'en'
local catalogs = {}
local function render_template(name, template, scope)
function ctx.set_language(langs)
for _, lang in ipairs(langs) do
if i18n.supported(lang) then
language = lang
catalogs = {}
return
end
end
end
function ctx.i18n(pkg)
local cat = catalogs[pkg] or i18n.load(language, pkg)
if pkg then catalogs[pkg] = cat end
return cat
end
local function render_template(name, template, scope, pkg)
scope = scope or {}
local t = ctx.i18n(pkg)
local locals = {
renderer = ctx,
translate = ctx.translate,
translatef = ctx.translatef,
_translate = ctx._translate,
i18n = ctx.i18n,
translate = t.translate,
translatef = t.translatef,
_translate = t._translate,
include = function(name)
ctx.render(name, scope)
ctx.render(name, scope, pkg)
end,
}
setfenv(template, setmetatable({}, {
__index = function(tbl, key)
return scope[key] or env[key] or locals[key]
return scope[key] or locals[key] or env[key]
end
}))
@ -46,7 +66,7 @@ function renderer(env)
--- Render a certain template.
-- @param name Template name
-- @param scope Scope to assign to template (optional)
function ctx.render(name, scope)
function ctx.render(name, scope, pkg)
local sourcefile = viewdir .. name .. ".html"
local template, _, err = tparser.parse(sourcefile)
@ -54,45 +74,19 @@ function renderer(env)
"Error while parsing template '" .. sourcefile .. "':\n" ..
(err or "Unknown syntax error"))
render_template(name, template, scope)
render_template(name, template, scope, pkg)
end
--- Render a template from a string.
-- @param template Template string
-- @param scope Scope to assign to template (optional)
function ctx.render_string(str, scope)
function ctx.render_string(str, scope, pkg)
local template, _, err = tparser.parse_string(str)
assert(template, "Error while parsing template:\n" ..
(err or "Unknown syntax error"))
render_template('(local)', template, scope)
end
function ctx.setlanguage(lang)
lang = lang:gsub("_", "-")
if not lang then return false end
if lang ~= 'en' and not fs.access(i18ndir .. "gluon-web." .. lang .. ".lmo") then
return false
end
return tparser.load_catalog(lang, i18ndir)
end
-- Returns a translated string, or nil if none is found
function ctx._translate(key)
return (tparser.translate(key))
end
-- Returns a translated string, or the original string if none is found
function ctx.translate(key)
return tparser.translate(key) or key
end
function ctx.translatef(key, ...)
local t = ctx.translate(key)
return t:format(...)
render_template('(local)', template, scope, pkg)
end
return ctx

View File

@ -23,15 +23,12 @@
#include <sys/mman.h>
#include <arpa/inet.h>
#include <dirent.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
struct lmo_entry {
@ -41,28 +38,6 @@ struct lmo_entry {
uint32_t length;
} __attribute__((packed));
typedef struct lmo_entry lmo_entry_t;
struct lmo_archive {
size_t length;
const lmo_entry_t *index;
char *data;
const char *end;
struct lmo_archive *next;
};
typedef struct lmo_archive lmo_archive_t;
struct lmo_catalog {
char lang[6];
struct lmo_archive *archives;
struct lmo_catalog *next;
};
typedef struct lmo_catalog lmo_catalog_t;
static inline uint16_t get_le16(const void *data) {
const uint8_t *d = data;
@ -122,12 +97,13 @@ static uint32_t sfh_hash(const void *input, size_t len)
return hash;
}
static lmo_archive_t * lmo_open(const char *file)
bool lmo_load(lmo_catalog_t *cat, const char *file)
{
int fd = -1;
lmo_archive_t *ar = NULL;
struct stat s;
cat->data = MAP_FAILED;
fd = open(file, O_RDONLY|O_CLOEXEC);
if (fd < 0)
goto err;
@ -135,111 +111,43 @@ static lmo_archive_t * lmo_open(const char *file)
if (fstat(fd, &s))
goto err;
if ((ar = calloc(1, sizeof(*ar))) != NULL) {
ar->data = mmap(NULL, s.st_size, PROT_READ, MAP_SHARED, fd, 0);
cat->data = mmap(NULL, s.st_size, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
fd = -1;
if (ar->data == MAP_FAILED)
if (cat->data == MAP_FAILED)
goto err;
ar->end = ar->data + s.st_size;
cat->end = cat->data + s.st_size;
uint32_t idx_offset = get_be32(ar->end - sizeof(uint32_t));
ar->index = (const lmo_entry_t *)(ar->data + idx_offset);
uint32_t idx_offset = get_be32(cat->end - sizeof(uint32_t));
cat->index = (const lmo_entry_t *)(cat->data + idx_offset);
if ((const char *)ar->index > (ar->end - sizeof(uint32_t)))
if ((const char *)cat->index > (cat->end - sizeof(uint32_t)))
goto err;
ar->length = (ar->end - sizeof(uint32_t) - (const char *)ar->index) / sizeof(lmo_entry_t);
cat->length = (cat->end - sizeof(uint32_t) - (const char *)cat->index) / sizeof(lmo_entry_t);
return ar;
}
return true;
err:
if (fd >= 0)
close(fd);
if (ar != NULL) {
if ((ar->data != NULL) && (ar->data != MAP_FAILED))
munmap(ar->data, ar->end - ar->data);
free(ar);
}
return NULL;
}
static lmo_catalog_t *lmo_catalogs;
static lmo_catalog_t *lmo_active_catalog;
bool lmo_change_catalog(const char *lang)
{
lmo_catalog_t *cat;
for (cat = lmo_catalogs; cat; cat = cat->next) {
if (!strncmp(cat->lang, lang, sizeof(cat->lang))) {
lmo_active_catalog = cat;
return true;
}
}
if (cat->data != MAP_FAILED)
munmap(cat->data, cat->end - cat->data);
return false;
}
bool lmo_load_catalog(const char *lang, const char *dir)
void lmo_unload(lmo_catalog_t *cat)
{
DIR *dh = NULL;
char pattern[16];
char path[PATH_MAX];
struct dirent *de = NULL;
lmo_archive_t *ar = NULL;
lmo_catalog_t *cat = NULL;
if (lmo_change_catalog(lang))
return true;
if (!(dh = opendir(dir)))
goto err;
if (!(cat = calloc(1, sizeof(*cat))))
goto err;
snprintf(cat->lang, sizeof(cat->lang), "%s", lang);
snprintf(pattern, sizeof(pattern), "*.%s.lmo", lang);
while ((de = readdir(dh)) != NULL) {
if (!fnmatch(pattern, de->d_name, 0)) {
snprintf(path, sizeof(path), "%s/%s", dir, de->d_name);
ar = lmo_open(path);
if (ar) {
ar->next = cat->archives;
cat->archives = ar;
}
}
}
closedir(dh);
cat->next = lmo_catalogs;
lmo_catalogs = cat;
lmo_active_catalog = cat;
return true;
err:
if (dh)
closedir(dh);
free(cat);
return false;
if (cat->data != MAP_FAILED)
munmap(cat->data, cat->end - cat->data);
}
static int lmo_compare_entry(const void *a, const void *b)
{
const lmo_entry_t *ea = a, *eb = b;
@ -253,34 +161,26 @@ static int lmo_compare_entry(const void *a, const void *b)
return 0;
}
static const lmo_entry_t * lmo_find_entry(const lmo_archive_t *ar, uint32_t hash)
static const lmo_entry_t * lmo_find_entry(const lmo_catalog_t *cat, uint32_t hash)
{
lmo_entry_t key;
key.key_id = htonl(hash);
return bsearch(&key, ar->index, ar->length, sizeof(lmo_entry_t), lmo_compare_entry);
return bsearch(&key, cat->index, cat->length, sizeof(lmo_entry_t), lmo_compare_entry);
}
bool lmo_translate(const char *key, size_t keylen, char **out, size_t *outlen)
bool lmo_translate(const lmo_catalog_t *cat, const char *key, size_t keylen, const char **out, size_t *outlen)
{
if (!lmo_active_catalog)
uint32_t hash = sfh_hash(key, keylen);
const lmo_entry_t *e = lmo_find_entry(cat, hash);
if (!e)
return false;
uint32_t hash = sfh_hash(key, keylen);
for (const lmo_archive_t *ar = lmo_active_catalog->archives; ar; ar = ar->next) {
const lmo_entry_t *e = lmo_find_entry(ar, hash);
if (!e)
continue;
*out = ar->data + ntohl(e->offset);
*out = cat->data + ntohl(e->offset);
*outlen = ntohl(e->length);
if (*out + *outlen > ar->end)
continue;
if (*out + *outlen > cat->end)
return false;
return true;
}
return false;
}

View File

@ -24,7 +24,21 @@
#include <stddef.h>
bool lmo_load_catalog(const char *lang, const char *dir);
bool lmo_translate(const char *key, size_t keylen, char **out, size_t *outlen);
typedef struct lmo_entry lmo_entry_t;
struct lmo_catalog {
size_t length;
const lmo_entry_t *index;
char *data;
const char *end;
};
typedef struct lmo_catalog lmo_catalog_t;
bool lmo_load(lmo_catalog_t *cat, const char *file);
void lmo_unload(lmo_catalog_t *cat);
bool lmo_translate(const lmo_catalog_t *cat, const char *key, size_t keylen, const char **out, size_t *outlen);
#endif

View File

@ -29,7 +29,7 @@
#include <string.h>
#define TEMPLATE_LUALIB_META "gluon.web.template.parser"
#define TEMPLATE_CATALOG "gluon.web.template.parser.catalog"
static int template_L_do_parse(lua_State *L, struct template_parser *parser, const char *chunkname)
@ -87,40 +87,64 @@ static int template_L_pcdata(lua_State *L)
return 1;
}
static int template_L_load_catalog(lua_State *L) {
const char *lang = luaL_optstring(L, 1, "en");
const char *dir = luaL_checkstring(L, 2);
lua_pushboolean(L, lmo_load_catalog(lang, dir));
return 1;
}
static int template_L_load_catalog(lua_State *L)
{
const char *file = luaL_checkstring(L, 1);
static int template_L_translate(lua_State *L) {
size_t len;
char *tr;
size_t trlen;
const char *key = luaL_checklstring(L, 1, &len);
lmo_catalog_t *cat = lua_newuserdata(L, sizeof(*cat));
if (!lmo_load(cat, file)) {
lua_pop(L, 1);
return 0;
}
if (lmo_translate(key, len, &tr, &trlen))
lua_pushlstring(L, tr, trlen);
else
lua_pushnil(L);
luaL_getmetatable(L, TEMPLATE_CATALOG);
lua_setmetatable(L, -2);
return 1;
}
static int template_catalog_call(lua_State *L)
{
size_t inlen, outlen;
lmo_catalog_t *cat = luaL_checkudata(L, 1, TEMPLATE_CATALOG);
const char *in = luaL_checklstring(L, 2, &inlen), *out;
if (!lmo_translate(cat, in, inlen, &out, &outlen))
return 0;
lua_pushlstring(L, out, outlen);
return 1;
}
static int template_catalog_gc(lua_State *L)
{
lmo_catalog_t *cat = luaL_checkudata(L, 1, TEMPLATE_CATALOG);
lmo_unload(cat);
return 0;
}
/* module table */
static const luaL_reg R[] = {
{ "parse", template_L_parse },
{ "parse_string", template_L_parse_string },
{ "pcdata", template_L_pcdata },
{ "load_catalog", template_L_load_catalog },
{ "translate", template_L_translate },
{}
};
static const luaL_reg template_catalog_methods[] = {
{ "__call", template_catalog_call },
{ "__gc", template_catalog_gc },
{}
};
__attribute__ ((visibility("default")))
LUALIB_API int luaopen_gluon_web_template_parser(lua_State *L) {
luaL_register(L, TEMPLATE_LUALIB_META, R);
luaL_register(L, "gluon.web.template.parser", R);
luaL_newmetatable(L, TEMPLATE_CATALOG);
luaL_register(L, NULL, template_catalog_methods);
lua_pop(L, 1);
return 1;
}