- NSIS Discussion
- Language strings: Include them or add them as resources?
Archive: Language strings: Include them or add them as resources?
CancerFace
2nd June 2006 15:31 UTC
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
Afrow UK
2nd June 2006 15:47 UTC
Very nice :)
Perhaps you could put a Wiki page up for this.
-Stu
CancerFace
2nd June 2006 20:17 UTC
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
CancerFace
4th June 2006 22:32 UTC
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
Afrow UK
5th June 2006 09:54 UTC
I'm not sure how important it is, but I've read that the System plugin works faster if you use /NOUNLOAD...
-Stu
CancerFace
5th June 2006 10:55 UTC
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
CancerFace
5th June 2006 15:28 UTC
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
CancerFace
18th June 2006 13:01 UTC
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
kichik
30th June 2006 08:56 UTC
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.
ginglese
22nd February 2008 12:52 UTC
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
kichik
22nd February 2008 14:33 UTC
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`.
ginglese
22nd February 2008 14:46 UTC
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.
kichik
22nd February 2008 14:49 UTC
Instead of passing $LANGUAGE, you can simply pass `a`.
System::Call `kernel32::FormatMessageW(i2559,iR1,i${MESSAGE_ID},ia ...`
ginglese
22nd February 2008 14:55 UTC
;)
so i had understood but this seems to be "too simple" ...
and it works, thanks
Guillaume.
isawen
26th May 2010 15:29 UTC
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
ginglese
31st May 2010 16:03 UTC
hi isawen,
I never tried to play with resources table.
so sorry i cannot help you.
guillaume
robbertdam
31st May 2011 14:33 UTC
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
robbertdam
31st May 2011 14:36 UTC
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?