diff --git a/package/gluon-web-admin/luasrc/lib/gluon/web/controller/admin/upgrade.lua b/package/gluon-web-admin/luasrc/lib/gluon/web/controller/admin/upgrade.lua
index 6d8e15459e7b26dd93877ee71304d845e04c4899..7c13377a1a6605154a9ddc8223e2f05fb4dc0e44 100644
--- a/package/gluon-web-admin/luasrc/lib/gluon/web/controller/admin/upgrade.lua
+++ b/package/gluon-web-admin/luasrc/lib/gluon/web/controller/admin/upgrade.lua
@@ -106,36 +106,27 @@ local function action_upgrade(http, renderer)
 			fs.unlink(tmpfile)
 		end
 
-		renderer.render("layout", {
-			content = "admin/upgrade",
-			env = {
-				bad_image = has_image and not has_support,
-			},
-			pkg = 'gluon-web-admin',
-		})
+		renderer.render_layout('admin/upgrade', {
+			bad_image = has_image and not has_support,
+		}, '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',
-		})
+		renderer.render_layout('admin/upgrade_confirm', {
+			checksum   = image_checksum(tmpfile),
+			filesize   = fs.stat(tmpfile).size,
+			flashsize  = storage_size(),
+			keepconfig = (http:formvalue("keepcfg") == "1"),
+		}, 'gluon-web-admin')
+
 	elseif step == 3 then
 		if http:formvalue("keepcfg") == "1" then
 			fork_exec("/sbin/sysupgrade", tmpfile)
 		else
 			fork_exec("/sbin/sysupgrade", "-n", tmpfile)
 		end
