9

I'd like to get i18n working in my OCaml code (translating text messages into the user's preferred language). Most translatable strings are the first non-labelled argument to one of a small group of functions. e.g.

  • Text output: print "Feed '%s':" new_feed
  • Errors: raise_safe "Local feed file '%s' does not exist" path
  • Logging: log_warning ~ex "Error checking signature for %s" feed

In the GUI code, strings in ~label arguments should be extracted too.

Is there some way I can extract these strings automatically? To make things more interesting, I use some syntax extensions (e.g. Lwt) too.

I see there is an ocaml-gettext package, but:

  • It only seems to support a fixed set of functions (f_, s_, etc).
  • OPAM says nothing uses it.
  • It failed to install (had to unset $NAME first), then failed to uninstall too, which also suggests it's not heavily used.

What do people actually use/recommend?

Thomas Leonard
  • 7,068
  • 2
  • 36
  • 40
  • Just to mention that Gasoline mentioned in my answer maturated to [v0.1](https://github.com/michipili/gasoline/releases/tag/v0.1). its diagnostic facility is [described in the wiki](https://github.com/michipili/gasoline/wiki/DiagnosticFacility) and [illustrated by examples](https://github.com/michipili/gasoline/tree/master/example). – Michaël Le Barbier Oct 25 '14 at 10:56
  • Thanks. This particular program will go in various Linux distributions, so it can't depend on libraries that aren't widely packaged (even if they are in OPAM), but I'll bear Gasoline in mind for other projects. – Thomas Leonard Oct 25 '14 at 11:42

1 Answers1

2

This is an issue I want to address in my Gasoline project, an application toolkit.

An answer from the future

In the examples, there is an over-engineered wordcount program, which serves as use-case for the library. The function Component.Message.cannot_open uses its format string as a key in an internationalised messages database:

let cannot_open name reason =
  send sink Error "${FILENAME:s}: cannot open (${REASON:s})"
  [ "FILENAME", make String name;
    "REASON", make String reason;
  ]

The sink is a global state of the software component it belongs to, it is bound to a database of internationalised messages where a translation is looked up and used to prepare the actual message.

Was suggested by the example, this facility supports formatting indications, much like printf does, we may write ${FILENAME:+20s} for instance.

Corrections for the present

For now, there is no actual database lookup implemented. The send function is defined in the functor Generic_message.Sink.Make parametrised by a Database module. The current implementation of wordcount uses the following implementation of the database:

module Database =
struct
  type t = unit
  type connection_token = unit
  let opendb () = ()
  let find () id = id
  let close () = ()
end

It is however possible to replace this definition with an actual database lookup — which is in the backlog.

Organisation of messages

In my opinion, message internationalisation should be accomplished by software components — the largest organisational unity below the whole application — and libraries or similar facilities should use standard constant sum types to report errors.

Michaël Le Barbier
  • 6,103
  • 5
  • 28
  • 57