Multi-Language (again!)

Share your advanced PureBasic knowledge/code with the community.
User avatar
ChrisR
Addict
Addict
Posts: 1124
Joined: Sun Jan 08, 2017 10:27 pm
Location: France

Multi-Language (again!)

Post by ChrisR »

Hi,
I didn't reinvent the wheel, it's based on the excellent GPI's code version that I love!
With some small changes to make it work out of the box without any intervention.
Except for the datasection, of course for your default keys words and values.

At startup, it creates the language file with the Native name if it does not already exist,
otherwise it updates the language file (with possible keywords and values addition) without changing the current translated values.
Then it loads the default language and updates it with the translated values of the language file.
+#CRLF$+ and +#DQUOTE$+ without any space is accepted as an escape character for the values.

You only need to use: XIncludeFile "Multi-Language.pb" and then use the macros: GetInterfaceLang, GetTooltipLang,... You're done.
Here, with an example window for demo, to be removed.

Code: Select all

; Multi-Language.pb based on the excellent GPI's MultiLanguage code that I love!

EnableExplicit

Global LangFolder.s = "Lang"

Structure Lang
  Map LangKey.s()
EndStructure
Global NewMap Lang.Lang()

;- ----- Public  -----
Macro GetLang(Section, Keyname)
  ReplaceCrlfQuote(Lang(UCase(Section))\LangKey(UCase(Keyname)))
EndMacro

Macro GetInterfaceLang(Keyname)
  ReplaceCrlfQuote(Lang("INTERFACE")\LangKey(UCase(Keyname)))
EndMacro

Macro GetTooltipLang(Keyname)
  ReplaceCrlfQuote(Lang("TOOLTIP")\LangKey(UCase(Keyname)))
EndMacro

Macro GetMenuLang(Keyname)
  ReplaceCrlfQuote(Lang("MENU")\LangKey(UCase(Keyname)))
EndMacro

Macro GetToolBarLang(Keyname)
  ReplaceCrlfQuote(Lang("TOOLBAR")\LangKey(UCase(Keyname)))
EndMacro

Macro GetStatusBarLang(Keyname)
  ReplaceCrlfQuote(Lang("STATUSBAR")\LangKey(UCase(Keyname)))
EndMacro

Macro GetMessageLang(Keyname)
  ReplaceCrlfQuote(Lang("MESSAGE")\LangKey(UCase(Keyname)))
EndMacro

