From 16c5dd78b4d118383c8bd8ff1ee57db7a5711822 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:00:56 +0100 Subject: [PATCH] feat: consume LOL via liblol C ABI (Gnosis.I18n) --- casket-ssg.cabal | 3 ++ locales/de.txt | 5 --- locales/en.txt | 5 --- locales/es.txt | 5 --- locales/fr.txt | 5 --- src/Gnosis/I18n.hs | 102 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 20 deletions(-) delete mode 100644 locales/de.txt delete mode 100644 locales/en.txt delete mode 100644 locales/es.txt delete mode 100644 locales/fr.txt create mode 100755 src/Gnosis/I18n.hs diff --git a/casket-ssg.cabal b/casket-ssg.cabal index c54180d..9126b6a 100644 --- a/casket-ssg.cabal +++ b/casket-ssg.cabal @@ -22,6 +22,7 @@ executable casket-ssg , Gnosis.Paxos , Gnosis.SixSCM , Gnosis.DAX + , Gnosis.I18n hs-source-dirs: src default-language: Haskell2010 ghc-options: -Wall -O2 @@ -33,6 +34,8 @@ executable casket-ssg , filepath >= 1.4 , pandoc >= 3.0 , pandoc-types >= 1.23 + , text-icu >= 0.7 + extra-libraries: lol source-repository head type: git diff --git a/locales/de.txt b/locales/de.txt deleted file mode 100644 index 931fb35..0000000 --- a/locales/de.txt +++ /dev/null @@ -1,5 +0,0 @@ -# German translations -read_more: Weiterlesen -published: Veröffentlicht -tags: Schlagwörter -home: Startseite diff --git a/locales/en.txt b/locales/en.txt deleted file mode 100644 index 0a08bbe..0000000 --- a/locales/en.txt +++ /dev/null @@ -1,5 +0,0 @@ -# English translations -read_more: Read more -published: Published -tags: Tags -home: Home diff --git a/locales/es.txt b/locales/es.txt deleted file mode 100644 index d46fd51..0000000 --- a/locales/es.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Spanish translations -read_more: Leer más -published: Publicado -tags: Etiquetas -home: Inicio diff --git a/locales/fr.txt b/locales/fr.txt deleted file mode 100644 index 237a6dd..0000000 --- a/locales/fr.txt +++ /dev/null @@ -1,5 +0,0 @@ -# French translations -read_more: Lire la suite -published: Publié -tags: Tags -home: Accueil diff --git a/src/Gnosis/I18n.hs b/src/Gnosis/I18n.hs new file mode 100755 index 0000000..01cbed4 --- /dev/null +++ b/src/Gnosis/I18n.hs @@ -0,0 +1,102 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE OverloadedStrings #-} + +module Gnosis.I18n ( + LolHandle, + initLol, + freeLol, + resolveLang, + translate, + translatePlural, + getLastError +) where + +import Foreign +import Foreign.C.Types +import Foreign.C.String +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Foreign as TF +import System.IO.Unsafe (unsafePerformIO) +import Foreign.Ptr (nullPtr) + +data LolHandleStruct +type LolHandle = Ptr LolHandleStruct + +data LolTranslationResult +data LolLocale + +foreign import ccall "lol_init" c_lol_init :: CString -> IO LolHandle +foreign import ccall "lol_free" c_lol_free :: LolHandle -> IO () +foreign import ccall "lol_resolve_locale" c_lol_resolve_locale :: LolHandle -> CString -> IO (Ptr LolLocale) +foreign import ccall "lol_free_locale" c_lol_free_locale :: Ptr LolLocale -> IO () +foreign import ccall "lol_translate" c_lol_translate :: LolHandle -> CString -> CString -> IO (Ptr LolTranslationResult) +foreign import ccall "lol_translate_plural" c_lol_translate_plural :: LolHandle -> CString -> CString -> Word64 -> IO (Ptr LolTranslationResult) +foreign import ccall "lol_translation_text" c_lol_translation_text :: Ptr LolTranslationResult -> IO CString +foreign import ccall "lol_free_translation" c_lol_free_translation :: Ptr LolTranslationResult -> IO () +foreign import ccall "lol_last_error" c_lol_last_error :: IO CString + +initLol :: FilePath -> IO LolHandle +initLol path = withCString path c_lol_init + +freeLol :: LolHandle -> IO () +freeLol = c_lol_free + +resolveLang :: LolHandle -> Text -> IO (Maybe Text) +resolveLang handle tag = TF.withCStringLen tag $ \(ptr, len) -> do + -- Using withCString since lol_resolve_locale expects null-terminated + tagStr <- peekCStringLen (ptr, len) + withCString tagStr $ \cTag -> do + locPtr <- c_lol_resolve_locale handle cTag + if locPtr == nullPtr + then return Nothing + else do + -- In a real implementation we would extract the lang/dir fields from the struct + -- For now just return the tag to mirror missing + c_lol_free_locale locPtr + return (Just tag) + +translate :: LolHandle -> Text -> Text -> IO Text +translate handle locale key = + withTextCString locale $ \cLoc -> + withTextCString key $ \cKey -> do + resPtr <- c_lol_translate handle cLoc cKey + if resPtr == nullPtr + then return $ "(:MISSING:" <> key <> ")" + else do + cStr <- c_lol_translation_text resPtr + res <- if cStr == nullPtr + then return $ "(:MISSING:" <> key <> ")" + else peekCString cStr >>= return . T.pack + c_lol_free_translation resPtr + return res + +translatePlural :: LolHandle -> Text -> Text -> Word64 -> IO Text +translatePlural handle locale key quantity = + withTextCString locale $ \cLoc -> + withTextCString key $ \cKey -> do + resPtr <- c_lol_translate_plural handle cLoc cKey quantity + if resPtr == nullPtr + then return $ "(:MISSING:" <> key <> ")" + else do + cStr <- c_lol_translation_text resPtr + res <- if cStr == nullPtr + then return $ "(:MISSING:" <> key <> ")" + else peekCString cStr >>= return . T.pack + c_lol_free_translation resPtr + return res + +getLastError :: IO (Maybe Text) +getLastError = do + cStr <- c_lol_last_error + if cStr == nullPtr + then return Nothing + else do + str <- peekCString cStr + return $ Just (T.pack str) + +withTextCString :: Text -> (CString -> IO a) -> IO a +withTextCString t action = do + let str = T.unpack t + withCString str action