-		renderer.render("layout", {
-			content = "admin/upgrade_reboot",
+		renderer.render_layout('admin/upgrade_reboot', nil, 'gluon-web-admin', {
 			hidenav = true,
-			pkg = 'gluon-web-admin',
 		})
 	end
 end
diff --git a/package/gluon-web-model/files/lib/gluon/web/view/model/wrapper.html b/package/gluon-web-model/files/lib/gluon/web/view/model/wrapper.html
index 2d0cfdd508af3105899eb9fc7d6d37a706c61b4f..a49fe938babe068614e71c54edf64f02130219e0 100644
--- a/package/gluon-web-model/files/lib/gluon/web/view/model/wrapper.html
+++ b/package/gluon-web-model/files/lib/gluon/web/view/model/wrapper.html
@@ -3,4 +3,4 @@
 		map:render(renderer)
 	end
 %>
-<script type="text/javascript" src="<%|resource%>/gluon-web.js"></script>
+<script type="text/javascript" src="/static/gluon-web-model.js"></script>
diff --git a/package/gluon-web-model/files/lib/gluon/web/www/static/resources/gluon-web.js b/package/gluon-web-model/files/lib/gluon/web/www/static/gluon-web-model.js
similarity index 100%
rename from package/gluon-web-model/files/lib/gluon/web/www/static/resources/gluon-web.js
rename to package/gluon-web-model/files/lib/gluon/web/www/static/gluon-web-model.js
diff --git a/package/gluon-web-model/javascript/gluon-web.js b/package/gluon-web-model/javascript/gluon-web-model.js
similarity index 98%
rename from package/gluon-web-model/javascript/gluon-web.js
rename to package/gluon-web-model/javascript/gluon-web-model.js
index 57bbf379bdf6bd51b98f830313d280c07aef6c59..c05a171e4975776fb19fdc59074c58bcffe877a5 100644
--- a/package/gluon-web-model/javascript/gluon-web.js
+++ b/package/gluon-web-model/javascript/gluon-web-model.js
@@ -13,7 +13,7 @@
 /*
 	Build using:
 
-	uglifyjs javascript/gluon-web.js -o files/lib/gluon/web/www/static/resources/gluon-web.js -c -m --support-ie8
+	uglifyjs javascript/gluon-web-model.js -o files/lib/gluon/web/www/static/gluon-web-model.js -c -m --support-ie8
 */
 
 
diff --git a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model.lua b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model.lua
index 4914dee6dd8c8bf2ece5a45811d2dc7b6c42cddd..8c968dfe6a86bd5ca4e23678e75ca6c0b1e1cbf0 100644
--- a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model.lua
+++ b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model.lua
@@ -58,11 +58,9 @@ return function(config, http, renderer, name, pkg)
 		hidenav = hidenav or map.hidenav
 	end
 
-	renderer.render('layout', {
-		content = 'model/wrapper',
-		env = {
-			maps = maps,
-		},
+	renderer.render_layout('model/wrapper', {
+		maps = maps,
+	}, nil, {
 		hidenav = hidenav,
 	})
 end
diff --git a/package/gluon-web-theme/Makefile b/package/gluon-web-theme/Makefile
index 87e97bbf0b65c01ba0935c7d621e11d87e1e3b71..2dd47ab71dd8fcf0877d02c81e647076bfd5eaf0 100644
--- a/package/gluon-web-theme/Makefile
+++ b/package/gluon-web-theme/Makefile
@@ -11,26 +11,27 @@ PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
 
 include ../gluon.mk
 
+PKG_CONFIG_DEPENDS += $(GLUON_I18N_CONFIG)
+
 
 define Package/gluon-web-theme
   SECTION:=gluon
   CATEGORY:=Gluon
   TITLE:=gluon-web theme
-  DEPENDS:=+gluon-core +gluon-web-model
+  DEPENDS:=+gluon-core +gluon-web
 endef
 
 define Build/Prepare
 	mkdir -p $(PKG_BUILD_DIR)
 endef
 
-define Build/Configure
-endef
-
 define Build/Compile
+	$(call GluonBuildI18N,gluon-web-theme,i18n)
 endef
 
 define Package/gluon-web-theme/install
 	$(CP) ./files/* $(1)/
+	$(call GluonInstallI18N,gluon-web-theme,$(1))
 endef
 
 $(eval $(call BuildPackage,gluon-web-theme))
diff --git a/package/gluon-web-theme/files/lib/gluon/web/view/themes/gluon/layout.html b/package/gluon-web-theme/files/lib/gluon/web/view/theme/layout.html
similarity index 95%
rename from package/gluon-web-theme/files/lib/gluon/web/view/themes/gluon/layout.html
rename to package/gluon-web-theme/files/lib/gluon/web/view/theme/layout.html
index fb24b3a3f94fb61ae21360579e9574ea6aac82fd..874419a7b56e81e3af1b3e47eb84181ce13fdc80 100644
--- a/package/gluon-web-theme/files/lib/gluon/web/view/themes/gluon/layout.html
+++ b/package/gluon-web-theme/files/lib/gluon/web/view/theme/layout.html
@@ -100,7 +100,7 @@ You may obtain a copy of the License at
 <html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
 	<head>
 		<meta charset="UTF-8" />
-		<link rel="stylesheet" type="text/css" media="screen" href="<%|media%>/cascade.css" />
+		<link rel="stylesheet" type="text/css" media="screen" href="/static/gluon.css" />
 		<title><%| hostname .. ((rnode and rnode.title) and ' - ' .. title(rnode) or '') %></title>
 	</head>
 	<body>
@@ -140,7 +140,7 @@ You may obtain a copy of the License at
 			</noscript>
 
 			<%
-				ok, err = pcall(renderer.render, content, env, pkg)
+				ok, err = pcall(renderer.render, content, scope, pkg)
 				if not ok then
 					renderer.render('error/500', {message = err}, 'gluon-web')
 				end
diff --git a/package/gluon-web-theme/files/lib/gluon/web/www/static/gluon/cascade.css b/package/gluon-web-theme/files/lib/gluon/web/www/static/gluon.css
similarity index 100%
rename from package/gluon-web-theme/files/lib/gluon/web/www/static/gluon/cascade.css
rename to package/gluon-web-theme/files/lib/gluon/web/www/static/gluon.css
diff --git a/package/gluon-web-theme/i18n/de.po b/package/gluon-web-theme/i18n/de.po
new file mode 100644
index 0000000000000000000000000000000000000000..b18a346dfed827c2a8bfb8051b327fbe20c2285a
--- /dev/null
+++ b/package/gluon-web-theme/i18n/de.po
@@ -0,0 +1,20 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: 2013-03-29 12:13+0200\n"
+"Last-Translator: Matthias Schiffer <mschiffer@universe-factory.net>\n"
+"Language-Team: German\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "JavaScript required!"
+msgstr "JavaScript benötigt!"
+
+msgid ""
+"You must enable JavaScript in your browser or the web interface will not "
+"work properly."
+msgstr "Bitte aktiviere JavaScript in deinem Browser, damit das Webinterface "
+"korrekt funktionieren kann."
diff --git a/package/gluon-web-theme/i18n/fr.po b/package/gluon-web-theme/i18n/fr.po
new file mode 100644
index 0000000000000000000000000000000000000000..95e73b1a86084dc3e443a3bc2d01cf596266fdc0
--- /dev/null
+++ b/package/gluon-web-theme/i18n/fr.po
@@ -0,0 +1,19 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: 2013-12-22 17:11+0200\n"
+"Last-Translator: goofy <pierre.gaufillet@gmail.com>\n"
+"Language-Team: French\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+msgid "JavaScript required!"
+msgstr ""
+
+msgid ""
+"You must enable JavaScript in your browser or the web interface will not "
+"work properly."
+msgstr ""
diff --git a/package/gluon-web-theme/i18n/gluon-web-theme.pot b/package/gluon-web-theme/i18n/gluon-web-theme.pot
new file mode 100644
index 0000000000000000000000000000000000000000..1676d857eca6a35a322e75971bac40a94624fe63
--- /dev/null
+++ b/package/gluon-web-theme/i18n/gluon-web-theme.pot
@@ -0,0 +1,10 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+msgid "JavaScript required!"
+msgstr ""
+
+msgid ""
+"You must enable JavaScript in your browser or the web interface will not "
+"work properly."
+msgstr ""
diff --git a/package/gluon-web-theme/sass/cascade.scss b/package/gluon-web-theme/sass/gluon.scss
similarity index 98%
rename from package/gluon-web-theme/sass/cascade.scss
rename to package/gluon-web-theme/sass/gluon.scss
index 3acd9c83047406babcfe77c625f79f8079603f92..be29c9907bebaf294ac1066aa75ead56086efeed 100644
--- a/package/gluon-web-theme/sass/cascade.scss
+++ b/package/gluon-web-theme/sass/gluon.scss
@@ -4,7 +4,7 @@
 
    Use sass like this to update it:
 
-   sass --sourcemap=none -C -t compressed sass/cascade.scss files/lib/gluon/web/www/static/gluon/cascade.css
+   sass --sourcemap=none -C -t compressed sass/gluon.scss files/lib/gluon/web/www/static/gluon.css
 
    When commiting changes to this file make sure to commit the respective
    changes to the compilid version within the same commit!
diff --git a/package/gluon-web/files/lib/gluon/web/view/layout.html b/package/gluon-web/files/lib/gluon/web/view/layout.html
deleted file mode 100644
index 34f287c1ec89e9c09aed389aaea5d0662fecd1d9..0000000000000000000000000000000000000000
--- a/package/gluon-web/files/lib/gluon/web/view/layout.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<%
-	include("themes/" .. theme .. "/layout")
-%>
diff --git a/package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon b/package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon
index 9f8c41ab47f808d682dfc9add19c429689e555df..cceb2a6884572dca16172b657d1f19aed48a45a3 100755
--- a/package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon
+++ b/package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon
@@ -2,4 +2,7 @@
 
 require 'gluon.web.cgi' {
 	base_path = '/lib/gluon/web',
+
+	layout_package = 'gluon-web-theme',
+	layout_template = 'theme/layout',
 }
diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
index 4acc2c6c943b9cc175b1792bfc24407c604f63b4..64449cd0d0fe4e145b6e89812ca84dfa051107e3 100644
--- a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
+++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
@@ -86,9 +86,6 @@ local function dispatch(config, http, request)
 		write       = function(...) return http:write(...) end,
 		pcdata      = util.pcdata,
 		urlencode   = proto.urlencode,
-		media       = '/static/gluon',
-		theme       = 'gluon',
-		resource    = '/static/resources',
 		attr        = attr,
 		url         = function(path) return build_url(http, path) end,
 	}, { __index = _G }))
@@ -136,10 +133,10 @@ local function dispatch(config, http, request)
 					end
 				end,
 
-				template = function(view)
+				template = function(view, scope)
 					local pkg = _pkg
 					return function()
-						renderer.render("layout", {content = view, pkg = pkg})
+						renderer.render_layout(view, scope, pkg)
 					end
 				end,
 
@@ -178,15 +175,11 @@ local function dispatch(config, http, request)
 
 	if not node or not node.target then
 		http:status(404, "Not Found")
-		renderer.render("layout", {
-			content = "error/404",
-			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',
-		})
+		renderer.render_layout("error/404", {
+			message =
+				"No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
+			        "If this URL belongs to an extension, make sure it is properly installed.\n",
+		}, 'gluon-web')
 		return
 	end
 
@@ -195,15 +188,11 @@ local function dispatch(config, http, request)
 	local ok, err = pcall(node.target)
 	if not ok then
 		http:status(500, "Internal Server Error")
-		renderer.render("layout", {
-			content = "error/500",
-			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',
-		})
+		renderer.render_layout("error/500", {
+			message =
+				"Failed to execute dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
+				"The called action terminated with an exception:\n" .. tostring(err or "(unknown)"),
+		}, 'gluon-web')
 	end
 end
 
diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua
index 0aea336db7dc7d90b76eb7d17e69b853fa596341..84a9b5c1b2966ffe9b8dd8ed30d93eba8eb2428d 100644
--- a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua
+++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua
@@ -64,6 +64,7 @@ return function(config, env)
 	--- Render a certain template.
 	-- @param name		Template name
 	-- @param scope		Scope to assign to template (optional)
+	-- @param pkg		i18n namespace package (optional)
 	function ctx.render(name, scope, pkg)
 		local sourcefile = viewdir .. name .. ".html"
 		local template, _, err = tparser.parse(sourcefile)
@@ -78,6 +79,7 @@ return function(config, env)
 	--- Render a template from a string.
 	-- @param template	Template string
 	-- @param scope		Scope to assign to template (optional)
+	-- @param pkg		i18n namespace package (optional)
 	function ctx.render_string(str, scope, pkg)
 		local template, _, err = tparser.parse_string(str)
 
@@ -87,5 +89,20 @@ return function(config, env)
 		render_template('(local)', template, scope, pkg)
 	end
 
+	--- Render a template, wrapped in the configured layout.
+	-- @param name		Template name
+	-- @param scope		Scope to assign to template (optional)
+	-- @param pkg		i18n namespace package (optional)
+	-- @param layout_scope  Additional variables to pass to the layout template
+	function ctx.render_layout(name, scope, pkg, layout_scope)
+		ctx.render(config.layout_template, setmetatable({
+			content = name,
+			scope = scope,
+			pkg = pkg,
+		}, {
+			__index = layout_scope
+		}), config.layout_package)
+	end
+
 	return ctx
 end