Browse Source

gluon-web: add i18n package namespaces

Matthias Schiffer 6 years ago
parent
commit
557565e189
30 changed files with 375 additions and 313 deletions
  1. 4 2
      package/gluon-config-mode-autoupdater/luasrc/lib/gluon/config-mode/wizard/0050-autoupdater-info.lua
  2. 4 2
      package/gluon-config-mode-contact-info/luasrc/lib/gluon/config-mode/wizard/0500-contact-info.lua
  3. 3 1
      package/gluon-config-mode-core/files/lib/gluon/web/view/gluon/config-mode/welcome.html
  4. 3 1
      package/gluon-config-mode-core/luasrc/lib/gluon/config-mode/reboot/0900-msg-reboot.lua
  5. 2 0
      package/gluon-config-mode-core/luasrc/lib/gluon/web/controller/gluon-config-mode/index.lua
  6. 2 0
      package/gluon-config-mode-core/luasrc/lib/gluon/web/model/gluon-config-mode/wizard.lua
  7. 4 2
      package/gluon-config-mode-domain-select/luasrc/lib/gluon/config-mode/wizard/0200-domain-select.lua
  8. 9 6
      package/gluon-config-mode-geo-location/luasrc/lib/gluon/config-mode/wizard/0400-geo-location.lua
  9. 3 1
      package/gluon-config-mode-hostname/luasrc/lib/gluon/config-mode/wizard/0100-hostname.lua
  10. 5 3
      package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua
  11. 7 5
      package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/wizard/0300-mesh-vpn.lua
  12. 3 0
      package/gluon-web-admin/luasrc/lib/gluon/web/controller/admin/index.lua
  13. 15 5
      package/gluon-web-admin/luasrc/lib/gluon/web/controller/admin/upgrade.lua
  14. 2 0
      package/gluon-web-autoupdater/luasrc/lib/gluon/web/controller/admin/autoupdater.lua
  15. 2 0
      package/gluon-web-logging/luasrc/lib/gluon/web/controller/admin/logging.lua
  16. 2 2
      package/gluon-web-mesh-vpn-fastd/Makefile
  17. 2 0
      package/gluon-web-mesh-vpn-fastd/luasrc/lib/gluon/web/controller/admin/mesh_vpn_fastd.lua
  18. 1 0
      package/gluon-web-mesh-vpn-fastd/luasrc/lib/gluon/web/model/admin/mesh_vpn_fastd.lua
  19. 2 0
      package/gluon-web-network/luasrc/lib/gluon/web/controller/admin/network.lua
  20. 2 0
      package/gluon-web-node-role/luasrc/lib/gluon/web/controller/admin/noderole.lua
  21. 2 0
      package/gluon-web-private-wifi/luasrc/lib/gluon/web/controller/admin/privatewifi.lua
  22. 9 5
      package/gluon-web-theme/files/lib/gluon/web/view/themes/gluon/layout.html
  23. 2 0
      package/gluon-web-wifi-config/luasrc/lib/gluon/web/controller/admin/wifi-config.lua
  24. 93 75
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
  25. 54 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/i18n.lua
  26. 15 12
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua
  27. 34 40
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua
  28. 32 132
      package/gluon-web/src/template_lmo.c
  29. 16 2
      package/gluon-web/src/template_lmo.h
  30. 41 17
      package/gluon-web/src/template_lualib.c

+ 4 - 2
package/gluon-config-mode-autoupdater/luasrc/lib/gluon/config-mode/wizard/0050-autoupdater-info.lua

@@ -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

+ 4 - 2
package/gluon-config-mode-contact-info/luasrc/lib/gluon/config-mode/wizard/0500-contact-info.lua

@@ -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"

+ 3 - 1
package/gluon-config-mode-core/files/lib/gluon/web/view/gluon/config-mode/welcome.html

@@ -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>

+ 3 - 1
package/gluon-config-mode-core/luasrc/lib/gluon/config-mode/reboot/0900-msg-reboot.lua

