Tuesday, March 3, 2009

AHK - How to Make a Language Pack

This tutorial should help you understand how to create programs that support the use of language packs. A language pack is usually a module that can be added to a program that will translate that program to a different language. These modules usually contain little more than strings of text that are translated versions of the original text.

First, we need the functions for using a language pack. These are a couple of functions that I've created. Note they require AHK 1.0.48+.
; This is the only function that you need to call in order to 
; retrieve the text for your current language. The styntax is
; as follows:
; Lang(Loc, Label)
; Loc - The area where the string is used.
; Label - The label for the string.
; Note that the Loc and Label should remain the same for every
; language used. These are identifiers that let you insert the
; correct string where you want it.

Lang(Loc, Label) {
Global Lang
Return (IsFunc(F := "Lang_" Lang) && L := %F%(Loc "_" Label))
? Lang_Parse(L)
: "<String """ Loc "_" Label """ not found for """ Lang """>"
}


; This function is used internally in Lang(). It isn't necessary to
; have, but it is somewhat useful. What it does is simply allow you
; to insert variables into the string. For example, if you want to
; have a string that says 'I have %N% apples!', instead of having to
; split it into 'Lang("General","Have") . N . Lang("General","Apples")'
; you can simply use 'Lang("General","HaveApples")'. More information
; on how to insert variables into strings will be available later.

Lang_Parse(Str) {
Global
Local M,M1
While RegExMatch(Str, "[^\\]\$([^\$]+)\$", M)
Str := RegExReplace(Str,"\$" M1 "\$",%M1%)
Return % RegExReplace(Str,"\\\$","$")
}

Now for the next step: creating the "module" functions. First, you need to figure out how you're going to identify the language. Generally the best way to do this is by using the 2-3 letter language code. For example, english is "en", german/deutsch is "de", french is "fr" etc. etc. So the name of the function for english then would be "Lang_en". Here's how I set up my module functions. Note that you can do this more than one way, this is simply what worked for me. The only requirement is that it names variables uniformly and accepts a parameter containing the name of the variable to return.
Lang_en(Var) {
Static
If (!Init) {
Init = 1
General_HaveApples = I have $N$ apples!
}
Return L := %Var%
}
; And the deutsch version (same file)
Lang_de(Var) {
Static
If (!Init) {
Init = 1
General_HaveApples = Ich habe $N$ Äpfel!
}
Return L := %Var%
}

So now you have a basic language pack basically completed. Notice how you can include variables in strings in a similar manner to AHK itself, but instead of using "%VarName%", you use "$VarName$". To put a plain "$" in a string, simply escape it using a backslash ("\").
If you put all of these functions in your script, and have the var %Lang% set to "en", whenever you call 'Lang("General","HaveApples")' it will return "I have %N% apples!". If you change the value of %Lang% to "de", the same function call will return "Ich habe %N% Äpfel!". This even allows for the changing of languages while the script is still running.

The final script could look a little like this:
Lang = en
MsgBox % Lang("General","HaveApples")
Lang = de
MsgBox % Lang("General","HaveApples")
Return

Lang(Loc, Label) {
Global Lang
Return (IsFunc(F := "Lang_" Lang) && L := %F%(Loc "_" Label))
? Lang_Parse(L)
: "<String """ Loc "_" Label """ not found for """ Lang """>"
}
Lang_Parse(Str) {
Global
Local M,M1
While RegExMatch(Str, "[^\\]\$([^\$]+)\$", M)
Str := RegExReplace(Str,"\$" M1 "\$",%M1%)
Return % RegExReplace(Str,"\\\$","$")
}
Lang_en(Var) {
Static
If (!Init) {
Init = 1
General_HaveApples = I have $N$ apples!
}
Return L := %Var%
}
Lang_de(Var) {
Static
If (!Init) {
Init = 1
General_HaveApples = Ich habe $N$ Äpfel!
}
Return L := %Var%
}


A note about the module functions: They have assume-static set, and initiate all of the variable they contain the first time that particular module is called. This is the reason for the "if (!Init) {" and "Init = 1" lines. This isn't necessary, and can be changed along with the Lang_Parse() function in order to make this method compatible with AHK versions older than 1.0.48.