Declare.s LocaleInfo(LCType.i = #LOCALE_SNATIVELANGNAME)
Declare.s ReplaceCrlfQuote(String.s)
Declare   LangLoadDefault()
Declare   LangLoad(Filename.s = "")
Declare   LangSaveFile(Filename.s)
Declare   LangSave(Filename.s)

;- ----- Private -----
Procedure.s LocaleInfo(LCType.i = #LOCALE_SNATIVELANGNAME)
  Protected LCData.s, LCDataLen.i
  ; MS: The application should call GetLocaleInfoEx function in preference To GetLocaleInfo If designed To run only on Windows Vista And later. But it works
  LCDataLen = GetLocaleInfo_(#LOCALE_USER_DEFAULT, LCType, @LCData, 0)
  LCData = Space(LCDataLen)
  If GetLocaleInfo_(#LOCALE_USER_DEFAULT, LCType, @LCData, LCDataLen)
    ProcedureReturn LCData
  EndIf
EndProcedure

Procedure.s ReplaceCrlfQuote(String.s)
  Protected NewString.s, NewString2.s, I.i
  For I = 1 To CountString(String, "+#CRLF$+")  + 1
    If I = 1
      NewString2 + StringField(String, I, "+#CRLF$+")
    Else
      NewString2 + #CRLF$ + StringField(String, I, "+#CRLF$+")
    EndIf
  Next
  For I = 1 To CountString(NewString2, "+#DQUOTE$+")  + 1
    If I = 1
      NewString + StringField(NewString2, I, "+#DQUOTE$+")
    Else
      NewString + #DQUOTE$ + StringField(NewString2, I, "+#DQUOTE$+")
    EndIf
  Next
  ProcedureReturn NewString
EndProcedure

Procedure LangLoadDefault()
  Protected Section.s = "COMMON", Keyname.s, Value.s
  ClearMap(Lang())
  Restore DefaultLang:
  Repeat
    Read.s Keyname
    Read.s Value
    Keyname = UCase(Keyname)
    Select Keyname
      Case "", "_END_"
        Break
      Case "_SECTION_"
        Section = UCase(Value)
      Default
        Lang(Section)\LangKey(Keyname) = Value
        ;Debug Section + "\" + Keyname + "=" + Value
    EndSelect
  ForEver
  ProcedureReturn #True
EndProcedure

Procedure LangLoad(Filename.s = "")
  If Filename
    If OpenPreferences(LangFolder + "\" + Filename)       
      ForEach Lang()
        PreferenceGroup(MapKey(Lang()))
        ForEach Lang()\LangKey()
          Lang()\LangKey() = ReadPreferenceString(MapKey(Lang()\LangKey()), Lang()\LangKey())
        Next
      Next
      ClosePreferences()
    Else
      ProcedureReturn #False
    EndIf
  EndIf
  ProcedureReturn #True    
EndProcedure

Procedure LangSaveFile(Filename.s)
  PreferenceGroup("Info")   ;just in case we need this information sometimes
  WritePreferenceString("Program", GetFilePart(ProgramFilename()))
  WritePreferenceString("Version", "1.00")
  ForEach Lang()       
    PreferenceGroup(MapKey(Lang()))
    ForEach Lang()\LangKey()
      If ReadPreferenceString(MapKey(Lang()\LangKey()), "") = ""
        WritePreferenceString(MapKey(Lang()\LangKey()), Lang()\LangKey())
      EndIf
    Next
  Next
EndProcedure

Procedure LangSave(Filename.s)
  If Filename
    If FileSize(LangFolder) = -1 : CreateDirectory(LangFolder) : EndIf
    If FileSize(LangFolder) = -2
      If MapSize(Lang()) = 0 : LangLoadDefault() : EndIf
      If OpenPreferences(LangFolder + "\" + Filename, #PB_Preference_GroupSeparator)
        LangSaveFile(Filename)
      ElseIf CreatePreferences(LangFolder + "\" + Filename, #PB_Preference_GroupSeparator)
        PreferenceComment("Language File")
        LangSaveFile(Filename)
      Else
        ProcedureReturn #False
      EndIf
      ClosePreferences()
      ProcedureReturn #True
    EndIf
  EndIf
  ProcedureReturn #False
EndProcedure


;- ----- Main    -----

; LocaleInfo() is for Windows only, see Keya's topic for Linux or MacOS: https://www.purebasic.fr/english/viewtopic.php?t=66552
Define NativeLangName.s = LocaleInfo()   ; Or use LocaleInfo(#LOCALE_SENGLANGUAGE) For English Name
NativeLangName = UCase(Left(NativeLangName, 1)) + Mid(NativeLangName, 2)

If NativeLangName = "English"
  LangLoadDefault()
Else
  Define LangFileName.s = NativeLangName + ".lang"   ; Or use LangFileName = "French.lang"
  
  ; Create the language file if it does not already exist otherwise update the language file (with possible addition) without changing the current values
  LangSave(LangFileName)
  
  ; Load the default language then update with the language file translated values
  LangLoad(LangFileName)
EndIf

; If you want to let the user choose his language, use:
;LangLoadDefault()
;LangSave(LangName + ".lang")   ; If not English. LangName.s is your variable with the Language to load. By respecting the Windows Native name, if possible.

; Load the default values (English) before saving for a new template or to refresh the language file, if needed.
;LangLoadDefault()
;LangSave("German.lang")

;- Exemple
Enumeration Window
  #MainWindow
EndEnumeration

Enumeration Gadgets
  #Text_Welcome
  #Check_Agree
  #Button_Confirm
EndEnumeration

If OpenWindow(#MainWindow, 0, 0, 220, 150, "Test Multi-Language", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  TextGadget(#Text_Welcome, 20, 20, 180, 20, GetInterfaceLang("Text_Welcome"), #PB_Text_Center)
  CheckBoxGadget(#Check_Agree, 20, 50, 180, 20, GetInterfaceLang("0001"))
  GadgetToolTip(#Check_Agree, GetTooltipLang("0001"))
  ButtonGadget(#Button_Confirm, 20, 80, 180, 50, GetInterfaceLang("ConfirmValidate"), #PB_Button_MultiLine)
  GadgetToolTip(#Button_Confirm, GetTooltipLang("ConfirmValidate"))
  
  Repeat
    Select WaitWindowEvent()
      Case #PB_Event_CloseWindow
        Break
      Case #PB_Event_Gadget
        Select EventGadget()
          Case #Button_Confirm
            If GetGadgetState(#Check_Agree)
              MessageRequester("terms and conditions", GetMessageLang("Msg001"))
              Break
            EndIf
        EndSelect
    EndSelect
  ForEver
EndIf


DataSection
  ; Here the default language is specified (usually in English). It is a list of Section and of Key name with its Value,
  ;
  ; with some special keywords for the Section:
  ;   "_SECTION_" will indicate a new Section in the datasection, the second value is the Section name
  ;   "_END_" will indicate the end of the language list (as there is no fixed number)
  ;
  ; Note: The Section and Key name are case insensitive to make live easier :)
  
  DefaultLang:
  ; =========================================================================
  Data.s "_SECTION_",              "Interface"
  ; =========================================================================
  Data.s "Text_Welcome",           "Hello World!"
  Data.s "0001",                   "I Agree To Terms"
  Data.s "ConfirmValidate",        "Confirm+#CRLF$++#DQUOTE$+Validate+#DQUOTE$+"
  
  ; =========================================================================
  Data.s "_SECTION_",              "Tooltip"
  ; =========================================================================
  Data.s "0001",                   "I agree to terms and conditions"
  Data.s "ConfirmValidate",        "Confirm and validate the information entered"
  
  ; =========================================================================
  Data.s "_SECTION_",              "Menu"
  ; =========================================================================
  Data.s "File",                   "File"
  Data.s "New",                    "New"
  Data.s "Open",                   "Open..."
  Data.s "Save",                   "Save"
  
  ; =========================================================================
  Data.s "_SECTION_",              "ToolBar"
  ; =========================================================================
  
  ; =========================================================================
  Data.s "_SECTION_",              "StatusBar"
  ; =========================================================================
  
  ; =========================================================================
  Data.s "_SECTION_",              "Message"
  ; =========================================================================
  Data.s "Msg001",                 "Thank you for accepting the terms and conditions!"
  
  ; =========================================================================
  Data.s "_END_",                ""
  ; =========================================================================
  
EndDataSection

Edit: Do not create a language file for the default language, English

Edit2: Add +#CRLF$+ and +#DQUOTE$+ escape character
Last edited by ChrisR on Fri Oct 01, 2021 1:08 pm, edited 5 times in total.
AZJIO
Addict
Addict
Posts: 1298
Joined: Sun May 14, 2017 1:48 am

Re: Multi-Language (again!)

Post by AZJIO »

When creating a program, it is important to control the texts of messages. If they are written in the form of abbreviations, it is difficult to control the accuracy of the wording. It is better to write a program with explicit texts, and then convert it into a multilingual program. That is, the ability to convert automatically should be added to this code.
[INTERFACE]
CHECK_0 = ...
BTN_0 = ...
TXT_0 = ...
                ↓
[INTERFACE]
001 = ...
002 = ...
003 = ...
User avatar
ChrisR
Addict
Addict
Posts: 1124
Joined: Sun Jan 08, 2017 10:27 pm
Location: France

Re: Multi-Language (again!)

Post by ChrisR »

Hi AZJIO,
The example is probably not well chosen.
It can be done as you say, without changing the code, only the DataSection, ex:

Code: Select all

Data.s "_SECTION_",                    "Interface"
Data.s "0001",                         "OK"
Data.s "0002",                         "Cancel"
-->
ButtonGadget(#Button_OK, 20, 60, 180, 40, GetInterfaceLang("0001"))
or with more explicit names

Code: Select all

Data.s "_SECTION_",                         "Interface"
Data.s "Button_OK",                         "OK"
Data.s "Button_Cancel",                     "Cancel"
-->
ButtonGadget(#Button_OK, 20, 60, 180, 40, GetInterfaceLang("Button_OK"))

I don't know which one is easier to use!
Probably like you do, it is made in the same way in Windows .mui files, with the localised language file and the en-US fallback language
6002, "Windows command file"


Here, it is open to make it the way you want, it should work out of the box, just by changing the DataSection (the FallBack language).
XIncludeFile "Multi-Language.pb" and you're done

To improve here, it might be a good to add some escape char like /n => #CRLF$
or to keep PB syntax: "Confirm+#CRLF$+and Validate" --> "Confirm" +#CRLF$+ "and Validate"
User avatar
ChrisR
Addict
Addict
Posts: 1124
Joined: Sun Jan 08, 2017 10:27 pm
Location: France

Re: Multi-Language (again!)

Post by ChrisR »

ChrisR wrote: Fri Oct 01, 2021 9:38 am To improve here, it might be a good to add some escape char like /n => #CRLF$
or to keep PB syntax: "Confirm+#CRLF$+and Validate" --> "Confirm" +#CRLF$+ "and Validate"
Done, +#CRLF$+ without any space is now accepted as an escape character for the values.
I have updated, post #1, with a more clear example, with a MultiLine Button by using +#CRLF$+ and with different type of keywords: 0001, Text_Welcome (as constant name) or ConfirmValidate
AZJIO
Addict
Addict
Posts: 1298
Joined: Sun May 14, 2017 1:48 am

Re: Multi-Language (again!)

Post by AZJIO »

ChrisR
I meant that I would like a program that will replace all the lines inside the code with generated numbers. But now I realized that I can do this with my program, which inserts an array. I just need to replace the array with a function using a regular expression, since I need to add quotes. That is, I want to make the translation of the program automated.
I want to write in my native language and translate the interface before publishing the program.
User avatar
ChrisR
Addict
Addict
Posts: 1124
Joined: Sun Jan 08, 2017 10:27 pm
Location: France

Re: Multi-Language (again!)

Post by ChrisR »

Ha, Ok, it's not really the same use.
It might be nice to have another tool available to do this automatically :)

ps: The goal here is also to integrate it in IceDesign to have the multi-language support from the design stage.
AZJIO
Addict
Addict
Posts: 1298
Joined: Sun May 14, 2017 1:48 am

Re: Multi-Language (again!)

Post by AZJIO »

I'm taking the easy way. While my programs are not outstanding, I try to quickly insert language support without spending a lot of effort. It is important that the translation of the program does not take much time. That's why I'm using my own version for now. And besides, I did with Linux support.
screenshot
User avatar
ChrisR
Addict
Addict
Posts: 1124
Joined: Sun Jan 08, 2017 10:27 pm
Location: France

Re: Multi-Language (again!)

Post by ChrisR »

With your array, if you wish, you should be able to use the code above, quite easily with something like
Remove the example and the data after DataSection in multi-language.pb

Code: Select all

Procedure.s StringToLength(String.s, Length.i)
  ProcedureReturn String + Space(Length - Len(String))
EndProcedure

Code = " ; =========================================================================" +#CRLF$
Code + " Data.s "+#DQUOTE$+ StringToLength("_SECTION_" +#DQUOTE$+ ",", 40) +#DQUOTE$+ "Interface" +#DQUOTE$+#CRLF$
Code + " ; =========================================================================" +#CRLF$
For I = 1 to ArraySize(Array() +1
  Code + "Data.s "+#DQUOTE$+ StringToLength(Array(I)\Key   +#DQUOTE$+ ",", 40) +#DQUOTE$+ Array(I)\Value +#DQUOTE$+#CRLF$
Next
Code + "  ; =========================================================================" +#CRLF$
Code + "  Data.s " +#DQUOTE$+ StringToLength("_END_" +#DQUOTE$+ ",",    40) +#DQUOTE$+#DQUOTE$+#CRLF$
Code + "  ; =========================================================================" +#CRLF$
Code + "EndDataSection"
          
hFile = CreateFile(#PB_Any, GetPathPart(sFilePath) + GetFilePart(sFilePath, #PB_FileSystem_NoExtension) + "_Lang.pb", #PB_UTF8)
If hFile
  WriteStringFormat(hFile, #PB_UTF8)
  WriteData(hFile, ?MultiLanguage, ?EndMultiLanguage - ?MultiLanguage)
  WriteStringN(hFile, Code)
  CloseFile(hFile)
EndIf
                  
DataSection
  MultiLanguage:      : IncludeBinary "Multi-Language.pb" : EndMultiLanguage:
EndDataSection
Post Reply