#!/usr/bin/env hbmk2
/*
 * Manage translations and automatic doc generation
 *
 * Copyright 2013 Viktor Szakats (vszakats.net/harbour)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 * (or visit their website at https://www.gnu.org/licenses/).
 *
 */

/* Requires:
 *   - curl (built with SSL)
 *   - hbmk2 and hbi18n in PATH
 *   - the target .prg be runnable as script (for doc_make only)
 * Reference: https://docs.transifex.com/api/introduction
 */

#pragma -w3
#pragma -km+
#pragma -ko+

#include "directry.ch"

STATIC sc_hLangMapping := { ;
   "zh_CN.GB2312" => "zh_sim", ;
   "sr"           => "sr_cyr", ;
   "sr@latin"     => "sr_lat" }

PROCEDURE Main( cCommand, cMain, ... )

   LOCAL hCommand := { ;
      "doc_make" => @doc_make(), ; /* Generate doc files for all languages */
      "src_push" => @src_push(), ; /* Upload translation source to Transifex localization service */
      "trs_pull" => @trs_pull(), ; /* Download translations from Transifex localization service */
      "trs_push" => @trs_push() }  /* Upload local translations to Transifex localization service */

   IF ! Empty( cCommand ) .AND. cCommand $ hCommand .AND. HB_ISSTRING( cMain )
      Set( _SET_DEFEXTENSIONS, .F. )
      Eval( ;
         hCommand[ cCommand ], ;
         iif( Empty( hb_FNameName( cMain := hb_DirSepToOS( cMain ) ) ), cMain + hb_FNameName( hb_DirSepDel( cMain ) ) + ".prg", cMain ), ;
         ... )
   ELSE
      ? "unrecognized command or missing target"
   ENDIF

   RETURN

/* --- */

STATIC PROCEDURE doc_make( cMain )

   LOCAL hPar := LoadPar( cMain )

   LOCAL file
   LOCAL cLang
   LOCAL cTempHRB

   IF ! Empty( hPar[ "docoption" ] )

      cTempHRB := hb_FNameExtSet( hPar[ "entry" ], ".hrb" )

      ? hPar[ "name" ], "generating documentation:"

      hb_run( hb_StrFormat( "hbmk2 -hbraw -q0 %1$s -gh -o%2$s", hPar[ "entry" ], cTempHRB ) )

      FOR EACH cLang IN hb_AIns( hPar[ "langs" ], 1, hPar[ "baselang" ], .T. )

         ?? "", cLang

         hb_run( hb_StrFormat( "hbi18n -q -g %1$s -o%2$s", ;
            hPar[ "po" ] + hb_FNameName( hPar[ "entry" ] ) + "." + cLang + ".po", ;
            hb_FNameDir( hPar[ "entry" ] ) + hb_FNameName( hPar[ "entry" ] ) + "." + cLang + ".hbl" ) )

         file := hPar[ "doc" ] + hPar[ "name" ] + "." + cLang + hPar[ "docext" ]
         hb_run( hb_StrFormat( "hbmk2 %1$s %2$s > %3$s", ;
            cTempHRB, StrTran( ArrayToList( hPar[ "docoption" ] ), "{LNG}", cLang ), file ) )
         FToNativeEOL( file )

         FErase( hb_FNameDir( hPar[ "entry" ] ) + hb_FNameName( hPar[ "entry" ] ) + "." + cLang + ".hbl" )
      NEXT

      FErase( cTempHRB )
   ENDIF

   RETURN

STATIC FUNCTION FToNativeEOL( cFile )
   RETURN hb_MemoWrit( cFile, StrTran( hb_MemoRead( cFile ), e"\n", hb_eol() ) )

/* --- */

