Archive: Language strings: Include them or add them as resources?


Language strings: Include them or add them as resources?
Recently I decided to add support for multiple languages to some NSIS tool that I have been developing, after I was asked if the end user could use some sort of resource editing tool to modify my program in such a way that localization in their language would be achieved.
There is of course the well documented way of adding for example multiple languages for some text to be displayed:

...
LangString Message ${LANG_ENGLISH} "String in English"
LangString Message ${LANG_FRENCH} "Texte en français"
...
MessageBox MB_OK|MB_ICONINFORMATION "$(Message)"
...
However if someone wanted to add some new language they would have to obtain the source code and recompile the program after adding the string in their language.

So the question that I was faced with was how can I add support for another language if I do not want to have the default language(s) present, without changing the source code?

I don't like the idea of having external language files and since I was playing around with Message Table resources in DLLs recently I thought that I could give it a shot and try to add my own messages to my program in the form of Message Table resources. In this way the end user can choose to use something like Resource Hacker to change the language of the resource and modify all the strings on that table, thus achieving localization for their system. Furthermore I can use two tables inside the message table resource, one using {LANG_NEUTRAL,SUBLANG_NEUTRAL} and the other using some other language (for example english). The end user can change the table that contains the language (1033 for example) but leave the neutral language table unaltered, so that if the program is executed on a system that does not contain the end user's language, the neutral language would be picked instead.

To achieve the above I used a tool called XN Resource Editor to add a new message table resource to an already compiled version of my program. After saving the modified program I used Resource Hacker to edit the message table(s) and exported them as RES files. These files are binary and not easy to edit, but once they are imported to an EXE file they can be easily manipulated using Resource Hacker.

So now I had the message tables in an RES format and I wanted to add them to my program upon compiling it. I used the following batch script during compilation (header.cmd):
echo Adding Message Table resources
"%ProgramFiles%\Resource Hacker\ResHacker.exe" -add "%TEMP%\exehead.dat", "%TEMP%\exehead.dat", "%~dps0Lang\Lang_Default.res" , MESSAGETABLE,,
"%ProgramFiles%\Resource Hacker\ResHacker.exe" -add "%TEMP%\exehead.dat", "%TEMP%\exehead.dat", "%~dps0Lang\Lang_EN.res" , MESSAGETABLE,,
echo.
echo Compressing the header with UPX
echo.
upx --best --compress-icons=0 "%TEMP%\exehead.dat"
echo.
This batch file was called from my NSIS script during compilation time:
!packhdr "$%TEMP%\exehead.dat" 'header.cmd'
The resulting program contained the message table(s) and it was easy to generate a macro that would pick the strings from those tables depending on the system language:
!macro GetMessage MESSAGE_ID VAR
; Get the installer's name on $R0
System::Call 'kernel32::GetModuleFileNameA(i 0, t .R0, i 1024) i r1'

; Get the handle of the installer and place it on $R1
System::Call 'kernel32::GetModuleHandleA(t R0)i .R1'

; Get the system language
System::Call 'kernel32::GetSystemDefaultLangID(i v)i .R7'

; Get the message out
StrCpy $0 ${FORMAT_MESSAGE_FROM_HMODULE}
IntOp $0 $0 + ${FORMAT_MESSAGE_ALLOCATE_BUFFER}
IntOp $0 $0 + ${FORMAT_MESSAGE_MAX_WIDTH_MASK}
StrCpy $1 ${MESSAGE_ID}
StrCpy $2 $R7
StrCpy $3 0
System::Call 'kernel32::FormatMessageW(i r0, i R1, i r1, i r2, *w .R3, i r3, i n) i .R4'
StrCpy ${VAR} $R3
!macroend
Using the macro is also straight forward:
Var "MessageText"
...
!insertmacro ReadStringFromInstaller 2 $MessageText
MessageBox MB_OK|MB_ICONINFORMATION "$MessageText"
...

Everything looked sweet, until I tried to emulate the end user's behavior ... I tried to edit the strings on the message tables using resource Hacker on the final program and failed miserably: the program fails to run after any string has been tampered with!

I managed to get around this problem by adding
CRCCheck off
to my NSIS script. Not ideal but at least it works.

Although I am quite happy with the result, I am not sure if this is the best approach to the problem described at the beginning of this post. I am certainly not happy having to disable the CRC check.

I would appreciate any comments/suggestions/thoughts.

:D

CF

Very nice :)
Perhaps you could put a Wiki page up for this.

-Stu


Added a WIKI page

There is also an error on my first post, the macro is called GetMessage and not ReadStringFromInstaller. So the code should look like this:

Var "MessageText"
...
!insertmacro GetMessage2 $MessageText
MessageBox MB_OK|MB_ICONINFORMATION "$MessageText"
...

I also forgot to include the definitions for the FormatMessage API call:

!define FORMAT_MESSAGE_ALLOCATE_BUFFER 0x00000100
!define FORMAT_MESSAGE_FROM_HMODULE 0x00000800
!define FORMAT_MESSAGE_MAX_WIDTH_MASK 0x000000FF

CF

Hmmm ... No need to get the filename of the installer since we can just get a handle and use it.
Here is the final code:


!define FORMAT_MESSAGE_ALLOCATE_BUFFER 0x00000100
!define FORMAT_MESSAGE_FROM_HMODULE 0x00000800
!define FORMAT_MESSAGE_MAX_WIDTH_MASK 0x000000FF

!macro GetMessage MESSAGE_ID VAR
; Get the handle of the installer and place it on $R1
System::Call 'kernel32::GetModuleHandleA(i n)i .R1'

; Get the system language
System::Call 'kernel32::GetSystemDefaultLangID(i v)i .R7'

; Get the message out
StrCpy $0 ${FORMAT_MESSAGE_FROM_HMODULE}
IntOp $0 $0 + ${FORMAT_MESSAGE_ALLOCATE_BUFFER}
IntOp $0 $0 + ${FORMAT_MESSAGE_MAX_WIDTH_MASK}
StrCpy $1 ${MESSAGE_ID}
StrCpy $2 $R7
StrCpy $3 0
System::Call 'kernel32::FormatMessageW(i r0, i R1, i r1, i r2, *w .R3, i r3, i n) i .R4'
StrCpy ${VAR} $R3
System::Free $R1
!macroend


CF

I'm not sure how important it is, but I've read that the System plugin works faster if you use /NOUNLOAD...

-Stu


To be honest I could not see any difference with and without the /NOUNLOAD switch :)

However I noticed that in some cases my installer would crash if I used the macro posted before (regardless of the /NOUNLOAD switch). A workaround was to create a function in its place and call it:

Var "Handle"
Function .onInit
System::Call /NOUNLOAD 'kernel32::GetModuleHandleA()i .s'
Pop $Handle
...
FunctionEnd

...

Function GetMessage
Pop $0
StrCpy $R0 $Handle
System::Call /NOUNLOAD 'kernel32::GetSystemDefaultLangID(i v)i .R7'
StrCpy $1 ${FORMAT_MESSAGE_FROM_HMODULE}
IntOp $1 $1 + ${FORMAT_MESSAGE_ALLOCATE_BUFFER}
IntOp $1 $1 + ${FORMAT_MESSAGE_MAX_WIDTH_MASK}
StrCpy $2 $R7
StrCpy $3 0
System::Call /NOUNLOAD 'kernel32::FormatMessageW(i r1,i R1,i r0,i r2,*w .R3,i r3,i n)i .R4'
Push $R3
FunctionEnd
Calling the function is easy:
Push 13   # message ID in the table
Call GetMessage
Pop $0 # Message Text


CF

[Edit] Right ... I am getting a handle on the EXE itself then I am using the System::Free call ... This was the cause of the unpredicted crashes so ignore it. Better yet, get a handle on the running process at .onInit, place it on some variable and then use the already open handle instead of getting a new handle every time ...
I updated the code of this post to reflect these changes
CF

I played a bit more with this and it seems that both the macro and the function that I posted don't work too well when the banner blugin is in use. I managed to get them both operational though by using the FreeLibrary API call:

!define FORMAT_MESSAGE_ALLOCATE_BUFFER  0x00000100
!define FORMAT_MESSAGE_FROM_HMODULE 0x00000800
!define FORMAT_MESSAGE_MAX_WIDTH_MASK 0x000000FF

!macro GetMessage MESSAGE_ID VAR
System::Call 'kernel32::GetModuleHandleW(i n)i .R1'
System::Call 'kernel32::GetSystemDefaultLangID(i v)i .R7'
StrCpy $0 ${FORMAT_MESSAGE_FROM_HMODULE}
IntOp $0 $0 + ${FORMAT_MESSAGE_ALLOCATE_BUFFER}
IntOp $0 $0 + ${FORMAT_MESSAGE_MAX_WIDTH_MASK}
StrCpy $1 ${MESSAGE_ID}
StrCpy $2 $R7
StrCpy $3 0
System::Call 'kernel32::FormatMessageW(i r0, i R1, i r1, i r2, *w .R3, i r3, i n) i .R4'
StrCpy ${VAR} $R3
System::Call 'kernel32::FreeLibrary(i R1)i.r0'
!macroend

