Friday, April 3, 2009

Configure and install internationalized Haskell application

My tutorial about internationalization of Haskell programs will not be complete without explaining how to configure and install Haskell package with internationalization support. Who tried all steps of previous tutorial could admit, that installation of localized data is very long, complex and boring routine, so there must be a way to simplify it (a Haskell way).

Now it is. From the version 0.1.5 of hgettext package, there is included module, that teaches Cabal to install language files.

So, download new hgettext and create for our hello program real world installer.

Directory structure

Currently we have following files:

Main.hs
The `hello` program itself.
messages.pot
Template file, which contain all strings to be translated. This file
should be included into the distribution to allow other users to
generate translation file for their language.
en.po, de.po
Translations to the English and German
languages. These files should be installed to the `locale` folder and
our program has to be able to find them (has to know where they going
to be installed)

Any other files could be generated from the previous, so they shouldn't be included to the distribution package.

Let's create the directory structure for our project. This is simple project, so directory structure should be simple too. Here it is:

hello\
|
|-po\
| |
| |-messages.pot
| |-en.po
| |-de.po
|
|-src\
|
|-Main.hs

Create install script

In order to create a cabal package, we have to add only two files. The first is hello.cabal:

Name:                   hello
Version: 0.1.3
Cabal-Version: >= 1.6

License: BSD3

Author: James Bond
Maintainer: James.Bond@MI6.bi
Copyright: 2009 James Bond
Category: Hello

Synopsis: Internationalized Hello sample
Build-Type: Simple

Extra-Source-Files: po/*.po po/*.pot

x-gettext-po-files: po/*.po
x-gettext-domain-name: hs-hello

Executable hello
Main-Is: Main.hs
Hs-Source-Dirs: src
Build-Depends: base,hgettext >= 0.1.5, setlocale

This is standard .cabal file, but there we added two more lines:

x-gettext-po-files
Tells cabal where ar PO files to install
x-gettext-domain-name
Sets the domain name, under which files will be installed 

For other details see documentation for hgettext Distribution.Simple.I18N.GetText module.

Note that we also enumerated *.po files in the extra-source-files section to add them to the distribution package.

The second file to create --- Setup.hs:

import Distribution.Simple.I18N.GetText

main = gettextDefaultMain

The gettextDefaultMain function substitutes the defaultMain function, but also adds several install hooks to the cabal package, to handle internationalization stuff.

Update the program code

So our installer knows where to put the *.po files and the domain name for them. Our code should know it too --- to make proper initialization. It is not Haskell way to duplicate same information twice, so let's modify the code to get the initialization parameters directly from the installer:

module Main where

import Text.Printf
import Text.I18N.GetText
import System.Locale.SetLocale
import System.IO.Unsafe

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

main = do
setLocale LC_ALL (Just "")
bindTextDomain __MESSAGE_CATALOG_DOMAIN__ (Just __MESSAGE_CATALOG_DIR__)
textDomain __MESSAGE_CATALOG_DOMAIN__

putStrLn (__ "Please enter your name:")
name <- getLine
printf (__ "Hello, %s, how are you?\n") name

So, the only lines were changed are:

  bindTextDomain __MESSAGE_CATALOG_DOMAIN__ (Just __MESSAGE_CATALOG_DIR__)
textDomain __MESSAGE_CATALOG_DOMAIN__

Nice. __MESSAGE_CATALOG_DOMAIN__ and __MESSAGE_CATALOG_DIR__ are macro definitions, whose hold configured strings from the Cabal.

That's all?

Actually, yes. Now you could configure, build and install newly created package by invoking commands:

runhaskell Setup.hs configure
runhaskell Setup.hs build
runhaskell Setup.hs install

And test it.

Have a nice weekend :)



PS: Complete project tarball you can find here.

4 comments:

  1. Hi!

    Thanks a lot for plumbing this big hole in Haskell's eco-system. :-)

    I asked about that some time ago (haskell-cafe) and received (almost) zero feedback.

    However, having proper gettext-like support in Haskell is essential so one can write i18n apps.

    Unfortunately, atm, I'm too busy with Pythobn/Django :-(


    Sincerely,
    Gour

    ReplyDelete
  2. Thank You for comment,

    It is very nice to see, that my work is useful for others :)

    ReplyDelete
  3. This is great work, but it seems that you're setting the locale at compile time rather than at run time. On Linux and other Unix-based systems, this won't work. The user can change the locale at any time (e.g. by logging out, choosing a new locale, and logging in again) and all installed programs must respect the new locale. That's why gettext determines the locale at run time.

    ReplyDelete
  4. freeourbooks: I think, you missed a point in my thoughts (yes, I explaining not so clear as I want :)). I implemented similar to GNU's gettext internationalization. Look at the my previous post where I wrote the minimal Hello World and tried dynamically change the locale (at the end of the text).

    Anyway, following post shows more sophisticated example, where I change locale of the Gtk application on the fly, without program restarting :).

    ReplyDelete