STATIC PROCEDURE src_push( cMain )

   LOCAL hPar := LoadPar( cMain )

   LOCAL json
   LOCAL cTempContent
   LOCAL cTempResult
   LOCAL cContent

   FClose( hb_FTempCreateEx( @cTempContent, , , ".pot" ) )
   FClose( hb_FTempCreateEx( @cTempResult ) )

   ? hPar[ "name" ], "generating translation source"

   IF Empty( hPar[ "po" ] )
      cContent := LangToPO( LangToCoreLang( hPar[ "baselang" ] ) )
   ELSE
      hb_run( hb_StrFormat( "hbmk2 -hbraw -q0 %1$s -j%2$s -s", hPar[ "entry" ], cTempContent ) )

      POT_Sort( cTempContent )

      cContent := hb_MemoRead( cTempContent )
   ENDIF

   IF Empty( hPar[ "po" ] )
      #ifdef DEBUG
         hb_MemoWrit( hPar[ "baselang" ] + ".po", cContent )
      #endif
   ELSE
      cContent := hb_StrFormat( ;
         "#, c-format" + hb_eol() + ;
         'msgid ""' + hb_eol() + ;
         'msgstr ""' + hb_eol() + ;
         '"Project-Id-Version: %1$s\n"' + hb_eol() + ;
         '"Language: %2$s\n"' + hb_eol() + ;
         '"MIME-Version: 1.0\n"' + hb_eol() + ;
         '"Content-Type: text/plain; charset=UTF-8\n"' + hb_eol() + ;
         '"Content-Transfer-Encoding: 8bit\n"', hb_FNameName( hPar[ "entry" ] ), hPar[ "baselang" ] ) + hb_eol() + ;
         hb_eol() + ;
         cContent
      ? hPar[ "name" ], "saving locally"
      hb_MemoWrit( hPar[ "po" ] + hb_FNameName( hPar[ "entry" ] ) + "." + hPar[ "baselang" ] + ".po", cContent )
   ENDIF

   ? hPar[ "name" ], "uploading", "size", hb_ntos( Len( cContent ) )

   hb_MemoWrit( cTempContent, hb_jsonEncode( { "content" => StrTran( cContent, hb_eol(), e"\n" ) } ) )

   hb_run( hb_StrFormat( 'curl -s -i -L --user %1$s -X ' + ;
      'PUT -d @%2$s -H "Content-Type: application/json" ' + ;
      'https://www.transifex.com/api/2/project/%3$s/resource/%4$s/content/' + ;
      ' -o %5$s', ;
      hPar[ "login" ], cTempContent, hPar[ "project" ], ;
      hb_FNameName( hPar[ "entry" ] ), cTempResult ) )

   IF hb_jsonDecode( GetJSON( hb_MemoRead( cTempResult ) ), @json ) > 0
      ? hb_ValToExp( json )
   ELSE
      ? "API error"
   ENDIF

   FErase( cTempContent )
   FErase( cTempResult )

   RETURN

STATIC FUNCTION POT_Sort( cFileName )

   LOCAL aTrans
   LOCAL cErrorMsg

   IF ( aTrans := __i18n_potArrayLoad( cFileName, @cErrorMsg ) ) != NIL .AND. ;
      __i18n_potArraySave( cFileName, __i18n_potArraySort( aTrans ), @cErrorMsg )
      RETURN .T.
   ENDIF

   ? "i18n error", cErrorMsg

   RETURN .F.

/* --- */