@@ -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, {

+ 2 - 0
package/gluon-config-mode-core/luasrc/lib/gluon/web/controller/gluon-config-mode/index.lua

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

+ 2 - 0
package/gluon-config-mode-core/luasrc/lib/gluon/web/model/gluon-config-mode/wizard.lua

@@ -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

+ 4 - 2
package/gluon-config-mode-domain-select/luasrc/lib/gluon/config-mode/wizard/0200-domain-select.lua

@@ -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

+ 9 - 6
package/gluon-config-mode-geo-location/luasrc/lib/gluon/config-mode/wizard/0400-geo-location.lua

@@ -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"

+ 3 - 1
package/gluon-config-mode-hostname/luasrc/lib/gluon/config-mode/wizard/0100-hostname.lua

@@ -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)

+ 5 - 3
package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/reboot/0100-mesh-vpn.lua

@@ -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
 

+ 7 - 5
package/gluon-config-mode-mesh-vpn/luasrc/lib/gluon/config-mode/wizard/0300-mesh-vpn.lua

@@ -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"

+ 3 - 0
package/gluon-web-admin/luasrc/lib/gluon/web/controller/admin/index.lua

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

+ 15 - 5
package/gluon-web-admin/luasrc/lib/gluon/web/controller/admin/upgrade.lua

@@ -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",
-			bad_image = has_image and not has_support,
+			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",
-			checksum   = image_checksum(tmpfile),
-			filesize   = fs.stat(tmpfile).size,
-			flashsize  = storage_size(),
-			keepconfig = (http:formvalue("keepcfg") == "1"),
+			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

+ 2 - 0
package/gluon-web-autoupdater/luasrc/lib/gluon/web/controller/admin/autoupdater.lua

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

+ 2 - 0
package/gluon-web-logging/luasrc/lib/gluon/web/controller/admin/logging.lua

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

+ 2 - 2
package/gluon-web-mesh-vpn-fastd/Makefile

@@ -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

+ 2 - 0
package/gluon-web-mesh-vpn-fastd/luasrc/lib/gluon/web/controller/admin/mesh_vpn_fastd.lua

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

+ 1 - 0
package/gluon-web-mesh-vpn-fastd/luasrc/lib/gluon/web/model/admin/mesh_vpn_fastd.lua

@@ -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')

+ 2 - 0
package/gluon-web-network/luasrc/lib/gluon/web/controller/admin/network.lua

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

+ 2 - 0
package/gluon-web-node-role/luasrc/lib/gluon/web/controller/admin/noderole.lua

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

+ 2 - 0
package/gluon-web-private-wifi/luasrc/lib/gluon/web/controller/admin/privatewifi.lua

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

+ 9 - 5
package/gluon-web-theme/files/lib/gluon/web/view/themes/gluon/layout.html

@@ -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
 			%>
 

+ 2 - 0
package/gluon-web-wifi-config/luasrc/lib/gluon/web/controller/admin/wifi-config.lua

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

+ 93 - 75
package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua

@@ -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,68 +143,6 @@ function dispatch(http, request)
 		url         = function(path) return build_url(http, path) end,
 	}, { __index = _G }))
 
-	local subdisp = setmetatable({
-		node = function(...)
-			return _node({...})
-		end,
-
-		entry = function(path, target, title, order)
-			local c = _node(path, true)
-
-			c.target = target
-			c.title  = title
-			c.order  = order
-
-			return c
-		end,
-
-		alias = function(...)
-			local req = {...}
-			return function()
-				http:redirect(build_url(http, req))
-			end
-		end,
-
-		call = function(func, ...)
-			local args = {...}
-			return function()
-				func(http, renderer, unpack(args))
-			end
-		end,
-
-		template = function(view)
-			return function()
-				renderer.render("layout", {content = view})
-			end
-		end,
-
-		model = function(name)
-			return function()
-				local hidenav = false
-
-				local model = require "gluon.web.model"
-				local maps = model.load(name, renderer)
-
-				for _, map in ipairs(maps) do
-					map:parse(http)
-				end
-				for _, map in ipairs(maps) do
-					map:handle()
-					hidenav = hidenav or map.hidenav
-				end
-
-				renderer.render("layout", {
-					content = "model/wrapper",
-					maps = maps,
-					hidenav = hidenav,
-				})
-			end
-		end,
-
-		_ = function(text)
-			return text
-		end,
-	}, { __index = _G })
 
 	local function createtree()
 		local base = util.libpath() .. "/controller/"
