//////////////////////////////////////////////////////////////////////
// qLanguage.CC  for dQuery standalone                              //
// dBASE Inc. R&D Group                                             //
// Copyright 2001 dBASE Inc.                                        //
// Original file 08/03/2000                                         //
// Last change   03/14/2001                                         //
// Modification 08/06/2001                                          //
// Modification 08/2002 for dQuery2                                          //
// Version 1.5                                                      //
//////////////////////////////////////////////////////////////////////
// DESCRIPTION:                                                     //
// This class provides a mechanism for loading text strings from    //
// a simple text data file format and using the text strings for a  //
// variety of purposes.                                             //
//                                                                  //
// The strings are stored in the files as KEY=VALUE pairs similar   //
// to simple INI file contents. However, there is no requirement    //
// for or dependency on [SectionName] in the translation file.      //
//                                                                  //
// In addition, other types of data can be stored in the text data  //
// files and retrieved as string, numeric, logical, or date data    //
// types. Support for these data types allows for many varied uses. //
//                                                                  //
// The qLanguage class contains methods for automatically loading a //
// file based on system language settings. This is to facilitate    //
// loading UI text strings and customization of UI elements for     //
// language translation purposes.The text strings can be placed in  //
// individual files based on the translation language.              //
//                                                                  //
// Additional methods are contained in the qLanguage class to apply //
// key=value data to object property members.                       //
//                                                                  //
//////////////////////////////////////////////////////////////////////
// USAGE:                                                           
// 
//  this.qLang = new qLanguage()
//  this.qLang.setMembersFromDTF( <path to dtf file> + <dtfStub>, ( this ), <base language> )
//  
//  where
//   <path to dtf file>
//       is a relative or absolute path to the dtf folder
//   <dtfStub>   
//       is the stub name of the dtf file, for example,
//       'dquery' is a stub for the German dtf file 'dquery_DE.dtf'
//   ( this )
//       is the current object, whose string member values are replaced
//       with translated values at load time
//   <base language> (optional)
//       is the languague of the hard coded text strings in the source
//
//   code adjustments
//      no changes needed for text which are properties of an component
//          (in the form hierarchy)
//       msgbox() should be encoded to use method iMsgBox()
//          foo.qLang.iMsgBox( "", "", N, p1, p2, p3) 
//       free-standing text can be accessed by way of method iGetText()
//          foo.qLang.iGetText( "", p1, p2, p3 )
//      .. where the p1, p2, p3 parameters are optional, 
//        and are used as markers for inserting parameter values
//        into the text/translation
//////////////////////////////////////////////////////////////////////

#define _dB_CLASSNAME            "qLanguage"
#define _dB_CLASSVERSION         "1.32/7.00"
#define _dB_OBJECTNAME           _dB_CLASSNAME
#define _dB_DEFAULTPARENT        "_app"
#define _dB_DEFAULTEXT           ".DTF"
#define _dB_PLAINTEXTTAG         "iMsg."

#define CASTWORD(a)              (mod(a,2^16))
#define CASTLONG(a)              (mod(a,2^32))
#define LOWORD(l)                (CASTWORD(l))
#define HIWORD(l)                (bitrshift(CASTLONG(l),16))

#define LANG_NEUTRAL             0x00
#define SUBLANG_DEFAULT          0x01    /* user default */
#define MAKELANGID(p, s)         (bitor(bitlshift(LOWORD(s),10),LOWORD(p)))
#define LANG_USER_DEFAULT        (MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT))

#define SORT_DEFAULT             0x0     /* sorting default */
#define MAKELCID(lgid, srtid)    (bitor(bitlshift(LOWORD(srtid),16),LOWORD(lgid)))
#define LOCALE_USER_DEFAULT      (MAKELCID(LANG_USER_DEFAULT, SORT_DEFAULT))

#define LOCALE_SABBREVLANGNAME   0x00000003