STATIC PROCEDURE trs_pull( cMain )

   LOCAL hPar := LoadPar( cMain )

   LOCAL json
   LOCAL cLang
   LOCAL cTempResult

   FClose( hb_FTempCreateEx( @cTempResult ) )

   ? hPar[ "name" ], "pulling translations:"

   FOR EACH cLang IN hPar[ "langs" ]

      ?? "", cLang

      hb_run( hb_StrFormat( "curl -s -i -L --user %1$s -X " + ;
         "GET https://www.transifex.com/api/2/project/%2$s/resource/%3$s/translation/%4$s/" + ;
         " -o %5$s", ;
         hPar[ "login" ], hPar[ "project" ], ;
         hb_FNameName( hPar[ "entry" ] ), cLang, cTempResult ) )

      IF hb_jsonDecode( GetJSON( hb_MemoRead( cTempResult ) ), @json ) > 0
         hb_MemoWrit( cTempResult, json[ "content" ] )
         IF ! Empty( hPar[ "po" ] )
            POT_Sort( cTempResult )
            /* should only do this if the translation is primarily done
               on Transifex website. This encouraged and probably the case
               in practice. Delete source information, delete empty
               translations and apply some automatic transformation for
               common translation mistakes. */
            PO_Clean( cTempResult, hPar[ "po" ] + hb_FNameName( hPar[ "entry" ] ) + "." + cLang + ".po", ;
               .F., .F., @DoctorTranslation() )
         ELSE
            PO_Clean( cTempResult, cTempResult, ;
               .F., .T., @DoctorTranslation() )
            POToLang( ;
               cTempResult, ;
               hb_DirBase() + hb_DirSepToOS( "../src/lang/" ) + "l_" + LangToCoreLang( cLang ) + ".c", ;
               LangToCoreLang( cLang ) )
            #ifdef DEBUG
               hb_MemoWrit( cLang + ".po", json[ "content" ] )
            #endif
         ENDIF
      ELSE
         ? "API error"
      ENDIF
   NEXT

   FErase( cTempResult )

   RETURN

STATIC FUNCTION DoctorTranslation( cString, cOri )

   LOCAL lRightToLeft := IsRightToLeft( cString )

   LOCAL regex, hit

   IF lRightToLeft
      IF ! Empty( Left( cOri, 1 ) )
         cString := RTrim( cString )
      ENDIF
      IF ! Empty( Right( cOri, 1 ) )
         cString := LTrim( cString )
      ENDIF
   ELSE
      IF ! Empty( Left( cOri, 1 ) )
         cString := LTrim( cString )
      ENDIF
      IF ! Empty( Right( cOri, 1 ) )
         cString := RTrim( cString )
      ENDIF
   ENDIF

   /* Only if original doesn't have elongated spaces */
   IF cOri == StrUnspace( cOri )
      cString := StrUnspace( cString )
   ENDIF

   /* For Transifex: RETURN SYMBOL to real new line */
   cString := StrTran( cString, hb_UChar( 0x23CE ), e"\n" )

   IF lRightToLeft
      /* Common typos: extra space or punctuation */
      cString := hb_StrReplace( cString, { ;
         e"\n "  => e"\n"  , ;
         e". \n" => e".\n" , ;
         "( "    => "("    , ;
         " )"    => ")"    , ;
         ": "    => ":"    , ;
         ", "    => ","    , ;
         "; "    => ";"    , ;
         " .:"   => ": "   , ;
         " . :"  => ": "   , ;
         " ,:"   => ": "   , ;
         " , :"  => ": "   } )

      /* Common typos: missing space */
      FOR EACH regex IN { "([A-Za-z]):", "(\S)[,;]", "(\w)\(" }
         FOR EACH hit IN hb_regexAll( regex, cString,,,,, .T. )
            IF ! hit[ 1 ] $ cOri
               cString := StrTran( cString, hit[ 1 ], StrTran( hit[ 1 ], hit[ 2 ], hit[ 2 ] + " " ) )
            ENDIF
         NEXT
      NEXT
   ELSE
      /* Common typos: extra space or punctuation */
      cString := hb_StrReplace( cString, { ;
         e"\n "  => e"\n"  , ;
         e" .\n" => e".\n" , ;
         "( "    => "("    , ;
         " )"    => ")"    , ;
         " :"    => ":"    , ;
         " ,"    => ","    , ;
         " ;"    => ";"    , ;
         ":. "   => ": "   , ;
         ": . "  => ": "   , ;
         ":, "   => ": "   , ;
         ": , "  => ": "   } )

      /* Common typos: missing space */
      FOR EACH regex IN { ":([A-Za-z])", "[,;](\S)", "\)(\w)" }
         FOR EACH hit IN hb_regexAll( regex, cString,,,,, .T. )
            IF ! hit[ 1 ] $ cOri
               cString := StrTran( cString, hit[ 1 ], StrTran( hit[ 1 ], hit[ 2 ], " " + hit[ 2 ] ) )
            ENDIF
         NEXT
      NEXT
   ENDIF

   RETURN cString