@@ -216,6 +150,80 @@ function dispatch(http, request)
 		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,
+
+				entry = function(path, target, title, order)
+					local c = _node(path, true)
+
+					c.target = target
+					c.title  = title
+					c.order  = order
+					c.pkg    = _pkg
+
+					return c
+				end,
+
+				alias = function(...)
+					local req = {...}
+					return function()
+						http:redirect(build_url(http, req))
+					end
+				end,
+
+				call = function(func, ...)
+					local args = {...}
+					return function()
+						func(http, renderer, unpack(args))
+					end
+				end,
+
+				template = function(view)
+					local pkg = _pkg
+					return function()
+						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, pkg)
+
+						for _, map in ipairs(maps) do
+							map:parse(http)
+						end
+						for _, map in ipairs(maps) do
+							map:handle()
+							hidenav = hidenav or map.hidenav
+						end
+
+						renderer.render("layout", {
+							content = "model/wrapper",
+							env = {
+								maps = maps,
+							},
+							hidenav = hidenav,
+						})
+					end
+				end,
+
+				_ = function(text)
+					return text
+				end,
+			}, { __index = _G })
+
 			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 =
-			"No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
-		        "If this URL belongs to an extension, make sure it is properly installed.\n"
+		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",
+			},
+			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 =
-			"Failed to execute dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
-			"The called action terminated with an exception:\n" .. tostring(err or "(unknown)")
+		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)"),
+			},
+			pkg = 'gluon-web',
 		})
 	end
 end

+ 54 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/i18n.lua

@@ -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

+ 15 - 12
package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua

@@ -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
 

+ 34 - 40
package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua

@@ -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

+ 32 - 132
package/gluon-web/src/template_lmo.c

@@ -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;
+	close(fd);
+	fd = -1;
 
-		if (ar->data == MAP_FAILED)
-			goto err;
+	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)))
-			goto err;
+	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)
-		return false;
-
 	uint32_t hash = sfh_hash(key, keylen);
+	const lmo_entry_t *e = lmo_find_entry(cat, hash);
+	if (!e)
+		return false;
 
-	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);
-		*outlen = ntohl(e->length);
-
-		if (*out + *outlen > ar->end)
-			continue;
+	*out = cat->data + ntohl(e->offset);
+	*outlen = ntohl(e->length);
 
-		return true;
-	}
+	if (*out + *outlen > cat->end)
+		return false;
 
-	return false;
+	return true;
 }

+ 16 - 2
package/gluon-web/src/template_lmo.h

@@ -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

+ 41 - 17
package/gluon-web/src/template_lualib.c

@@ -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));
+static int template_L_load_catalog(lua_State *L)
+{
+	const char *file = luaL_checkstring(L, 1);
+
+	lmo_catalog_t *cat = lua_newuserdata(L, sizeof(*cat));
+	if (!lmo_load(cat, file)) {
+		lua_pop(L, 1);
+		return 0;
+	}
+
+        luaL_getmetatable(L, TEMPLATE_CATALOG);
+        lua_setmetatable(L, -2);
+
 	return 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);
+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;
 
-	if (lmo_translate(key, len, &tr, &trlen))
-		lua_pushlstring(L, tr, trlen);
-	else
-		lua_pushnil(L);
+	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;
 }