CLASS qLanguage Of AssocArray 
   // un-used //   PARAMETERS oParent, cName
   PRIVATE cRefThis
   LOCAL n

   // Auto-store a reference to this object to the object member or variable name
   //   specified in cName
   // Defaults of _dB_DEFAULTPARENT for parent and _dB_CLASSNAME for name are used
   // For example:
   // New qLanguage(_app, "qLanguage")  // Create qLanguage object referenced as _app.qLanguage
   // oqLanguage = New qLanguage(Null, "oqLanguage") // Create qLanguage object referenced as oqLanguage
   // New qLanguage(FORM)              // Create qLanguage object referenced as FORM.qLanguage1
   // New qLanguage(FORM, "qLanguage")  // Create qLanguage object referenced as FORM.qLanguage
   this.Parent = IIf(Type("oParent") $ "O|A", oParent, _dB_DEFAULTPARENT)
   If Type("cName") == "C"
      this.Name = Upper(cName)
   Else
      n = 1
      Do
         this.Name = _dB_OBJECTNAME + n++
         If Not (Type("this.Parent." + this.Name) $ "O|A")
            Exit
         EndIf
      Until False
   EndIf
   cRefThis = IIf(Type("this.Parent") $ "O|A", "this.Parent." + this.Name, this.Name)
   &cRefThis. = this // Store object reference to this object in reference name

   // Setup external function prototypes.
   If Type("dB_GetLocaleInfo") # "FP"
      //extern CUINT dB_GetLocaleInfo(CUINT, CUINT, CSTRING, CUINT) Kernel32;
      //       From "GetLocaleInfoA"
      extern CINT dB_GetLocaleInfo(CUINT, CUINT, CSTRING, CINT) Kernel32;
             From "GetLocaleInfoA"
   EndIf
   If Type("dB_GetPrivateProfileString") # "FP"
      extern CINT dB_GetPrivateProfileString(CSTRING, CSTRING, CSTRING, CPTR, CLONG, CSTRING) Kernel32;
             From "GetPrivateProfileStringA"
   EndIf

   // Set custom property members.
   this.Version =                _dB_CLASSVERSION
   this.ErrorCode =              0
   this.Filename =               ""
   this.IgnoreCase =             False         // True = Keys are case to due iMsg matching

   STATIC cUserPreference = lower( this.GetINISetting( Home() + "dQuery.INI","CommandSettings", "Language", ""))

   // determine if OEM conversion is needed
   //  check charset to determine encoding assumptions
   this.oemAssumed = ( subs( _app.charset, 1,3 ) = 'DOS' ) 
   this.userPreference = cUserPreference

   // Protect internal property members.
   PROTECT Filename

   FUNCTION Release
      // Releases object (instance) of this class.
      // Returns True on success, False on failure.
      LOCAL lRet
      PRIVATE cRefThis

      cRefThis = IIf(Type("this.Parent") $ "O|A",;
                     "this.Parent." + this.Name, this.Name)
      &cRefThis. = Null                   // Destroy object reference to this.
      lRet = Type(&cRefThis) == "U"
      If Not Type("this.Parent") $ "O|A"
         Release &cRefThis
      EndIf
      Release Object this
      RETURN lRet


   FUNCTION GetINISetting(cFile, cSection, cKey, cDefault)
      LOCAL cBuff, nCnt, cRet, nInd

      cBuff = Replicate(Chr(0), 128)
      nCnt = dB_GetPrivateProfileString(cSection, cKey, "", cBuff, 256, cFile)
      cRet = ""
      If nCnt > 0                  // If the key exists with a value.
         For nInd = 0 To nCnt - 1  // Convert single-byte chars to double-byte.
            cRet += Chr(cBuff.GetByte(nInd))
         EndFor
         RETURN cRet               // Return value from INI file.
      EndIf
      RETURN cDefault              // Return default value.

   FUNCTION LoadDTF
      // Load <filename>_LL.DTF where <filename> is a string passed to cFile and:
      // LL = User default language (if file is available) or...
      // LL = _app.Language (if file is available) or...
      // <filename>.DTF (if file is available).
      // If nothing is passed to cFile, then reload the currently loaded file
      // if it has been previously loaded.
      PARAMETERS cFile, cBaseLang
      LOCAL c

      this.ErrorCode := 0
      If Not Type("cFile") $ "C|L"
         this.ErrorCode := 11
         RETURN False
      EndIf
      //  Load nothing if explict user preference matches base language.
      //   Prevents loading of DTFs that might be present that
      IF Type("cBaseLang") == 'C'
         IF lower( cBaseLang ) == this.userPreference And Not isblank( this.userPreference )
            // msgbox( "Load nothing if explict user preference matches base language.")
            RETURN False
         EndIf
      EndIf
      // If no filename is specified, attempt to load currently set filename.
      If Empty(cFile) And File(this.Filename)
         // msgbox( "If no filename is specified, attempt to load currently set filename." )
         RETURN this.Load()
      EndIf
      // Attempt to load file based on explicit dBASE user preference.
      c = cFile + "_" + this.userPreference + _dB_DEFAULTEXT 
      If File(c) And this.Load(c)
         // msgbox( "Attempt to load file based on explicit dBASE user preference.")
         RETURN True
      EndIf

      // M.Kay - dBase, LLC - 01/28/2013
      // Do Not Check Windows Locale Language setting - Not Appropriate
      //   for Project Explorer