STATIC FUNCTION IsRightToLeft( cString )

   LOCAL nChar
   LOCAL tmp

   FOR tmp := 1 TO Len( cString )
      nChar := Asc( SubStr( cString, tmp, 1 ) )
      IF ( nChar >= 0x0590 .AND. nChar <= 0x05FF ) .OR. ;  /* hebrew */
         ( nChar >= 0x0600 .AND. nChar <= 0x06FF )         /* arabic */
         RETURN .T.
      ENDIF
   NEXT

   RETURN .F.

/* Converts multiple spaces to just one */
STATIC FUNCTION StrUnspace( cString )

   LOCAL cResult := ""
   LOCAL cChar, cCharPrev
   LOCAL tmp

   FOR tmp := 1 TO Len( cString )

      cChar := SubStr( cString, tmp, 1 )

      IF ! cChar == " " .OR. ! cCharPrev == " "
         cResult += cChar
      ENDIF

      cCharPrev := cChar
   NEXT

   RETURN cResult

STATIC FUNCTION PO_Clean( cFNSource, cFNTarget, ... )

   LOCAL aTrans
   LOCAL cErrorMsg

   IF ( aTrans := __i18n_potArrayLoad( cFNSource, @cErrorMsg ) ) != NIL .AND. ;
      __i18n_potArraySave( cFNTarget, __i18n_potArrayClean( aTrans, ... ), @cErrorMsg )
      RETURN .T.
   ENDIF

   ? "i18n error", cErrorMsg

   RETURN .F.

/* --- */

STATIC PROCEDURE trs_push( cMain )

   LOCAL hPar := LoadPar( cMain )

   LOCAL json
   LOCAL cLang
   LOCAL cTempContent
   LOCAL cTempResult
   LOCAL cContent

   FClose( hb_FTempCreateEx( @cTempContent ) )
   FClose( hb_FTempCreateEx( @cTempResult ) )

   ? hPar[ "name" ], "uploading translation:"

   FOR EACH cLang IN hPar[ "langs" ]

      ?? "", cLang

      IF Empty( hPar[ "po" ] )
         cContent := LangToPO( LangToCoreLang( cLang ) )
         #ifdef DEBUG
            hb_MemoWrit( cLang + ".po", cContent )
         #endif
      ELSE
         cContent := hb_MemoRead( hPar[ "po" ] + hb_FNameName( hPar[ "entry" ] ) + "." + cLang + ".po" )
      ENDIF

      ?? "", hb_ntos( Len( cContent ) )

      hb_MemoWrit( cTempContent, hb_jsonEncode( { "content" => StrTran( cContent, hb_eol(), e"\n" ) } ) )

      hb_run( hb_StrFormat( 'curl -s -i -L --user %1$s -X ' + ;
         'PUT -d @%2$s -H "Content-Type: application/json" ' + ;
         'https://www.transifex.com/api/2/project/%3$s/resource/%4$s/translation/%5$s/' + ;
         ' -o %6$s', ;
         hPar[ "login" ], cTempContent, hPar[ "project" ], ;
         hb_FNameName( hPar[ "entry" ] ), cLang, cTempResult ) )

      IF hb_jsonDecode( GetJSON( hb_MemoRead( cTempResult ) ), @json ) > 0
         ? hb_ValToExp( json )
      ELSE
         ? "API error"
      ENDIF
   NEXT

   FErase( cTempContent )
   FErase( cTempResult )

   RETURN

/* --- */

STATIC FUNCTION GetJSON( cString )

   cString := SubStr( cString, At( "{", cString ) )
   cString := Left( cString, RAt( "}", cString ) )

   RETURN cString

STATIC FUNCTION ArrayToList( array )

   LOCAL cString := ""
   LOCAL tmp

   FOR EACH tmp IN array
      cString += tmp
      IF ! tmp:__enumIsLast()
         cString += " "
      ENDIF
   NEXT

   RETURN cString

