I18n made easy for OCaml apps.
The translations are described in tsv files. A ppx extension is used to inject the text in the current language in your program.
Ocsigen-i18n supports simple strings to make it usable for any OCaml program. It also has extensions for generating HTML elements with Tyxml or for client-server application programming with Eliom.
Define your translations into a tsv file. The first column is the key to use (i.e. the name of the OCaml variable to use in your code). The next columns are the translation for each language you want to use.
foo This is a simple key. Ceci est une clé toute simple.
a_human a human un humain
bar I am {{x}}. Je suis {{x}}.
baz There {{{c?are||is}}} apple{{{c?s||}}} here! Il y a {{{c?des||une}}} pomme{{{c?s||}}} ici !
bu I am {{x %s}} ({{n %d}}). Je suis {{x %s}} ({{n %d}}).
{{x}}defines a variable labeled~x{{x %d}}defines a variable labeled~x, and the%dformat is used to displayxas a string.{{x?a||b}}defines an optional boolean value?x. Ifxistruethenawill be displayed. If it isfalse(which is the default), thenbis chosen.
To simplify the syntax, Ocsigen-i18n comes with a PPX extension.
For example [%i18n foo] will correspond to the text corresponding to key foo in the current language.
[%i18n foo]
[%i18n bar ~x:[%i18n a_human]]
[%i18n bar ~x:("Jean-Michel ("^string_of_int id^")")]
[%i18n baz]
[%i18n baz ~c:(nb > 1)]
[%i18n bu ~x:"Jean-Michel" ~n:id ]If you are using Tyxml (and/or Eliom) (see below options --tyxml and --eliom),
the default will generate HTML elements:
[%i18n foo]
[%i18n bar ~x:[%i18n a_human]]
[%i18n bar ~x:[ txt "Jean-Michel ("
; txt @@ string_of_int id
; txt ")" ] ]
[%i18n baz]
[%i18n baz ~c:(nb > 1)]
[%i18n bu ~x:"Jean-Michel" ~n:id ]The PPX will generate an HTML fragment as a list of Tyxml elements.
If you want a string
instead of a list of elements, prefix the variable name by S., e.g.
[%i18n S.bar ~s:[%i18n S.a_human]].
A conditional value {{{c?if_true||if_false}}} will generate a function
taking an optional parameter ?(c=false) to define if if_true or if_false
needs to be printed.
Languages does not need to use the same labeled variables. The compiler will generate a function taking all the parameters it can detect when parsing the template.
Variable name used twice refers to the same argument.
Use command ocsigen_i18n to generate OCaml values for each text entry found in a tab-separated values file.
The TSV file contains a key in the first column (which will be used as OCaml function names),
and the corresponding text in different languages in other columns.
usage: ocsigen-i18n [options] [< input] [> output]
--languages Comma-separated languages (e.g. en,fr-fr, or Foo.Fr,Foo.Us if using external types). Must be ordered as in source TSV file.
--default-language Set the default language (if not specified, the first one in --languages will be the default).
--input-file TSV file containing keys and translations. If option is omitted or set to -, read on stdin.
--output-file File TSV file containing keys and translations. If option is omitted or set to -, write on stdout.
--external-type Values passed to --languages option come from a predefined type (do not generate the type nor from/to string functions).
--primary Generated file is secondary and depends on given primary file.
--eliom Generate code for a client-server Eliom app
--tyxml Generate code for a Tyxml-based app (for example a server-side Eliom app)
--header Generate only the file header
-help Display this list of options
--help Display this list of options
Example (with the same tsv file as above):
$ ocsigen-i18n --languages en,fr < example.tsv
type t = En|Fr
exception Unknown_language of string
let string_of_language = function
| En -> "en" | Fr -> "fr"
let language_of_string = function
| "en" -> En | "fr" -> Fr| s -> raise (Unknown_language s)
let guess_language_of_string s =
try language_of_string s
with Unknown_language _ as e ->
try language_of_string (String.sub s 0 (String.index s '-'))
with Not_found ->
raise e
let languages = [En;Fr]
let default_language = En
(* We use a reference to store the language by default.
Customize these functions if needed.
For example use a scoped reference if you are using Eliom
and want the language to depend on a session/tab or session group. *)
let _language_ = default_language
let get_language () = !_language_
let set_language language = _language_ := language
module Tr = struct
let foo ?(lang = get_language ()) () () =
match lang with
| En -> "This is a simple key."
| Fr -> "Ceci est une clé toute simple."
let bar ?(lang = get_language ()) () ~x () =
match lang with
| En -> String.concat "" ["I am ";x;"."]
| Fr -> String.concat "" ["Je suis ";x;"."]
let baz ?(lang = get_language ()) () ?(c=false) () =
match lang with
| En -> String.concat "" ["There ";(if c then "are" else "is");" apple";(if c then "s" else "");" here!"]
| Fr -> String.concat "" ["Il y a ";(if c then "des" else "une");" pomme";(if c then "s" else "");" ici !"]
let bu ?(lang = get_language ()) () ~n ~x () =
match lang with
| En -> String.concat "" ["I am ";(Printf.sprintf "%s" x);" (";(Printf.sprintf "%d" n);")."]
| Fr -> String.concat "" ["Je suis ";(Printf.sprintf "%s" x);" (";(Printf.sprintf "%d" n);")."]
endUse a dune rule to generate the OCaml file from your TSV file:
(rule
(target example_i18n.ml)
(deps example_i18n.tsv)
(action
(run %{bin:ocsigen-i18n} --languages en,fr --input-file %{deps}
--output-file %{target})))For an Eliom application, use --eliom to generate an .eliom file:
(rule
(target example_i18n.eliom)
(deps example_i18n.tsv)
(action
(run %{bin:ocsigen-i18n} --eliom --languages en,fr --input-file %{deps}
--output-file %{target})))ocsigen-i18n-rewriter is the PPX extension that will generate automatically
the calls to these generated functions.
Just add option ocsigen-i18n in the list of PPX preprocessors in your Dune file,
and --default-module DEFAULT as option (where DEFAULT is the name of the module
generated by the ocsigen-i18n command).
Example: (preprocess (pps ocsigen-i18n -- --default-module Example_i18n)) in your dune file.
Or, without Dune, use option -ppx 'ocsigen-i18n-rewriter --default-module Example_i18n'.
This will call the right function turning
[%i18n foo] into DEFAULT.foo (),
[%i18n S.bar ~x:"foo"] into DEFAULT.S.bar ~x:"foo" ()] and so on...
You can use ocsigen-i18n to generate multiple .ml or .eliom files
from different TSV files. One of those will be the "default" one.
To specify a different module from the default one, you can do so using expressions such as these:
[%i18n MyI18n.foo]will becomeMyI18n.foo ()[%i18n MyI18n.S.bar ~x:[%i18n S.foo]]will becomeMyI18n.S.bar ~x:(DEFAULT.S.foo ()) ()
Because the rewriter infers your module from the module-path of your expression,
it needs to make the assumption that a module named S will always be
the one generated by ocsigen-i18n for strings.
Do not name your i18n file s.tsv. It won't be compatible.
Because the ppx already forces you to i18n in the expression,
and because most projects will want to keep sane files with i18n in their names,
it would be likely to end up with expressions like [%i18n I18n_a.entry].
This is redundant and heavy. So, if you like, you can auto-prefix and auto-suffix your module names.
Calling the rewriter with -ppx 'ocsigen-i18n-rewriter --prefix Pr_ --suffix _i18n --default-module DEFAULT'
will prefix and suffix all custom modules (i.e. not the DEFAULT one) with Pr_ and _i18n.
As such, [%i18n Feature.foo] will become Pr_Feature_i18n.foo (),
allowing for clearer and more readable expressions.
The prefix must begin with a capital letter, to make it a legit module name start.
If you use these options, note that your file will need to account for the capital in the middle part.
[%i18n Module.entry] still needs Module to be a module name.
opam install ocsigen-i18n