//      // Attempt to load file based on user default language setting.
//      c = cFile + "_" + this.UserDefaultLangName() + _dB_DEFAULTEXT
//
//      IF Type("cBaseLang") == 'C' 
//         IF lower( cBaseLang ) == this.UserDefaultLangName()
//            // msgbox( "Load nothing if implicit user preference matches base language.")
//            RETURN False
//         EndIf
//      EndIf
//      If File(c) And this.Load(c)
//         // msgbox( "Attempt to load file based on user default language setting." )
//         RETURN True
//      EndIf

      // Attempt to load file based on dBASE UI language.
      c = cFile + "_" + _app.Language + _dB_DEFAULTEXT
      If File(c) And this.Load(c)
         RETURN True
      EndIf

      // Attempt to load English language translation file
      //   if different from _app.Language
      if lower(_app.Language) <> "en"
         c = cFile + "_" + "EN" + _dB_DEFAULTEXT
         If File(c) And this.Load(c)
            RETURN True
         EndIf
      endif

      // Attempt to load default translation file.
      c = cFile + _dB_DEFAULTEXT
      If File(c) And this.Load(c)
         RETURN True
      EndIf
      RETURN False

   FUNCTION SetMembersFromDTF
      // Loads DTF file and applies loaded key/value pairs to object members.
      PARAMETERS cFile, oRef, cBaseLang
      Try
        If Type("oRef.First") # "O"
           RETURN False
        EndIf
        if not this.LoadDTF(cFile, cBaseLang )
           RETURN False
        EndIf
      Catch( exception e )
        ? e.filename, e.message, e.lineno, e.code
        RETURN false
      Endtry
      RETURN this.SetMemberValues(oRef)

   FUNCTION SetMemberValues(oRef)
      // Applies loaded key/value pairs to object members.
      PRIVATE cKey, cRef

      cKey = this.FirstKey
      Do While Not Empty(cKey)
         cRef = "oRef." + cKey
         If Type("&cRef") # "U"
            If Type("&cRef") == "C"
               &cRef. = this.oemAsNeeded( this.AsText(cKey) )
            EndIf
            If Type("&cRef") == "N"
               &cRef. = this.AsNumeric(cKey)
            EndIf
            If Type("&cRef") == "L"
               &cRef. = this.AsLogical(cKey)
            EndIf
            If Type("&cRef") == "D"
               &cRef. = this.AsDate(cKey)
            EndIf
         EndIf
         cKey = this.NextKey(cKey)
      EndDo
      RETURN True

   FUNCTION Load
      // Load specific filename passed to cFilename.
      PARAMETERS cFilename
      LOCAL oFile, cRead, nLen, cKey

      this.ErrorCode := 0                 // Reset ErrorCode
      If Type("cFilename") == "C"         // If filename was passed
         If Not File(cFilename)           // If file does not exist
            this.ErrorCode := 10          // Passed file does not exist
            RETURN False
         EndIf
         this.Filename := cFilename       // Set new filename
      ElseIf Not Empty(cFilename)         // If passed data type is not character
         this.ErrorCode := 11             // Invalid filename data type passed
         RETURN False
      EndIf
      oFile = New File()
      Try
         oFile.Open(this.Filename, "R")
      Catch (exception e)
         this.ErrorCode := 12             // File open error
         RETURN False
      EndTry
      Try
         Do While Not oFile.EOF()
            cRead = LTrim(oFile.Gets())   // Read a single line of text
            If Left(cRead, 1) # ";"       // If not a file comment
               nLen = At("=", cRead) - 1
               If nLen > 0                // If valid key=value pair
                  cKey = IIf(this.IgnoreCase,;
                             Upper(Left(cRead, nLen)),;
                             Left(cRead, nLen))
                  this[cKey] = Right(cRead, Len(cRead) - (nLen + 1))
               EndIf
            EndIf
         EndDo
         RETURN True
      Catch (exception e)
         If oFile.Error() # 14
            this.ErrorCode := 13          // File read error
         EndIf
         Try
            oFile.Close()
         Catch (exception e)
            this.ErrorCode := 14          // File close error after read error
            RETURN False
         EndTry
         RETURN False
      EndTry
      Try
         oFile.Close()
      Catch (exception e)
         this.ErrorCode := 15             // File close error
      EndTry
      RETURN True

   FUNCTION AsText(c)
      // Return value as a text string.

      RETURN this[IIf(this.IgnoreCase, Upper(c), c)]

   FUNCTION AsNumeric(c)
      // Return value as a numeric.

      RETURN Val(this[IIf(this.IgnoreCase, Upper(c), c)])

   FUNCTION AsLogical(c)
      // Return value as a logical.

      RETURN Val(this[IIf(this.IgnoreCase, Upper(c), c)]) # 0

   FUNCTION AsDate(c)
      // Return value as a date.

      RETURN CToD(this[IIf(this.IgnoreCase, Upper(c), c)])

   FUNCTION UserDefaultLangName
      // Obtain 2-letter user default abbreviated language setting from system.
      LOCAL xBUFF, iResult
      //xBUFF = Replicate(Chr(0), 4)
      xBUFF = Replicate(Chr(0), 8)
      iResult = 0
      iResult = dB_GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SABBREVLANGNAME, xBUFF, 8)
      RETURN iif( lower(Left(xBUFF,2)) = "jp", "ja", Left(xBUFF, 2))

   FUNCTION oemAsNeeded( cStr )
      RETURN iif( this.oemAssumed, oem( cStr ), cStr )

   FUNCTION iGetText( cKey, p1, p2, p3 )
      //  adopted to handle up to three parameters for placement within the string
      LOCAL iMsgKey, cKey
      // check the Assoc Array
      iMsgKey = _dB_PLAINTEXTTAG + cKey
      if this.iskey( iMsgKey ) 
         cKey = this.oemAsNeeded( this[ iMsgKey ] )
      endif
      // now we have the *local language* (or default) version of the string
      // insert one or more parameters in their desired location
      try
        //  if the Parmaeter=false. do nothing.
        //   handles string, numeric or date type parametersn
        if "%1"$cKey and not empty(p1)
           cKey = cKey.stuff( atC("%1",cKey)-1, 2, ''+p1 )
        endif
        if "%2"$cKey and not empty(p2)
           cKey = cKey.stuff( atC("%2",cKey)-1, 2, ''+p2 )
        endif
        if "%3"$cKey and not empty(p3)
           cKey = cKey.stuff( atC("%3",cKey)-1, 2, ''+p3 )
        endif
      catch( exception e )
         ? "qLanguage|"+e.message
      endtry

      RETURN cKey

   FUNCTION iMsgBox( cMsgMsg, cMsgTitle, nMsgCode, param1, param2, param3 )
      // replaces MsgBox.  Variables %1, %2 and %3 are allowed
      //   Each is a placeholder for the respective parameter
      // Examples of USAGE:
      // foo.qLang.iMsgbox( "It's %1, today", "Day of the week.", 16, cdow(date()) )
      // foo.qLang.iMsgbox( "Cannotload %1 - file needed for Report", "Error", 16, cDMDFileName )
      // foo.qLang.iMsgBox( "There are three products: %1, %2, and %3.", "Our Company", 16, "dBASE SE", "dBASE Plus", "dQuery by dBASE" ) 

      nMsgCode = iif( nMsgCode = false, 0, nMsgCode ) // zero is default
      return msgbox( this.iGetText( cMsgMsg, param1, param2, param3 ), this.iGetText( cMsgTitle, param1, param2, param3 ), nMsgCode )


ENDCLASS  // qLanguage Of AssocArray