!define FORMAT_MESSAGE_ALLOCATE_BUFFER  0x00000100
!define FORMAT_MESSAGE_FROM_HMODULE 0x00000800
!define FORMAT_MESSAGE_MAX_WIDTH_MASK 0x000000FF

Function GetMessage
Pop $0
System::Call 'kernel32::GetModuleHandleW(i n)i .R1'
System::Call /NOUNLOAD 'kernel32::GetSystemDefaultLangID(i v)i .R7'
StrCpy $1 ${FORMAT_MESSAGE_FROM_HMODULE}
IntOp $1 $1 + ${FORMAT_MESSAGE_ALLOCATE_BUFFER}
IntOp $1 $1 + ${FORMAT_MESSAGE_MAX_WIDTH_MASK}
StrCpy $2 $R7
StrCpy $3 0
System::Call /NOUNLOAD 'kernel32::FormatMessageW(i r1,i R1,i r0,i r2,*w .R3,i r3,i n)i .R4'
System::Call 'kernel32::FreeLibrary(i R1)i.r0'
Push $R3
FunctionEnd


As for the size of the installer, I should mention that for the message tables that I added (57 entries each), there was an increase in terms of size of about 4kb per table, as opposed to 0kb when using a header file.

:)

CF

Just a heads-up on the above: I noticed that using this method for displaying messages fails if it is coupled with the banner plugin. It seems that the banner plugin is also getting a handle to the installer and attaches the banner as a thread to the main module, if I read the code correctly. Even if I try to get a handle using GetModuleHandleExA in XP, using the pin flag (so that FreeLibrary will not destroy the handle), every time there is a change on the banner I get a GPF. In windows 2000 DrWatson was kind enough to inform me that the error is due to an access violation of GetModuleHandleW. I changed the macro/function so that they would call the ANSI version of the above function, but the error remained, and DrWatson reported an access violation on GetModuleHandleW again (?). But is it the banner plugin to blame? I think not ...

Playing a bit more with this, trying to isolate the error, I noticed that the following code does not work:

; FormatMessage Definitions
!define FORMAT_MESSAGE_ALLOCATE_BUFFER 0x00000100
!define FORMAT_MESSAGE_IGNORE_INSERTS 0x00000200
!define FORMAT_MESSAGE_FROM_HMODULE 0x00000800

!macro ReadStringFromMessageTable MESSAGE_ID VAR
StrCpy ${VAR} ""
System::Call /NOUNLOAD 'kernel32::GetModuleHandleA()i.R1'
System::Call /NOUNLOAD 'kernel32::GetSystemDefaultLangID(i v)i.R7'
System::Call /NOUNLOAD 'kernel32::FormatMessageW(i {FORMAT_MESSAGE_FROM_HMODULE}|${FORMAT_MESSAGE_ALLOCATE_BUFFER}|${FORMAT_MESSAGE_IGNORE_INSERTS},i R1,i ${MESSAGE_ID},i R7,*w .R3,i 0,i n)i.R4'
StrCpy ${VAR} "$R3"
System::Call /NOUNLOAD 'kernel32::FreeLibrary(i R1)i.r0'
!macroend

Function .onInit
IfSilent Silent NotSilent
Silent:
!insertmacro ReadStringFromMessageTable 4 $R8
MessageBox MB_OK|MB_ICONSTOP "$R8"
NotSilent:
!insertmacro ReadStringFromMessageTable 5 $R8
MessageBox MB_OK|MB_ICONSTOP "$R8"
Quit
FunctionEnd

as it shows both dialog boxes, while this code works:
!define FORMAT_MESSAGE_ALLOCATE_BUFFER             0x00000100
!define FORMAT_MESSAGE_IGNORE_INSERTS 0x00000200
!define FORMAT_MESSAGE_FROM_HMODULE 0x00000800

