docs: drop older docs

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
This commit is contained in:
Paul Donald
2026-02-12 18:43:09 +01:00
parent 90836a0fac
commit d612a68062
5 changed files with 0 additions and 805 deletions

View File

@@ -1,251 +0,0 @@
# Writing LuCI CBI models
See [online wiki](https://github.com/openwrt/luci/wiki/CBI) for latest version.
CBI models are Lua files describing the structure of an UCI config file and the resulting HTML form to be evaluated by the CBI parser.<br />
All CBI model files must return an object of type `luci.cbi.Map`.<br />
For a commented example of a CBI model, see the [Writing Modules tutorial](./ModulesHowTo.md).
The scope of a CBI model file is automatically extended by the contents of the module `luci.cbi` and the `translate` function from `luci.i18n`.
This Reference covers **the basics** of the CBI system.
## class Map (config, title, description)
This is the root object of the model.
* `config:` configuration filename to be mapped, see [UCI documentation](https://openwrt.org/docs/guide-user/base-system/uci) and the files in `/etc/config`
* `title:` title shown in the UI
* `description:` description shown in the UI
#### function :section (sectionclass, ...)
Creates a new section
* `sectionclass`: a class object of the section
* _additional parameters passed to the constructor of the section class_
----
## class NamedSection (name, type, title, description)
An object describing an UCI section selected by the name.<br />
To instantiate use: `Map:section(NamedSection, "name", "type", "title", "description")`
* `name:` UCI section name
* `type:` UCI section type
* `title:` The title shown in the UI
* `description:` description shown in the UI
#### function :option(optionclass, ...)
Creates a new option
* `optionclass:` a class object of the section
* _additional parameters passed to the constructor of the option class_
#### property .addremove = false
Allows the user to remove and recreate the configuration section.
#### property .dynamic = false
Marks this section as dynamic.
Dynamic sections can contain an undefinded number of completely userdefined options.
#### property .optional = true
Parse optional options
----
## class TypedSection (type, title, description)
An object describing a group of UCI sections selected by their type.<br />
To instantiate use: `Map:section(TypedSection, "type", "title", "description")`
* `type:` UCI section type
* `title:` The title shown in the UI
* `description:` description shown in the UI
#### function :option(optionclass, ...)
Creates a new option
* `optionclass:` a class object of the section
* _additional parameters passed to the constructor of the option class_
#### function :depends(key, value)
Only show this option field if another option `key` is set to `value` in the same section.<br />
If you call this function several times the dependencies will be linked with **"or"**
#### function .filter(self, section) -abstract-
You can override this function to filter certain sections that will not be parsed.
The filter function will be called for every section that should be parsed and returns `nil` for sections that should be filtered.
For all other sections it should return the section name as given in the second parameter.
#### property .addremove = false
Allows the user to remove and recreate the configuration section
#### property .dynamic = false
Marks this section as dynamic.
Dynamic sections can contain an undefinded number of completely userdefined options.
#### property .optional = true
Parse optional options
#### property .anonymous = false
Do not show UCI section names
----
## class Value (option, title, description)
An object describing an option in a section of a UCI File. Creates a standard text field in the formular.<br />
To instantiate use: `NamedSection:option(Value, "option", "title", "description")`<br />
or `TypedSection:option(Value, "option", "title", "description")`
* `option:` UCI option name
* `title:` The title shown in the UI
* `description:` description shown in the UI
#### function :depends(key, value)
Only show this option field if another option `key` is set to `value` in the same section.<br />
If you call this function several times the dependencies will be linked with **"or"**
#### function :value(key, value)
Convert this text field into a combobox if possible and add a selection option.
#### property .default = nil
The default value
#### property .maxlength = nil
The maximum input length (of chars) of the value
#### property .optional = false
Marks this option as optional, implies `.rmempty = true`
#### property .rmempty = true
Removes this option from the configuration file when the user enters an empty value
#### property .size = nil
The maximum number of chars displayed by form field
----
## class ListValue (option, title, description)
An object describing an option in a section of a UCI File.<br />
Creates a list box or list of radio (for selecting one of many choices) in the formular.<br />
To instantiate use: `NamedSection:option(ListValue, "option", "title", "description")`<br />
or `TypedSection:option(ListValue, "option", "title", "description")`
* `option:` UCI option name
* `title:` The title shown in the UI
* `description:` description shown in the UI
#### function :depends(key, value)
Only show this option field if another option `key` is set to `value` in the same section.<br />
If you call this function several times the dependencies will be linked with **"or"**
#### function :value(key, value)
Adds an entry to the selection list
#### property .widget = "select"
`select` shows a selection list, `radio` shows a list of radio buttons inside form
#### property .default = nil
The default value
#### property .optional = false
Marks this option as optional, implies `.rmempty = true`
#### property .rmempty = true
Removes this option from the configuration file when the user enters an empty value
#### property .size = nil
The size of the form field
----
## class Flag (option, title, description)
An object describing an option with two possible values in a section of a UCI File.<br />
Creates a checkbox field in the formular.<br />
To instantiate use: `NamedSection:option(Flag, "option", "title", "description")`<br />
or `TypedSection:option(Flag, "option", "title", "description")`
* `option:` UCI option name
* `title:` The title shown in the UI
* `description:` description shown in the UI
#### function :depends (key, value)
Only show this option field if another option `key` is set to `value` in the same section.<br />
If you call this function several times the dependencies will be linked with **"or"**
#### property .default = nil
The default value
#### property .disabled = 0
the value that should be set if the checkbox is unchecked
#### property .enabled = 1
the value that should be set if the checkbox is checked
#### property .optional = false
Marks this option as optional, implies `.rmempty = true`
#### property .rmempty = true
Removes this option from the configuration file when the user enters an empty value
----
## class MultiValue (option, title, description)
An object describing an option in a section of a UCI File.<br />
Creates a list of checkboxed or a multiselectable list as form fields.<br />
To instantiate use: `NamedSection:option(MultiValue, "option", "title", "description")`<br />
or `TypedSection:option(MultiValue, "option", "title", "description")`
* `option:` UCI option name
* `title:` The title shown in the UI
* `description:` description shown in the UI
#### function :depends (key, value)
Only show this option field if another option `key` is set to `value` in the same section.<br />
If you call this function several times the dependencies will be linked with **"or"**
#### function :value(key, value)
Adds an entry to the list
#### property .widget = "checkbox"
`select` shows a selection list, `checkbox` shows a list of checkboxes inside form
#### property .delimiter = " "
The string which will be used to delimit the values inside stored option
#### property .default = nil
The default value
#### property .optional = false
Marks this option as optional, implies `.rmempty = true`
#### property .rmempty = true
Removes this option from the configuration file when the user enters an empty value
#### property .size = nil
The size of the form field (only used if property `.widget = "select"`)
----
## class StaticList (option, title, description)
Similar to the `MultiValue`, but stores selected Values into a UCI list instead of a character-separated option.
----
## class DynamicList (option, title, description)
A extensible list of user-defined values. Stores Values into a UCI list
----
## class DummyValue (option, title, description)
Creates a readonly text in the form. !It writes no data to UCI!<br />
To instantiate use: `NamedSection:option(DummyValue, "option", "title", "description")`<br />
or `TypedSection:option(DummyValue, "option", "title", "description")`
* `option:` UCI option name
* `title:` The title shown in the UI
* `description:` description shown in the UI
#### function :depends (key, value)
Only show this option field if another option `key` is set to `value` in the same section.<br />
If you call this function several times the dependencies will be linked with **"or"**
----
## class TextValue (option, title, description)
An object describing a multi-line textbox in a section in a non-UCI form.
----
## class Button (option, title, description)
An object describing a Button in a section in a non-UCI form.

View File

@@ -1,196 +0,0 @@
# New in LuCI 0.10
See [online wiki](https://github.com/openwrt/luci/wiki/LuCI-0.10) for latest version.
This document describes new features and incompatibilities to LuCI 0.9.x.
It is targeted at module authors developing external addons to LuCI.
## I18N Changes
### API
The call conventions for the i18n api changed, there is no dedicated translation
key anymore and the english text is used for lookup instead. This was done to
ease the maintenance of language files.
Code that uses `translate()` or `i18n()` must be changed as follows:
```lua
-- old style:
translate("some_text", "Some Text")
translatef("some_format_text", "Some formatted Text: %d", 123)
-- new style:
translate("Some Text")
translatef("Some formatted Text: %d", 123)
```
Likewise for templates:
```html
<!-- old style: -->
<%:some_text Some Text%>
<!-- new style: -->
<%:Some Text%>
```
If code must support both LuCI 0.9.x and 0.10.x versions, it is suggested to write the calls as follows:
```lua
translate("Some Text", "Some Text")
```
An alternative is wrapping translate() calls into a helper function:
```lua
function tr(key, alt)
return translate(key) or translate(alt) or alt
end
```
... which is used as follows:
```lua
tr("some_key", "Some Text")
```
### Translation File Format
Translation catalogs are now maintained in `*.po` format files.
During build those get translated into [*.lmo archives](https://github.com/openwrt/luci/wiki/LMO).
#### Components built within the LuCI tree
If components using translations are built along with the LuCI tree, the newly added *.po file are automatically
compiled into *.lmo archives during the build process. In order to bundle the appropriate *.lmo files into the
corresponding *.ipk packages, component Makefiles must include a "PO" variable specifying the files to include.
Given a module `applications/example/` which uses `po/en/example.po` and `po/en/example-extra.po`,
the `applications/example/Makefile` must be changed as follows:
```Makefile
PO = example example-extra
include ../../build/config.mk
include ../../build/module.mk
```
#### Standalone components
Authors who externally package LuCI components must prepare required `*.lmo` archives themselves.
To convert existing Lua based message catalogs to the `*.po` format, the `build/i18n-lua2po.pl` helper script can be used.
In order to convert `*.po` files into `*.lmo` files, the standalone `po2lmo` utility must be compiled as follows:
```
$ svn co http://svn.luci.subsignal.org/luci/branches/luci-0.10/libs/lmo
$ cd lmo/
$ make
$ ./src/po2lmo translations.po translations.lmo
```
Note that at the time of writing, the utility program needs Lua headers installed on the system in order to compile properly.
## CBI
### Datatypes
The server side UVL validation has been dropped to reduce space requirements on the target.
Instead it is possible to define datatypes for CBI widgets now:
```lua
opt = section:option(Value, "optname", "Title Text")
opt.datatype = "ip4addr"
```
User provided data is validated once on the frontend via JavaScript and on the server side prior to saving it.
A list of possible datatypes can be found in the [luci.cbi.datatypes](https://github.com/openwrt/luci/blob/master/modules/luci-compat/luasrc/cbi/datatypes.lua) class.
### Validation
Server-side validator functions can now return custom error messages to provide better feedback on invalid input.
```lua
opt = section:option(Value, "optname", "Title Text")
function opt.validate(self, value, section)
if input_is_valid(value) then
return value
else
return nil, "The value is invalid because ..."
end
end
```
### Tabs
It is now possible to break up CBI sections into multiple tabs to better organize longer forms.
The TypedSection and NamedSection classes gained two new functions to define tabs, `tab()` and `taboption()`.
```lua
sct = map:section(TypedSection, "name", "type", "Title Text")
sct:tab("general", "General Tab Title", "General Tab Description")
sct:tab("advanced", "Advanced Tab Title", "Advanced Tab Description")
opt = sct:taboption("general", Value, "optname", "Title Text")
```
The `tab()` function declares a new tab and takes up to three arguments:
* Internal name of the tab, must be unique within the section
* Title text of the tab
* Optional description text for the tab
The `taboption()` function wraps `option()` and assigns the option object to the given tab.
It takes up to five arguments:
* Name of the tab to assign the option to
* Option type, e.g. Value or DynamicList
* Option name
* Title text of the option
* Optional description text of the option
If tabs are used within a particular section, the `option()` function must not be used,
doing so results in undefined behaviour.
### Hooks
The CBI gained support for `hooks` which can be used to trigger additional actions during the
life-cycle of a map:
```lua
map = Map("config", "Title Text")
function map.on_commit(self)
-- do something if the UCI configuration got committed
end
```
The following hooks are defined:
* `on_cancel`: The user pressed cancel within a multistep Delegator or a SimpleForm instance
* `on_init`: The CBI is about to render the Map object
* `on_parse`: The CBI is about to read received HTTP form values
* `on_save`, `on_before_save`: The CBI is about to save modified UCI configuration files
* `on_after_save`: Modified UCI configuration files just got saved
* `on_before_commit`: The CBI is about to commit the changes
* `on_commit`, `on_after_commit`, `on_before_apply`: Modified configurations got committed and the CBI is about to restart associated services
* `on_apply`, `on_after_apply`: All changes where completely applied (only works on Map instances with the apply_on_parse attribute set)
### Sortable Tables
TypedSection instances which use the `cbi/tblsection` template may now use a new attribute `sortable` to allow the user to reorder table rows.
```lua
sct = map:section(TypedSection, "name", "type", "Title Text")
sct.template = "cbi/tblsection"
sct.sortable = true
```
## JavaScript
The LuCI 0.10 branch introduced a new JavaScript file `xhr.js` which provides support routines for `XMLHttpRequest` operations.
Each theme must include this file in the `<head>` area of the document for forms to work correctly.
It should be included like this:
```html
<script src="<%=resource%>/xhr.js"></script>
```

View File

@@ -1,171 +0,0 @@
# HowTo: Write Lua based Modules (deprecated for client side modules)
See [online wiki](https://github.com/openwrt/luci/wiki/ModulesHowTo) for latest version.
**Note:** If you plan to integrate your module into LuCI, you should read the [Module Reference](./Modules.md) in advance.
This tutorial describes how to write your own modules for the LuCI WebUI.
For this tutorial we refer to your LuCI installation directory as `lucidir` (`/usr/lib/lua/luci` on your OpenWRT device) and assume your LuCI installation is reachable through your webserver via `http://192.168.1.1/cgi-bin/luci`.
The recommended way to set up a development environment:
- Install OpenWRT on your router/device (You could use a QEMU or VirtualBox image instead)
- Install SSHFS on your host
- Mount your routers' root (`/`) someplace on your development host (eg. `/mnt/router`)
- Then open `/mnt/router/(lucidir)` in your favorite development studio
Extra:
- Add configurations to your dev studio which will delete the luci cache (detailed below) and then open a browser window to your routers' configuration page in order to see your module/application.
When testing, if you have edited index files, be sure to remove the folder `/tmp/luci-modulecache/*` and the file(s) `/tmp/luci-indexcache*`, then refresh the LUCI page to see your edits.
## The dispatching process
LuCI uses a dispatching tree that is built by executing the index-Function of every available controller.
The CGI-environment variable `PATH_INFO` will be used as the path in this dispatching tree, e.g.: `/cgi-bin/luci/foo/bar/baz`
resolves to `foo.bar.baz`.
To register a function in the dispatching tree, use the `entry`-function of `luci.dispatcher`. It takes 4 arguments (2 are optional):
```lua
entry(path, target, title=nil, order=nil)
```
* `path` is a table that describes the position in the dispatching tree: For example a path of `{"foo", "bar", "baz"}` would insert your node in `foo.bar.baz`.
* `target` describes the action that will be taken when a user requests the node. There are several predefined actions, of which the 3 most important (call, template, cbi) are described later on this page
* `title` defines the title that will be visible to the user in the menu (optional)
* `order` is a number with which nodes on the same level will be sorted in the menu (optional)
You can assign more attributes by manipulating the node table returned by the entry-function. A few example attributes:
* `i18n` defines which translation file should be automatically loaded when the page gets requested
* `dependent` protects plugins to be called out of their context if a parent node is missing
* `leaf` stops parsing the request at this node and goes no further in the dispatching tree
* `sysauth` requires the user to authenticate with a given system user account
# Naming and the module file
Now we can start writing modules. Choose the category and name of your new digital child.
Let's assume you want to create a new application `myapp` with a module `mymodule`.
So you have to create a new sub-directory `lucidir/controller/myapp` with a file `mymodule.lua` with the following content:
```lua
module("luci.controller.myapp.mymodule", package.seeall)
function index()
end
```
The first line is required for Lua to correctly identify the module and create its scope.
The `index`-Function will be used to register actions in the dispatching tree.
## Teaching your new child (Actions)
So it has a name, but no actions.
We assume you want to reuse your module myapp.mymodule that you began in the last step.
### Actions
Reopen `lucidir/controller/myapp/mymodule.lua` and just add a function to it with:
```lua
module("luci.controller.myapp.mymodule", package.seeall)
function index()
entry({"click", "here", "now"}, call("action_tryme"), "Click here", 10).dependent=false
end
function action_tryme()
luci.http.prepare_content("text/plain")
luci.http.write("Haha, rebooting now...")
luci.sys.reboot()
end
```
And now visit the path `/cgi-bin/luci/click/here/now` (`http://192.168.1.1/luci/click/here/now` if you are using the development environment) in your browser.
These action functions simply have to be added to a dispatching entry.
As you may or may not know: CGI specification requires you to send a `Content-Type` header before you can send your content. You will find several shortcuts (like the one used above) as well as redirecting functions in the module `luci.http`
### Views
If you only want to show the user text or some interesting family photos, it may be enough to use an HTML-template.
These templates can also include some Lua code but be aware that writing whole office-suites by only using these templates might be considered "dirty" by other developers.
Now let's create a little template `lucidir/view/myapp-mymodule/helloworld.htm` with the content:
```html
<%+header%>
<h1><%:Hello World%></h1>
<%+footer%>
```
and add the following line to the `index`-Function of your module file.
```lua
entry({"my", "new", "template"}, template("myapp-mymodule/helloworld"), "Hello world", 20).dependent=false
```
Now visit the path `/cgi-bin/luci/my/new/template` (`http://192.168.1.1/luci/my/new/template`) in your browser.
You may notice those special `<% %>`-Tags, these are [template markups](./Templates.md) used by the LuCI template processor.
It is always good to include header and footer at the beginning and end of a template as those create the default design and menu.
### CBI models
The CBI is one of the coolest features of LuCI.
It creates a formulae based user interface and saves its contents to a specific UCI config file.
You only have to describe the structure of the configuration file in a CBI model file and Luci does the rest of the work.
This includes generating, parsing and validating an XHTML form and reading and writing the UCI file.
So let's be serious at least for this paragraph and create a practical example `lucidir/model/cbi/myapp-mymodule/netifaces.lua` with the following contents:
```lua
m = Map("network", "Network") -- We want to edit the uci config file /etc/config/network
s = m:section(TypedSection, "interface", "Interfaces") -- Especially the "interface"-sections
s.addremove = true -- Allow the user to create and remove the interfaces
function s:filter(value)
return value ~= "loopback" and value -- Don't touch loopback
end
s:depends("proto", "static") -- Only show those with "static"
s:depends("proto", "dhcp") -- or "dhcp" as protocol and leave PPPoE and PPTP alone
p = s:option(ListValue, "proto", "Protocol") -- Creates an element list (select box)
p:value("static", "static") -- Key and value pairs
p:value("dhcp", "DHCP")
p.default = "static"
s:option(Value, "ifname", "interface", "the physical interface to be used") -- This will give a simple textbox
s:option(Value, "ipaddr", translate("ip", "IP Address")) -- Yes, this is an i18n function ;-)
s:option(Value, "netmask", "Netmask"):depends("proto", "static") -- You may remember this "depends" function from above
mtu = s:option(Value, "mtu", "MTU")
mtu.optional = true -- This one is very optional
dns = s:option(Value, "dns", "DNS-Server")
dns:depends("proto", "static")
dns.optional = true
function dns:validate(value) -- Now, that's nifty, eh?
return value:match("[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+") -- Returns nil if it doesn't match otherwise returns match
end
gw = s:option(Value, "gateway", "Gateway")
gw:depends("proto", "static")
gw.rmempty = true -- Remove entry if it is empty
return m -- Returns the map
```
and of course remember to add something like this to your module's `index`-Function.
```lua
entry({"admin", "network", "interfaces"}, cbi("myapp-mymodule/netifaces"), "Network interfaces", 30).dependent=false
```
There are many more features. See [the CBI reference](./CBI.md) and the modules shipped with LuCI.

View File

@@ -1,68 +0,0 @@
## Templates
See [online wiki](https://github.com/openwrt/luci/wiki/Templates) for latest version.
LuCI has a simple regex based template processor which parses HTML-files to Lua functions and allows to store precompiled template files.
The simplest form of a template is just an ordinary HTML-file. It will be printed out to the user as is.
In LuCI every template is an object with an own scope
It can therefore be instanced and each instance can have a different scope.
As every template processor. LuCI supports several special markups. Those are enclosed in `<% %>`-Tags.
By adding `-` (dash) right after the opening `<%` every whitespace before the markup will be stripped.
Adding a `-` right before the closing `%>` will equivalently strip every whitespace behind the markup.
## Builtin functions and markups
### Including Lua code
*Markup:*
```
<% code %>
```
### Writing variables and function values
*Syntax:*
```
<% write (value) %>
```
*Short-Markup:*
```
<%=value%>
```
### Including templates
*Syntax:*
```
<% include (templatename) %>
```
*Short-Markup:*
```
<%+templatename%>
```
### Translating
*Syntax:*
```
<%= translate("Text to translate") %>
```
*Short-Markup:*
```
<%:Text to translate%>
```
### Commenting
*Markup:*
```
<%# comment %>
```
## Builtin constants
* `REQUEST_URI`: The current URL (without server part)
* `controller`: Path to the Luci main dispatcher
* `resource`: Path to the resource directory
* `media`: Path to the active theme directory

View File

@@ -1,119 +0,0 @@
# Internationalization (i18n)
See [online wiki](https://github.com/openwrt/luci/wiki/i18n) for latest version.
## Use translation function
### Translations in JavaScript
Wrap translatable strings with `_()` e.g. `_('string to translate')` and the `i18n-scan.pl` and friends will correctly identify these strings for translation.
If you have multi line strings you can split them with concatenation:
```js
var mystr = _('this string will translate ' +
'correctly even though it is ' +
'a multi line string!');
```
You may also use line continuations `\` syntax:
```js
var mystr = _('this string will translate \
correctly even though it is \
a multi line string');
```
Usually if you have multiple sentences you may need to use a line break. Use the `<br />` HTML tag like so:
```js
var mystr = _('Port number.') + '<br />' +
_('E.g. 80 for HTTP');
```
Use `<br />` and **not** `<br>` or `<br/>`.
If you have a link inside a translation, move its attributes out of a translation key:
```js
var mystr = _('For further information <a %s>check the wiki</a>')
.format('href="https://openwrt.org/docs/" target="_blank" rel="noreferrer"')
```
This will generate a full link with HTML `For further information <a href="https://openwrt.org/docs/" target="_blank" rel="noreferrer">check the wiki</a>`. The `noreferrer` is important so that it is opened in a new tab (`target="_blank"`).
### Translations in LuCI lua+html templates
Use the `<%: text to translate %>` as documented on [Templates](./Templates.md)
### Translations in Lua controller code and Lua CBIs
As hinted at in the Templates doc, the `%:` invokes a `translate()` function.
In most controller contexts, this is already available for you, but if necessary, is available for include in `luci.i18n.translate`
## Translation files
Translations are saved in the folder `po/` within each individual LuCI component directory, e.g. `applications/luci-app-acl/po/`.
The template is in `po/templates/<package>.pot`.
The individual language translation files can be found at `po/[lang]/[package].po`.
In order to use the commands below you need to have the `gettext` utilities (`msgcat`, `msgfmt`, `msgmerge`) installed on your system.
On Debian/Ubuntu, install them with `sudo apt install gettext`.
### Initialize po files
When you add or update an app, run from your `applications/luci-app-acl/` app folder:
../../build/i18n-add-language.sh
This creates the skeleton .po files for all available languages open for translation for your app.
Or from the luci repo root:
./build/i18n-add-language.sh
This creates the skeleton .po files for all existing languages open for translation for all sub-folders.
### Rebuild po files (for existing languages)
After you make changes to a package, run:
./build/i18n-sync.sh applications/[application]
Example:
./build/i18n-sync.sh applications/luci-app-acl
This only updates those language .po files that already exist in `applications/luci-app-acl/po/`. See the previous step to add a new language.
Note: the directory argument can be omitted to update all po template and po files.
Some packages share translation files, in this case you need to scan through all their folders:
./build/i18n-scan.pl applications/[package-1] applications/[package-2] applications/[package-n] > [location of shared template]/[application].pot
This is what the `mkbasepot.sh` script does for the `luci-base` module:
./build/i18n-scan.pl \
modules/luci-base modules/luci-compat modules/luci-lua-runtime \
modules/luci-mod-network modules/luci-mod-status modules/luci-mod-system \
protocols themes \
> modules/luci-base/po/templates/base.pot
*Note:* The translation catalog for the base system covers multiple components. Use the following commands to update it:
./build/mkbasepot.sh
./build/i18n-update.pl
### LMO files
The `*.po` files are big so Luci needs them in a compact compiled [LMO format](./LMO.md).
Luci reads `*.lmo` translations from the `/usr/lib/lua/luci/i18n/` folder.
E.g. `luci-app-acl` has an Arabic translation in `luci-i18n-acl-ar` package that installs `/usr/lib/lua/luci/i18n/acl.ar.lmo` file.
In order to quickly convert a single `.po` file to `.lmo` file for testing on the target system use the `po2lmo` utility.
You will need to compile it from the `luci-base` module:
$ cd modules/luci-base/src/
$ make po2lmo
$ ./po2lmo
Usage: ./po2lmo input.po output.lmo
Now you can compile and upload the translation:
./po2lmo ../../../applications/luci-app-acl/po/ar/acl.po ./acl.ar.lmo
scp ./acl.ar.lmo root@192.168.1.1:/usr/lib/lua/luci/i18n/
You can change languages in [System /Language and Style](http://192.168.1.1/cgi-bin/luci/admin/system/system) and check the translation.