STATIC FUNCTION _HAGetDef( xContainer, xDefault, ... )

   LOCAL item

   IF PCount() > 2
      FOR EACH item IN { ... }
         IF ( HB_ISHASHKEY( item ) .AND. HB_ISHASH( xContainer ) .AND. item $ xContainer ) .OR. ;
            ( HB_ISNUMERIC( item ) .AND. HB_ISARRAY( xContainer ) .AND. item >= 1 .AND. item <= Len( xContainer ) )
            xContainer := xContainer[ item ]
         ELSE
            RETURN xDefault
         ENDIF
      NEXT
      RETURN xContainer
   ENDIF

   RETURN xDefault

STATIC FUNCTION LangToCoreLang( cLang )
   RETURN hb_HGetDef( sc_hLangMapping, cLang, cLang )

STATIC FUNCTION LoadPar( cMain )

   LOCAL hPar := { => }

   LOCAL cConfig
   LOCAL item
   LOCAL tmp

   hPar[ "project" ]   := "harbour"
   hPar[ "entry" ]     := cMain
   hPar[ "login" ]     := GetEnv( "HB_TRANSIFEX_LOGIN" )  /* Format: username:password */
   hPar[ "name" ]      := hb_FNameName( hPar[ "entry" ] )

   IF hPar[ "entry" ] == "core-lang"
      hPar[ "baselang" ]  := "en"
      hPar[ "po" ]        := NIL
      hPar[ "langs" ]     := CoreLangList()
      FOR EACH item IN hPar[ "langs" ] DESCEND
         IF Lower( item ) == hPar[ "baselang" ]
            hb_ADel( item:__enumBase(), item:__enumIndex(), .T. )
            LOOP
         ENDIF
         FOR EACH tmp IN sc_hLangMapping
            IF Lower( item ) == tmp
               item := tmp:__enumKey()
               EXIT
            ENDIF
         NEXT
      NEXT
   ELSE
      cConfig := hb_MemoRead( hb_FNameExtSet( hPar[ "entry" ], ".hbp" ) )

      hPar[ "doc" ]       := hb_FNameDir( hPar[ "entry" ] ) + hb_DirSepToOS( "doc/" )
      hPar[ "docext" ]    := _HAGetDef( hb_regexAll( "-3rd=_langhb_docext=([\S]*)", cConfig,,,,, .T. ), ".txt", 1, 2 )
      hPar[ "docoption" ] := {}
      FOR EACH item IN hb_regexAll( "-3rd=_langhb_docoption=([\S]*)", cConfig,,,,, .T. )
         AAdd( hPar[ "docoption" ], item[ 2 ] )
      NEXT

      item := _HAGetDef( hb_regexAll( "-3rd=_langhb_entry=([\S]*)", cConfig,,,,, .T. ), NIL, 1, 2 )
      IF item != NIL
         item := hb_FNameDir( hPar[ "entry" ] ) + hb_DirSepToOS( item )
         hPar[ "entry" ] := iif( Empty( hb_FNameName( item ) ), item + hb_FNameName( hb_DirSepDel( item ) ) + ".prg", item )
      ENDIF

      cConfig := hb_MemoRead( hb_FNameExtSet( hPar[ "entry" ], ".hbp" ) )

      hPar[ "langs" ]     := hb_ATokens( _HAGetDef( hb_regexAll( "-lng=([\w,]*)", cConfig,,,,, .T. ), "", 1, 2 ), "," )
      hPar[ "baselang" ]  := _HAGetDef( hb_regexAll( "-3rd=_langhb_base=([\w]*)", cConfig,,,,, .T. ), "en", 1, 2 )

      hPar[ "po" ]        := hb_FNameDir( hPar[ "entry" ] ) + hb_DirSepToOS( "po/" )
   ENDIF

   RETURN hPar

/* --- */

#include "lang2po.hb"
#include "po2lang.hb"
