From b2551f6a2209b8a11b42834cb0d63f5c03a2b95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Mart=C3=ADnez?= Date: Sat, 21 Jan 2017 01:04:03 -0300 Subject: [PATCH] Add support for gettext message catalogs. --- LICENSE.md | 25 ++++ README-es.md | 41 +++++++ README-es_UY.md | 136 --------------------- README.md | 160 +++++-------------------- doc/developer.md | 96 +++++++++++++++ doc/translator.md | 20 ++++ gettext.lua | 288 +++++++++++++++++++++++++++++++++++++++++++++ init.lua | 90 +++++++++++--- lib/intllib.lua | 45 +++++++ tools/xgettext.bat | 33 ++++++ tools/xgettext.sh | 23 ++++ 11 files changed, 677 insertions(+), 280 deletions(-) create mode 100644 LICENSE.md create mode 100644 README-es.md delete mode 100644 README-es_UY.md create mode 100644 doc/developer.md create mode 100644 doc/translator.md create mode 100644 gettext.lua create mode 100644 lib/intllib.lua create mode 100644 tools/xgettext.bat create mode 100755 tools/xgettext.sh diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9f2b419 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,25 @@ + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README-es.md b/README-es.md new file mode 100644 index 0000000..f1f428e --- /dev/null +++ b/README-es.md @@ -0,0 +1,41 @@ + +# Bilioteca de internacionalización para Minetest + +Por Diego Martínez (kaeza). +Lanzada bajo Unlicense. Véase `LICENSE.md` para más detalles. + +Éste mod es un intento por proveer soporte para internacionalización +de los mods (algo que a Minetest le falta de momento). + +Si tienes alguna duda/comentario, por favor publica en el +[tema del foro][topic]. Por reporte de errores, use el +[bugtracker][bugtracker] en Github. + +## Cómo usar + +Si eres un jugador regular en busca de textos traducidos, simplemente +[instala][installing_mods] éste mod como cualquier otro. + +El mod trata de detectar tu idioma, pero ya que no hay una forma portable de +hacerlo, prueba varias alternativas: + +* `language` setting in `minetest.conf`. +* `LANGUAGE` environment variable. +* `LANG` environment variable. + +En cualquier caso, el resultado final debería ser el +[Código de idioma ISO 639-1][ISO639-1] del idioma deseado. + +### Desarrolladores + +Si desarrollas mods y estás buscando añadir soporte de internacionalización +a tu mod, ve el fichero `doc/developer.md`. + +### Traductores + +Si eres un traductor, ve el fichero `doc/translator.md`. + +[topic]: https://forum.minetest.net/viewtopic.php?id=4929 +[bugtracker]: https://github.com/minetest-mods/intllib/issues +[installing_mods]: https://wiki.minetest.net/Installing_mods/es +[ISO639-1]: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes diff --git a/README-es_UY.md b/README-es_UY.md deleted file mode 100644 index 79ea84d..0000000 --- a/README-es_UY.md +++ /dev/null @@ -1,136 +0,0 @@ - -# Biblioteca de Internacionalización para Minetest - -Por Diego Martínez (kaeza). -Lanzada bajo WTFPL. - -Éste mod es un intento de proveer soporte para internacionalización para otros mods -(lo cual Minetest carece actualmente). - -## Cómo usar - -### Para usuarios finales - -Para usar éste mod, simplemente [instálalo](http://wiki.minetest.net/Installing_Mods) -y habilítalo en la interfaz. - -Éste mod intenta detectar el idioma del usuario, pero ya que no existe una solución -portable para hacerlo, éste intenta varias alternativas, y utiliza la primera -encontrada: - - * Opción `language` en `minetest.conf`. - * Si ésta no está definida, usa la variable de entorno `LANG` (ésta está - siempre definida en SOs como Unix). - * Si todo falla, usa `en` (lo cual básicamente significa textos sin traducir). - -En todo caso, el resultado final debe ser el In any case, the end result should be the -[Código de Idioma ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) -del idioma deseado. Tenga en cuenta tambien que (de momento) solo los dos primeros -caracteres son usados, así que por ejemplo, las opciones `de_DE.UTF-8`, `de_DE`, -y `de` son iguales. - -Algunos códigos comúnes: `es` para Español, `pt` para Portugués, `fr` para Francés, -`it` para Italiano, `de` para Aleman. - -### Para desarrolladores - -Para habilitar funcionalidad en tu mod, copia el siguiente fragmento de código y pégalo -al comienzo de tus archivos fuente: - -```lua --- Boilerplate to support localized strings if intllib mod is installed. -local S -if minetest.get_modpath("intllib") then - S = intllib.Getter() -else - -- Si no requieres patrones de reemplazo (@1, @2, etc) usa ésto: - S = function(s) return s end - - -- Si requieres patrones de reemplazo, pero no escapes, usa ésto: - S = function(s,a,...)a={a,...}return s:gsub("@(%d+)",function(n)return a[tonumber(n)]end)end - - -- Usa ésto si necesitas funcionalidad completa: - S = function(s,a,...)if a==nil then return s end a={a,...}return s:gsub("(@?)@(%(?)(%d+)(%)?)",function(e,o,n,c)if e==""then return a[tonumber(n)]..(o==""and c or"")else return"@"..o..n..c end end) end -end -``` - -Tambien necesitarás depender opcionalmente de intllib. Para hacerlo, añade `intllib?` -a tu archivo `depends.txt`. Ten en cuenta tambien que si intllib no está instalado, -la función `S` es definida para regresar la cadena sin cambios. Ésto se hace para -evitar la necesidad de llenar tu código con montones de `if`s (o similar) para verificar -que la biblioteca está instalada. - -Luego, para cada cadena de texto a traducir en tu código, usa la función `S` -(definida en el fragmento de arriba) para regresar la cadena traducida. Por ejemplo: - -```lua -minetest.register_node("mimod:minodo", { - -- Cadena simple: - description = S("My Fabulous Node"), - -- Cadena con patrones de reemplazo: - description = S("@1 Car", "Blue"), - -- ... -}) -``` - -Nota: Las cadenas en el código fuente por lo general deben estar en ingles ya que -es el idioma que más se habla. Es perfectamente posible especificar las cadenas -fuente en español y proveer una traducción al ingles, pero no se recomienda. - -Luego, crea un directorio llamado `locale` dentro del directorio de tu mod, y crea -un archivo "plantilla" (llamado `template.txt` por lo general) con todas las cadenas -a traducir (ver *Formato de archivo de traducciones* más abajo). Los traductores -traducirán las cadenas en éste archivo para agregar idiomas a tu mod. - -### Para traductores - -Para traducir un mod que tenga soporte para intllib al idioma deseado, copia el -archivo `locale/template.txt` a `locale/IDIOMA.txt` (donde `IDIOMA` es el -[Código de Idioma ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) -de tu idioma (`es` para español). - -Abre el archivo en tu editor favorito, y traduce cada línea colocando el texto -traducido luego del signo de igualdad. - -Ver *Formato de archivo de traducciones* más abajo. - -## Formato de archivo de traducciones - -He aquí un ejemplo de archivo de idioma para el español (`es.txt`): - -```cfg -# Un comentario. -# Otro comentario. -Ésta línea es ignorada porque no tiene un signo de igualdad. -Hello, World! = Hola, Mundo! -String with\nnewlines = Cadena con\nsaltos de linea -String with an \= equals sign = Cadena con un signo de \= igualdad -``` - -Archivos de idioma (o traducción) son archivos de texto sin formato que consisten de -líneas con el formato `texto fuente = texto traducido`. El archivo debe ubicarse en el -subdirectorio `locale` del mod, y su nombre debe ser las dos letras del -[Código de Idioma ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) -del lenguaje al cual se desea traducir. - -Los archivos deben usar la codificación UTF-8. - -Las líneas que comienzan en el símbolo numeral (`#`) son comentarios y son ignoradas -por el lector. Tenga en cuenta que los comentarios terminan al final de la línea; -no hay soporte para comentarios multilínea. Las líneas que no contengan un signo -de igualdad (`=`) tambien son ignoradas. - -## Palabras finales - -Gracias por leer hasta aquí. -Si tienes algún comentario/sugerencia, por favor publica en el -[tema en los foros](https://forum.minetest.net/viewtopic.php?id=4929). Para -reportar errores, usa el [rastreador](https://github.com/minetest-mods/intllib/issues/new) -en Github. - -¡Que se hagan las traducciones! :P - -\-- - -Suyo, -Kaeza diff --git a/README.md b/README.md index 185ca1d..de7cfa3 100644 --- a/README.md +++ b/README.md @@ -2,142 +2,42 @@ # Internationalization Lib for Minetest By Diego Martínez (kaeza). -Released as WTFPL. +Released under Unlicense. See `LICENSE.md` for details. This mod is an attempt at providing internationalization support for mods (something Minetest currently lacks). -## How to use - -### For end users - -To use this mod, just [install it](http://wiki.minetest.net/Installing_Mods) -and enable it in the GUI. - -The mod tries to detect the user's language, but since there's currently no -portable way to do this, it tries several alternatives, and uses the first one -found: - - * `language` setting in `minetest.conf`. - * If that's not set, it uses the `LANG` environment variable (this is - always set on Unix-like OSes). - * If all else fails, uses `en` (which basically means untranslated strings). - -In any case, the end result should be the -[ISO 639-1 Language Code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) -of the desired language. Also note that (currently) only up to the first two -characters are used, so for example, the settings `de_DE.UTF-8`, `de_DE`, -and `de` are all equal. - -Some common codes are `es` for Spanish, `pt` for Portuguese, `fr` for French, -`it` for Italian, `de` for German. - -### For mod developers - -In order to enable it for your mod, copy the following code snippet and paste -it at the beginning of your source file(s): - -```lua --- Boilerplate to support localized strings if intllib mod is installed. -local S -if minetest.get_modpath("intllib") then - S = intllib.Getter() -else - -- If you don't use insertions (@1, @2, etc) you can use this: - S = function(s) return s end - - -- If you use insertions, but not insertion escapes this will work: - S = function(s,a,...)a={a,...}return s:gsub("@(%d+)",function(n)return a[tonumber(n)]end)end - - -- Use this if you require full functionality - S = function(s,a,...)if a==nil then return s end a={a,...}return s:gsub("(@?)@(%(?)(%d+)(%)?)",function(e,o,n,c)if e==""then return a[tonumber(n)]..(o==""and c or"")else return"@"..o..n..c end end) end -end -``` - -You will also need to optionally depend on intllib, to do so add `intllib?` to -an empty line in your `depends.txt`. Also note that if intllib is not installed, -the `S` function is defined so it returns the string unchanged. This is done -so you don't have to sprinkle tons of `if`s (or similar constructs) to check -if the lib is actually installed. - -Next, for each translatable string in your sources, use the `S` function -(defined in the snippet) to return the translated string. For example: - -```lua -minetest.register_node("mymod:mynode", { - -- Simple string: - description = S("My Fabulous Node"), - -- String with insertions: - description = S("@1 Car", "Blue"), - -- ... -}) -``` - -Then, you create a `locale` directory inside your mod directory, and create -a "template" file (by convention, named `template.txt`) with all the -translatable strings (see *Locale file format* below). Translators will -translate the strings in this file to add languages to your mod. - -### For translators - -To translate an intllib-supporting mod to your desired language, copy the -`locale/template.txt` file to `locale/LANGUAGE.txt` (where `LANGUAGE` is the -[ISO 639-1 Language Code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) -of your language. - -Open up the new file in your favorite editor, and translate each line putting -the translated text after the equals sign. - -See *Locale file format* below for more information about the file format. - -## Locale file format - -Here's an example for a Spanish locale file (`es.txt`): - -```cfg -# A comment. -# Another comment. -This line is ignored since it has no equals sign. -Hello, World! = Hola, Mundo! -String with\nnewlines = Cadena con\nsaltos de linea -String with an \= equals sign = Cadena con un signo de \= igualdad -``` - -Locale (or translation) files are plain text files consisting of lines of the -form `source text = translated text`. The file must reside in the mod's `locale` -subdirectory, and must be named after the two-letter -[ISO 639-1 Language Code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) -of the language you want to support. - -The translation files should use the UTF-8 encoding. - -Lines beginning with a pound sign are comments and are effectively ignored -by the reader. Note that comments only span until the end of the line; -there's no support for multiline comments. Lines without an equals sign are -also ignored. - -Characters that are considered "special" can be "escaped" so they are taken -literally. There are also several escape sequences that can be used: - - * Any of `#`, `=` can be escaped to take them literally. The `\#` - sequence is useful if your source text begins with `#`. - * The common escape sequences `\n` and `\t`, meaning newline and - horizontal tab respectively. - * The special `\s` escape sequence represents the space character. It - is mainly useful to add leading or trailing spaces to source or - translated texts, as these spaces would be removed otherwise. - -## Final words - -Thanks for reading up to this point. Should you have any comments/suggestions, please post them in the -[forum topic](https://forum.minetest.net/viewtopic.php?id=4929). For bug -reports, use the [bug tracker](https://github.com/minetest-mods/intllib/issues/new) +[forum topic][topic]. For bug reports, use the [bug tracker][bugtracker] on Github. -Let there be translated texts! :P +## How to use -\-- +If you are a regular player looking for translated texts, just +[install][installing_mods] this mod like any other one, then enable it +in the GUI. -Yours Truly, -Kaeza +The mod tries to detect your language, but since there's currently no +portable way to do this, it tries several alternatives: + +* `language` setting in `minetest.conf`. +* `LANGUAGE` environment variable. +* `LANG` environment variable. +* If all else fails, uses `en`. + +In any case, the end result should be the [ISO 639-1 Language Code][ISO639-1] +of the desired language. + +### Mod developers + +If you are a mod developer looking to add internationalization support to +your mod, see `doc/developer.md`. + +### Translators + +If you are a translator, see `doc/translator.md`. + +[topic]: https://forum.minetest.net/viewtopic.php?id=4929 +[bugtracker]: https://github.com/minetest-mods/intllib/issues +[installing_mods]: https://wiki.minetest.net/Installing_mods +[ISO639-1]: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes diff --git a/doc/developer.md b/doc/developer.md new file mode 100644 index 0000000..9408b1a --- /dev/null +++ b/doc/developer.md @@ -0,0 +1,96 @@ + +# Intllib developer documentation + +In order to enable it for your mod, copy some boilerplate into your +source file(s). What you need depends on what you want to support. + +There are now two main interfaces: one using the old plain text file method, +and one using the new support for [gettext][gettext] message catalogs (`.mo`). +Read below for details on each one. + +You will also need to optionally depend on intllib, to do so add `intllib?` +to an empty line in your `depends.txt`. Also note that if intllib is not +installed, the getter functions are defined so they return the string +unchanged. This is done so you don't have to sprinkle tons of `if`s (or +similar constructs) to check if the lib is actually installed. + +## New interface + +You will need to copy the file `lib/intllib.lua` into the root directory of +your mod, then include this boilerplate code in files needing localization: + + -- Load support for intllib. + local MP = minetest.get_modpath(minetest.get_current_modname()) + local S, NS = dofile(MP.."/intllib.lua") + +Use the usual gettext tools (`xgettext`, `msgfmt`, etc.), to generate your +catalog files in a directory named `locale`. + +Note: Drop the `.mo` file directly as `locale/$lang.mo`. **Not** in +`locale/$lang/LC_MESSAGES/$domain.mo`! + +You should also provide the source `.po` and `.pot` files. + +### Basic workflow + +This is the basic workflow for working with [gettext][gettext] + +Each time you have new strings to be translated, you should do the following: + + cd /path/to/mod + /path/to/intllib/tools/xgettext.sh file1.lua file2.lua ... + +The script will create a directory named `locale` if it doesn't exist yet, +and will generate the file `template.pot`. If you already have translations, +the script will proceed to update all of them with the new strings. + +The script passes some options to the real `xgettext` that should be enough +for most cases. You may specify other options if desired: + + xgettext.sh -o file.pot --keyword=blargh:4,5 a.lua b.lua ... + +NOTE: There's also a Windows batch file `xgettext.bat` for Windows users, +but you will need to install the gettext command line tools separately. See +the top of the file for configuration. + +Once a translator submits an updated translation, you should run the `msgfmt` +tool: + + msgfmt locale/ll_CC.po -o locale/ll_CC.mo + +## Old interface + +You will need this boilerplate code: + + -- Boilerplate to support localized strings if intllib mod is installed. + local S + if minetest.get_modpath("intllib") then + S = intllib.Getter() + else + -- If you don't use insertions (@1, @2, etc) you can use this: + S = function(s) return s end + + -- If you use insertions, but not insertion escapes this will work: + S = function(s,a,...)a={a,...}return s:gsub("@(%d+)",function(n)return a[tonumber(n)]end)end + + -- Use this if you require full functionality + S = function(s,a,...)if a==nil then return s end a={a,...}return s:gsub("(@?)@(%(?)(%d+)(%)?)",function(e,o,n,c)if e==""then return a[tonumber(n)]..(o==""and c or"")else return"@"..o..n..c end end) end + end + +Next, for each translatable string in your sources, use the `S` function +(defined in the snippet) to return the translated string. For example: + + minetest.register_node("mymod:mynode", { + -- Simple string: + description = S("My Fabulous Node"), + -- String with insertions: + description = S("@1 Car", "Blue"), + -- ... + }) + +Then, you create a `locale` directory inside your mod directory, and create +a "template" file (by convention, named `template.txt`) with all the +translatable strings (see *Locale file format* below). Translators will +translate the strings in this file to add languages to your mod. + +[gettext]: https://www.gnu.org/software/gettext/ diff --git a/doc/translator.md b/doc/translator.md new file mode 100644 index 0000000..3c278e8 --- /dev/null +++ b/doc/translator.md @@ -0,0 +1,20 @@ + +# Intllib translator documentation + +#### New interface + +Use your favorite tools to edit the `.po` files. + +#### Old interface + +To translate an intllib-supporting mod to your desired language, copy the +`locale/template.txt` file to `locale/LANGUAGE.txt` (where `LANGUAGE` is the +[ISO 639-1 Language Code][ISO639-1] of your language. + +Open up the new file in your favorite editor, and translate each line putting +the translated text after the equals sign. + +See `localefile.md` for more information about the file format. + +[gettext]: https://www.gnu.org/software/gettext/ +[ISO639-1]: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes diff --git a/gettext.lua b/gettext.lua new file mode 100644 index 0000000..6f3a1cb --- /dev/null +++ b/gettext.lua @@ -0,0 +1,288 @@ + +local strfind, strsub, strrep = string.find, string.sub, string.rep +local strmatch, strgsub = string.match, string.gsub +local floor = math.floor + +local function split(str, sep) + local pos, endp = 1, #str+1 + return function() + if (not pos) or pos > endp then return end + local s, e = strfind(str, sep, pos, true) + local part = strsub(str, pos, s and s-1) + pos = e and e + 1 + return part + end +end + +local function trim(str) + return strmatch(str, "^%s*(.-)%s*$") +end + +local escapes = { n="\n", r="\r", t="\t" } + +local function unescape(str) + return (strgsub(str, "(\\+)([nrt]?)", function(bs, c) + local bsl = #bs + local realbs = strrep("\\", bsl/2) + if bsl%2 == 1 then + c = escapes[c] + end + return realbs..c + end)) +end + +local function parse_po(str) + local state, msgid, msgid_plural, msgstrind + local texts = { } + local lineno = 0 + local function perror(msg) + return error(msg.." at line "..lineno) + end + for line in split(str, "\n") do repeat + lineno = lineno + 1 + line = trim(line) + + if line == "" or strmatch(line, "^#") then + state, msgid, msgid_plural = nil, nil, nil + break -- continue + end + + local mid = strmatch(line, "^%s*msgid%s*\"(.*)\"%s*$") + if mid then + if state == "id" then + return perror("unexpected msgid") + end + state, msgid = "id", unescape(mid) + break -- continue + end + + mid = strmatch(line, "^%s*msgid_plural%s*\"(.*)\"%s*$") + if mid then + if state ~= "id" then + return perror("unexpected msgid_plural") + end + state, msgid_plural = "idp", unescape(mid) + break -- continue + end + + local ind, mstr = strmatch(line, + "^%s*msgstr([0-9%[%]]*)%s*\"(.*)\"%s*$") + if ind then + if not msgid then + return perror("missing msgid") + elseif ind == "" then + msgstrind = 0 + elseif strmatch(ind, "%[[0-9]+%]") then + msgstrind = tonumber(strsub(ind, 2, -2)) + else + return perror("malformed msgstr") + end + texts[msgid] = texts[msgid] or { } + if msgid_plural then + texts[msgid_plural] = texts[msgid] + end + texts[msgid][msgstrind] = unescape(mstr) + state = "str" + break -- continue + end + + mstr = strmatch(line, "^%s*\"(.*)\"%s*$") + if mstr then + if state == "id" then + msgid = msgid..unescape(mstr) + break -- continue + elseif state == "idp" then + msgid_plural = msgid_plural..unescape(mstr) + break -- continue + elseif state == "str" then + local text = texts[msgid][msgstrind] + texts[msgid][msgstrind] = text..unescape(mstr) + break -- continue + end + end + + return perror("malformed line") + + until true end -- end for + + return texts +end + +local M = { } + +local domains = { } +local dgettext_cache = { } +local dngettext_cache = { } +local langs + +local function detect_languages() + if langs then return langs end + + langs = { } + + local function addlang(l) + local sep + langs[#langs+1] = l + sep = strfind(l, ".", 1, true) + if sep then + l = strsub(l, 1, sep-1) + langs[#langs+1] = l + end + sep = strfind(l, "_", 1, true) + if sep then + langs[#langs+1] = strsub(l, 1, sep-1) + end + end + + local v + + v = rawget(_G, "minetest") and minetest.setting_get("language") + if v and v~="" then + addlang(v) + end + + v = os.getenv("LANGUAGE") + if v then + for item in split(v, ":") do + addlang(item) + end + end + + v = os.getenv("LANG") + if v then + addlang(v) + end + + return langs +end + +local function warn(msg) + if rawget(_G, "minetest") then + minetest.log("warning", msg) + else + io.stderr:write("WARNING: ", msg, "\n") + end +end + +-- hax! +-- This function converts a C expression to an equivalent Lua expression. +-- It handles enough stuff to parse the `Plural-Forms` header correctly. +-- Note that it assumes the C expression is valid to begin with. +local function compile_plural_forms(str) + local plural = strmatch(str, "plural=([^;]+);?$") + local function replace_ternary(str) + local c, t, f = strmatch(str, "^(.-)%?(.-):(.*)") + if c then + return ("__if(" + ..replace_ternary(c) + ..","..replace_ternary(t) + ..","..replace_ternary(f) + ..")") + end + return str + end + plural = replace_ternary(plural) + plural = strgsub(plural, "&&", " and ") + plural = strgsub(plural, "||", " or ") + plural = strgsub(plural, "!=", "~=") + plural = strgsub(plural, "!", " not ") + local f, err = loadstring([[ + local function __if(c, t, f) + if c and c~=0 then return t else return f end + end + local function __f(n) + return (]]..plural..[[) + end + return (__f(...)) + ]]) + if not f then return nil, err end + local env = { } + env._ENV, env._G = env, env + setfenv(f, env) + return function(n) + local v = f(n) + if type(v) == "boolean" then + -- Handle things like a plain `n != 1` + v = v and 1 or 0 + end + return v + end +end + +local function parse_headers(str) + local headers = { } + for line in split(str, "\n") do + local k, v = strmatch(line, "^([^:]+):%s*(.*)") + if k then + headers[k] = v + end + end + return headers +end + +local function load_catalog(filename) + local f, data, err + + local function bail(msg) + warn(msg..(err and ": ")..(err or "")) + return nil + end + + f, err = io.open(filename, "rb") + if not f then + return --bail("failed to open catalog") + end + + data, err = f:read("*a") + + f:close() + + if not data then + return bail("failed to read catalog") + end + + data, err = parse_po(data) + if not data then + return bail("failed to parse catalog") + end + + err = nil + local hdrs = data[""] + if not (hdrs and hdrs[0]) then + print(dump(hdrs)) + return bail("catalog has no headers") + end + + hdrs = parse_headers(hdrs[0]) + + local pf = hdrs["Plural-Forms"] + if not pf then + return bail("failed to load catalog:" + .." catalog has no Plural-Forms header") + end + + data.plural_index, err = compile_plural_forms(pf) + if not data.plural_index then + return bail("failed to compile plural forms") + end + + --warn("loaded: "..filename) + + return data +end + +function M.load_catalogs(path) + detect_languages() + + local cats = { } + for _, lang in ipairs(langs) do + local cat = load_catalog(path.."/"..lang..".po") + if cat then + cats[#cats+1] = cat + end + end + + return cats +end + +return M diff --git a/init.lua b/init.lua index 79d4c31..ee6d1a9 100644 --- a/init.lua +++ b/init.lua @@ -21,6 +21,22 @@ if not (LANG and (LANG ~= "")) then LANG = "en" end local INS_CHAR = intllib.INSERTION_CHAR local insertion_pattern = "("..INS_CHAR.."?)"..INS_CHAR.."(%(?)(%d+)(%)?)" +local function do_replacements(str, ...) + local args = {...} + -- Outer parens discard extra return values + return (str:gsub(insertion_pattern, function(escape, open, num, close) + if escape == "" then + local replacement = tostring(args[tonumber(num)]) + if open == "" then + replacement = replacement..close + end + return replacement + else + return INS_CHAR..open..num..close + end + end)) +end + local function make_getter(msgstrs) return function(s, ...) local str @@ -33,24 +49,12 @@ local function make_getter(msgstrs) if select("#", ...) == 0 then return str end - local args = {...} - str = str:gsub(insertion_pattern, function(escape, open, num, close) - if escape == "" then - local replacement = tostring(args[tonumber(num)]) - if open == "" then - replacement = replacement..close - end - return replacement - else - return INS_CHAR..open..num..close - end - end) - return str + return do_replacements(str, ...) end end -function intllib.Getter(modname) +local function Getter(modname) modname = modname or minetest.get_current_modname() if not intllib.getters[modname] then local msgstr = intllib.get_strings(modname) @@ -60,6 +64,64 @@ function intllib.Getter(modname) end +function intllib.Getter(modname) + minetest.log("deprecated", "intllib.Getter is deprecated." + .."Please use intllib.make_gettext_pair instead.") + return Getter(modname) +end + + +local gettext = dofile(minetest.get_modpath("intllib").."/gettext.lua") + + +local function catgettext(catalogs, msgid) + for _, cat in ipairs(catalogs) do + local msgstr = cat and cat[msgid] + if msgstr then + return msgstr[0] + end + end +end + +local function catngettext(catalogs, msgid, msgid_plural, n) + n = math.floor(n) + for i, cat in ipairs(catalogs) do + print(i, dump(cat)) + local msgstr = cat and cat[msgid] + if msgstr then + local index = cat.plural_index(n) + print("catngettext:", index, msgstr[index]) + return msgstr[index] + end + end + return n==1 and msgid or msgid_plural +end + + +local gettext_getters = { } +function intllib.make_gettext_pair(modname) + modname = modname or minetest.get_current_modname() + if gettext_getters[modname] then + return unpack(gettext_getters[modname]) + end + local localedir = minetest.get_modpath(modname).."/locale" + local catalogs = gettext.load_catalogs(localedir) + local getter = Getter(modname) + local function gettext(msgid, ...) + local msgstr = (catgettext(catalogs, msgid) + or getter(msgid)) + return do_replacements(msgstr, ...) + end + local function ngettext(msgid, msgid_plural, n, ...) + local msgstr = (catngettext(catalogs, msgid, msgid_plural, n) + or getter(msgid)) + return do_replacements(msgstr, ...) + end + gettext_getters[modname] = { gettext, ngettext } + return gettext, ngettext +end + + local function get_locales(code) local ll, cc = code:match("^(..)_(..)") if ll then diff --git a/lib/intllib.lua b/lib/intllib.lua new file mode 100644 index 0000000..6669d72 --- /dev/null +++ b/lib/intllib.lua @@ -0,0 +1,45 @@ + +-- Fallback functions for when `intllib` is not installed. +-- Code released under Unlicense . + +-- Get the latest version of this file at: +-- https://raw.githubusercontent.com/minetest-mods/intllib/master/lib/intllib.lua + +local function format(str, ...) + local args = { ... } + local function repl(escape, open, num, close) + if escape == "" then + local replacement = tostring(args[tonumber(num)]) + if open == "" then + replacement = replacement..close + end + return replacement + else + return "@"..open..num..close + end + end + return (str:gsub("(@?)@(%(?)(%d+)(%)?)", repl)) +end + +local gettext, ngettext +if minetest.get_modpath("intllib") then + if intllib.make_gettext_pair then + -- New method using gettext. + gettext, ngettext = intllib.make_gettext_pair() + else + -- Old method using text files. + gettext = intllib.Getter() + end +end + +-- Fill in missing functions. + +gettext = gettext or function(msgid, ...) + return format(msgid, ...) +end + +ngettext = ngettext or function(msgid, msgid_plural, n, ...) + return format(n==1 and msgid or msgid_plural, ...) +end + +return gettext, ngettext diff --git a/tools/xgettext.bat b/tools/xgettext.bat new file mode 100644 index 0000000..18403db --- /dev/null +++ b/tools/xgettext.bat @@ -0,0 +1,33 @@ +@echo off +setlocal + +set me=%~n0 + +rem # Uncomment the following line if gettext is not in your PATH. +rem # Value must be absolute and end in a backslash. +rem set gtprefix=C:\path\to\gettext\bin\ + +if "%1" == "" ( + echo Usage: %me% FILE... 1>&2 + exit 1 +) + +set xgettext=%gtprefix%xgettext.exe +set msgmerge=%gtprefix%msgmerge.exe + +md locale > nul 2>&1 +echo Generating template... 1>&2 +echo %xgettext% --from-code=UTF-8 -kS -kNS:1,2 -k_ -o locale/template.pot %* +%xgettext% --from-code=UTF-8 -kS -kNS:1,2 -k_ -o locale/template.pot %* +if %ERRORLEVEL% neq 0 goto done + +cd locale + +for %%f in (*.po) do ( + echo Updating %%f... 1>&2 + %msgmerge% --update %%f template.pot +) + +echo DONE! 1>&2 + +:done diff --git a/tools/xgettext.sh b/tools/xgettext.sh new file mode 100755 index 0000000..6de353c --- /dev/null +++ b/tools/xgettext.sh @@ -0,0 +1,23 @@ +#! /bin/bash + +me=$(basename "${BASH_SOURCE[0]}"); + +if [[ $# -lt 1 ]]; then + echo "Usage: $me FILE..." >&2; + exit 1; +fi + +mkdir -p locale; +echo "Generating template..." >&2; +xgettext --from-code=UTF-8 -kS -kNS:1,2 -k_ \ + -o locale/template.pot "$@" \ + || exit; + +cd locale; + +for file in *.po; do + echo "Updating $file..." >&2; + msgmerge --update "$file" template.pot; +done + +echo "DONE!" >&2;