Internationalization of Haskell programs using gettext

From HaskellWiki
Revision as of 12:57, 28 March 2009 by Vasyl P (talk | contribs) (First version of text, copied from http://progandprog.blogspot.com/2009/03/i18n-and-haskell.html)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Most common in the GNU world approach to internationalization (i18n) of software is to use GNU gettext utilities. In this tutorial we will create simple "Hello world" program, with multilingual support.

Prepare program to internationalization

Consider we want to make the following program multilingual (file Main.hs):

module Main where
    
import IO 

main = do
  putStrLn "Please enter your name:"
  name <- getLine
  putStrLn $ "Hello, " ++ name ++ ", how are you?"

First of all, wrap all strings, you want to translate in function __:

module Main where
    
import IO 

__ = id

main = do
  putStrLn (__ "Please enter your name:")
  name <- getLine
  putStrLn $ (__ "Hello, ") ++ name ++ (__ ", how are you?")


We will return to the definition of __ a bit later, now live this function empty (id)

Translate

The next step is to generate POT file (template, which contain all strings to needed to be translated). For Python, C, C++ and Scheme languages there is xgettext utility, but it doesn't support Haskell. On Hackage you could download hgettext library and utility, which process haskell source files in the same way as xgettext C/C++ files:

cabal install --global hgettext

Now run from the directory, where your project is:

hgettext -k __ -o messages.pot Main.hs

Shortly, it gather all strings marked by function __ from the Main.hs and writes everything to messages.pot.

Now look at the resulting pot file:

# Translation file
 
msgid ""
msgstr ""
 
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-01-13 06:05-0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: Main.hs:0
msgid "Please enter your name:"
msgstr ""

#: Main.hs:0
msgid "Hello, "
msgstr ""

#: Main.hs:0
msgid ", how are you?"
msgstr ""


We are interested in the bottom part of this file (started from '#: Main.hs:...'). Here we can see pairs of lines: msgid and msgstr: msgid is the original text from the code, and msgstr is the translaged string. Each language, should have its own translation file. I will create two translations: German and English.

To create a PO file for specific locale we should use msginit utility:

To generate German translations template run:

msginit --input=messages.pot --locale=de.UTF-8

And for English translation run:

msginit --input=messages.pot --locale=en.UTF-8

If we look at the generated files (en.po and de.po), we will see, that English translation is completelly filled, we have only to edit German PO file. So fill it with following strings:

#: Main.hs:0
msgid "Please enter your name:"
msgstr "Wie heißen Sie?"

#: Main.hs:0
msgid "Hello, "
msgstr "Hallo, "

#: Main.hs:0
msgid ", how are you?"
msgstr ", wie geht es Ihnen?"

Install translation files

Now we have to create directories, where these translations should be placed. Originally all translation files are places on /usr/share/locale/ folder, but we are free to select different place. Run:

mkdir -p {de,en}/LC_MESSAGES

It will create two directories de and en, that contain LC_MESSAGES, in the current directory. Now use msgfmt tool, to encode our po files to mo files (binary translation files):

msgfmt --output-file=en/LC_MESSAGES/hello.mo en.po
msgfmt --output-file=de/LC_MESSAGES/hello.mo de.po

Enable internationalization in the code

As the final step we have to modify code, to support the internationalization:

module Main where
    
import IO 
import Text.I18N.GetText
import System.Locale.SetLocale
import System.IO.Unsafe

__ :: String -> String
__ = unsafePerformIO . getText

main = do
  setLocale LC_ALL (Just "") 
  bindTextDomain "hello" "." 
  textDomain "hello" 

  putStrLn (__ "Please enter your name:")
  name <- getLine
  putStrLn $ (__ "Hello, ") ++ name ++ (__ ", how are you?")


Here we added three initialization strings:

setLocale LC_ALL (Just "") 
bindTextDomain "hello" "." 
textDomain "hello"


The first one (you'll have to download setlocale package to enable this function), sets the current locale to default value. Next two functions tells gettext to take "hello.mo" message file from the locale directory (I set it to ".", but in general case, this directory should be passed from the package configuration).

The final step — define function __. It simply call getText from the module Text.I18N.GetText, but its type is String -> IO String so here is used unsafePerformIO to make it call more simpler.

Run the program

Now you can build and try this program in different locales:

user> ghc --make Main.hs
[1 of 1] Compiling Main             ( Main.hs, Main.o )
Linking Main ...

user> LOCALE=en_US.UTF-8 ./Main
Please enter your name:
Bond
Hello, Bond, how are you?

user> LOCALE=de_DE.UTF-8 ./Main
Wie heißen Sie?
Bond
Hallo, Bond, wie geht es Ihnen?

user>

Distribute internationalized cabal package

TBD