Function .onInit
Silent:
System::Call /NOUNLOAD 'kernel32::GetModuleHandleA()i.R1'
System::Call /NOUNLOAD 'kernel32::GetSystemDefaultLangID(i v)i.R7'
System::Call /NOUNLOAD 'kernel32::FormatMessageW(i ${FORMAT_MESSAGE_FROM_HMODULE}|${FORMAT_MESSAGE_ALLOCATE_BUFFER}|${FORMAT_MESSAGE_IGNORE_INSERTS},i R1,i 4,i R7,*w .R8,i 0,i n)i.R4'
System::Call /NOUNLOAD 'kernel32::FreeLibrary(i R1)i.r0' MessageBox MB_OK|MB_ICONSTOP "$R8"
NotSilent:
System::Call /NOUNLOAD 'kernel32::GetModuleHandleA()i.R1'
System::Call /NOUNLOAD 'kernel32::GetSystemDefaultLangID(i v)i.R7'
System::Call /NOUNLOAD 'kernel32::FormatMessageW(i ${FORMAT_MESSAGE_FROM_HMODULE}|${FORMAT_MESSAGE_ALLOCATE_BUFFER}|${FORMAT_MESSAGE_IGNORE_INSERTS},i R1,i 5,i R7,*w .R8,i 0,i n)i.R4'
StrCpy ${VAR} "$R3"
System::Call /NOUNLOAD 'kernel32::FreeLibrary(i R1)i.r0' MessageBox MB_OK|MB_ICONSTOP "$R8"
Quit
FunctionEnd
and it shows only the second dialog box.
I am getting the same effect if I use a function instead of a macro.

Any ideas?

CF

GetModuleHandleA is just a wrapper for GetModuleHandleW. That's why GetModuleHandleW is always called eventually.

The problem seems to be in your code. You're not supposed to call FreeLibrary on handles you get using GetModuleHandle. From MSDN:

The GetModuleHandle function returns a handle to a mapped module without incrementing its reference count. Therefore, use care when passing the handle to the FreeLibrary function, because doing so can cause a DLL module to be unmapped prematurely.

hi all,

First thanks CancerFacefor this hint.

i'm very interesting in that solution.
i have just a question about the way languages are detected.

author define this macro :

!macro GetMessage MESSAGE_ID VAR
System::Call 'kernel32::GetModuleHandleA(i n)i .R1'
System::Call 'kernel32::GetSystemDefaultLangID()i .R2'
System::Call 'kernel32::FormatMessageW(i 2559,i R1,i "${MESSAGE_ID}",i R2,*w .R3, \
i 0,i n) i .R4'
StrCpy ${VAR} $R3
!macroend
he uses GetSystemDefaultLangID to retrieve the lang ID.

i would like to use the $Language variable define thanks to MUI_LANGDLL_DISPLAY.
but FormatMessageW take a LANGID (that can be define with MAKELANGID).

what i have read is that LANGId is an hexadecimal value, $Language is not ? Does nsis provide something to convert from $Language to LANGId ?

guillaume

LANGID is a number. It can represented as hexadecimal or decimal. $LANGUAGE contains a number which is the LANGID for the current language. With the system plug-in, you can use `a` instead of `$LANGUAGE`.


no sure to understand

With the system plug-in, you can use `a` instead of `$LANGUAGE`.
anyway i have tested this
!macro GetMessage MESSAGE_ID VAR LANG_ID
System::Call 'kernel32::GetModuleHandleA(i n)i .R1'
System::Call 'kernel32::FormatMessageW(i 2559,i R1,i "${MESSAGE_ID}",i "${LANG_ID}",*w .R3, \
i 0,i n) i .R4'
StrCpy ${VAR} $R3
!macroend
and in the script
!insertmacro GetMessage 1 $R9 $LANGUAGE
MessageBox MB_OK|MB_ICONINFORMATION "$R9"

!insertmacro GetMessage 2 $R9 $LANGUAGE
MessageBox MB_OK|MB_ICONINFORMATION "$R9"
this work fine, i have selected different language and each time the right string was shown.

Instead of passing $LANGUAGE, you can simply pass `a`.

System::Call `kernel32::FormatMessageW(i2559,iR1,i${MESSAGE_ID},ia ...`

;)

so i had understood but this seems to be "too simple" ...

and it works, thanks

Guillaume.


Uninstaller icon & UI
Thank you for sharing this possibility with us.

My only problem is that after changing the table definition for any language and recompile the setup and after install the application I notice that the uninstall icon is missing and the UI of the uninstaller is not as expected. I'm using MUI.nsh.

Have you notice this behavior on your side after translating the setup, recompiling and install it ?

/Isawen


hi isawen,

I never tried to play with resources table.
so sorry i cannot help you.

guillaume


Hi,
I've used this approach for my app's installer and it works great. However, now I'm working on a Japanese version of my software. Resource Hacker doesn't seem to handle the Japanese chars well (it shows ? instead of the character). I also wonder is the solution given in this thread can handle Unicode?

Suggestions anyone?

Thanks,
Robbert


Addition: I see CancerFace has used FormatMessageW which is the Unicode variant of FormatMessage. So that should works. So how to edit the .res file and put Japanese chars in there?