Cixo Develop пре 4 месеци
родитељ
комит
5614951e62

+ 9 - 9
pyproject.toml

@@ -1,23 +1,23 @@
+[build-system]
+requires = ["flit_core >=3.11,<4"]
+build-backend = "flit_core.buildapi"
+
 [project]
 name = "cx_libtranslate"
-version = "1.0.0"
 authors = [
-    { name = "Cixo Develop", email = "[email protected]" }
+    {name = "Cixo Develop", email = "[email protected]"}
 ]
-description = "This is library for translating apps."
-readme = "README.md"
-requires-python = ">=3.7"
 classifiers = [
     "Programming Language :: Python :: 3",
     "Operating System :: OS Independent"
 ]
+readme = "README.md"
+version = "1.0.1"
 license = "MIT"
-license-files = [ "LICENSE" ]
+license-files = ["LICENSE"]
+dynamic = ["description"]
 
 [project.urls]
 Homepage = "https://git.cixoelectronic.pl/cixo-electronic/cx-libtranslate-python"
 Issues = "https://git.cixoelectronic.pl/cixo-electronic/cx-libtranslate-python/issues"
 
-[build-system]
-requires = [ "setuptools >= 77.0.3" ]
-build-backend = "setuptools.build_meta"

+ 9 - 0
src/cx_libtranslate/__init__.py

@@ -0,0 +1,9 @@
+""" 
+This is python version of cx-libtranslate library, which could be
+used to create translations for app.
+"""
+
+from .translation import translation
+from .phrasebook import phrasebook
+from .languages import languages
+from .loader import loader

+ 270 - 0
src/cx_libtranslate/languages.py

@@ -0,0 +1,270 @@
+import pathlib
+import json
+import typing
+
+from .loader import loader
+from .phrasebook import phrasebook
+
+class languages:
+    """ It manage languages in the project.
+    
+    It represents languages library, store location of languages directory,
+    and also names of laguages in the POSIX locale format, with paths to it 
+    in the directory. 
+
+    POSIX locale format mean that first two letters comes from ISO 639-1. and 
+    second two letters come from ISO 3166-1. For example: "en_US", "pl_PL".
+    
+    Methods
+    -------
+    add(name: str, file: pathlib.Path) -> languages
+        This add new languages file to library.
+    load(index: pathlib.Path) -> languages
+        This load languages library from index file.
+    select(name: str) -> phrasebook
+        This load phrasebook for given locales.
+    
+    Properties
+    ----------
+    avairable : typing.Iterable[str]
+        List of avairable languages names.
+    default : str
+        Default language name.
+    """
+
+    def __init__(self, path: pathlib.Path) -> None:
+        """ This create new languages library from path to languages library. 
+        
+        Parameters
+        ----------
+        path : pathlib.Path
+            Path to the languages directory.
+
+        Raises
+        ------
+        RuntimeError
+            When path is not directory or not exists.
+        """
+
+        if not path.is_dir():
+            raise RuntimeError("Path \"" + str(path) + "\" is not directory.")
+
+        if not path.exists():
+            raise RuntimeError("Directory \"" + str(path) + "\" not exists.")
+
+        self.__languages = dict()
+        self.__path = path        
+
+    def add(self, name: str, file: pathlib.Path) -> object:
+        """ This add new language to languages library.
+
+        Parameters
+        ----------
+        name : str
+            Name of the language in standard POSIX format, like "en_US", 
+            or "pl_PL".
+        file : pathlib.Path
+            Path to the file in the languages. It must be relative path. 
+        
+        Raises
+        ------
+        RuntimeError
+            When location of the language is not relavice.
+        Exception
+            When language already exists.
+        TypeError
+            When name of the language is not in property POSIX format.
+
+        Returns
+        -------
+        languages
+            Self to chain loading.
+        """
+
+        if name in self.__languages:
+            raise Exception("Language \"" + name + "\" already exists.")
+
+        if not self.__valid_locale(name):
+            raise TypeError("Name \"" + name + "\" is not property locale.")
+
+        if file.is_absolute():
+            raise RuntimeError(
+                "Location of the \"" + \
+                name + \
+                "\" must be relative."
+            )
+
+        self.__languages[name] = file
+        return self
+
+    @property
+    def avairable(self) -> typing.Iterable[str]:
+        """ It returns avairable languages.
+
+        Returns
+        -------
+        typing.Iterable[str]    
+            Languages which currently exists in the library.
+        """
+
+        return self.__languages.keys()
+
+    @property
+    def default(self) -> str:
+        """ This return default language name.
+
+        Returns
+        -------
+        str
+            Default language name.
+        """
+
+        if len(self.avairable) == 0:
+            raise RuntimeError("Load any language first.")
+            
+        for count in self.avairable:
+            return count
+
+    def __valid_locale(self, name: str) -> bool:
+        """ Check that language name is in property POSIX format.
+
+        Parameters
+        ----------
+        name : str
+            Name of the languages to check.
+
+        Returns
+        -------
+        bool
+            True when name is property formater, False when not.
+        """
+
+        splited = name.split("_")
+
+        if len(splited) != 2:
+            return False
+
+        first = splited[0]
+        second = splited[1]
+
+        if len(first) != 2 or len(second) != 2:
+            return False
+
+        if first != first.lower():
+            return False
+
+        if second != second.upper():
+            return False
+
+        return True
+
+    def load(self, index: pathlib.Path) -> object:
+        """ That load index of the languages.
+
+        To minimalize use of add function, it is avairable to create index
+        of the languages file. Index must be simple JSON file, with dict where
+        keys are POSIX formated languages names, and values are relative path
+        to the phrasebook files for that language. For example:
+        
+        ```JSON
+        {
+            "en_US": "english.json",
+            "pl_PL": "polish.json"
+        }
+        ``` 
+
+        Parameters
+        ----------
+        index : pathlib.Path
+            Relative to the index file from languages directory.
+        
+        Raises
+        ------
+        SyntaxError 
+            When index file has invalid syntax.
+        RuntimeError
+            When index file not exists or is not path to the file, or path
+            is not relative.
+        TypeError
+            When any item in index JSON file is not str.
+
+        Returns
+        -------
+        languages
+            Self to the chain loading.
+        """
+
+        if index.is_absolute():
+            raise RuntimeError(
+                "Index path \"" + \
+                str(intex) + \
+                "\" is absolute."
+            )
+
+        store = self.__path / index
+
+        if not store.is_file() or not store.exists():
+            raise RuntimeError("Index \"" + str(store) + "\" not exists.")
+
+        with store.open() as handle:
+            try:
+                loaded = json.loads(handle.read())
+            
+            except Exception as error:
+                raise SyntaxError(
+                    "Index file \"" + \
+                    str(self.__path) + \
+                    "\" has invalid syntax.\n" + \
+                    str(error)
+                ) 
+            
+        for name, file in loaded.items():
+            if type(name) is not str or not self.__valid_locale(name):
+                raise TypeError("Invalid \"" + str(name) + "\" locale.")
+
+            if type(file) is not str:
+                raise TypeError("Invalid file for \"" + name + "\".")
+
+            self.add(name, pathlib.Path(file))
+
+        return self
+    
+    def __get_lang_file(self, name: str) -> pathlib.Path:
+        """ This returns full path to the language with given name.
+
+        Parameters
+        ----------
+        name : str
+            Name of the file to get language of.
+
+        Returns
+        -------
+        pathlib.Path
+            Full path to that file.
+        """
+
+        return self.__path / self.__languages[name]
+
+    def select(self, name: str) -> phrasebook:
+        """ That load phrasebook from languages directory
+        
+        Parameters
+        ----------
+        name : str  
+            Name of the language to load phrasebook for.
+
+        Raises
+        ------
+        ValueError
+            When not exists in library.
+
+        Returns
+        -------
+        phrasebook
+            Loaded phrasebook for that language.
+        """
+
+        if not name in self.__languages:
+            raise ValueError("Language \"" + name + "\" not exists.")
+
+        return loader(self.__get_lang_file(name)).load()

+ 116 - 0
src/cx_libtranslate/loader.py

@@ -0,0 +1,116 @@
+import json
+import pathlib 
+
+from .phrasebook import phrasebook
+
+class loader:
+    """ It load phrasebook from JSNO file.
+    
+    Methods
+    -------
+    load() -> phrasebook    
+        This load phrasebook from path in constructor.
+    """
+
+    def __init__(self, path: pathlib.Path) -> None:
+        """ This create new phrasebook loader from path to phrasebook.
+
+        Parameters
+        ----------
+        path : pathlib.Path
+            Path to the phrasebook to load.
+        """
+
+        self.__path = path
+    
+    def load(self) -> object:
+        """ This load phrasebook given in constructor
+        
+        Raises
+        ------
+        RuntimeError
+            When phrasebook file does not exists.
+        SyntaxError
+            When phrasebook file has invalid syntax.
+        
+        Returns
+        -------
+        phrasebook
+            Loaded phrasebook
+        """
+
+        if not self.__path.is_file():
+            raise RuntimeError(
+                "Phrasebook file \"" + \
+                str(self.__path) + \
+                "\" not exists."
+            )
+
+        with self.__path.open() as handle:
+            try:
+                return self.__parse(json.loads(handle.read()))
+
+            except Exception as error:
+                raise SyntaxError(
+                    "Phrasebook file \"" + \
+                    str(self.__path) + \
+                    "\" has invalid syntax.\n" + \
+                    str(error)
+                ) 
+
+    def __parse(self, content: dict) -> phrasebook:
+        """ This parse phrasebook file to phrasebook object.
+
+        Parameters
+        ----------
+        content : dict
+            Content of the JSON phrasebook file.
+
+        Returns
+        -------
+        phrasebook
+            Loaded phrasebook file.
+        """
+
+        has_objects = ( 
+            "objects" in content and \
+            type(content["objects"]) is dict
+        )
+
+        has_phrases = (
+            "phrases" in content and \
+            type(content["phrases"]) is dict
+        )
+
+        is_nested = (has_objects or has_phrases)
+
+        if is_nested:
+            phrases = content["phrases"] if has_phrases else dict()
+            objects = content["objects"] if has_objects else dict()
+
+            return phrasebook(self.__parse_phrases(phrases), objects)
+
+        return phrasebook(self.__parse_phrases(content))
+
+    def __parse_phrases(self, content: dict) -> dict:
+        """ This parse phrases from phrasebook file to dict.
+
+        Parameters
+        ----------
+        content : dict
+            Content of the phrases part from file to parse.
+
+        Returns
+        -------
+        dict
+            Parsed phrases from file.
+        """
+
+        result = dict()
+        
+        for phrase, translation in content.items():
+            result[phrasebook.prepare(phrase)] = translation
+
+        return result
+
+

+ 216 - 0
src/cx_libtranslate/phrasebook.py

@@ -0,0 +1,216 @@
+from .translation import translation
+
+class phrasebook:
+    """ It store single collection of phrases.
+    
+    This is responsible for searching phrases in the phrasebook, or when
+    object exists, then also in the object notation.
+
+
+    Methods
+    -------
+    tr(phrase: str) -> translation
+        Translate phrase, short version of translate.
+
+    translate(phrase: str) -> translation
+        Translate phrase.
+
+    prepare(phrase: str) -> str
+        This prepare phrase, remove dots and white chars.  
+    """
+
+    def __init__(self, phrases: dict, objects: dict|None = None) -> None:
+        """ This initialize new phrasebook.
+
+        It require phrases dict and also objects for objects notation. Objects
+        is optional, could be leave empty.
+
+        Parameters
+        ----------
+        phrases : dict
+            Dictionary with phrases.
+        
+        objects : dict | None, default: None
+            Objects to take phrases for object notation.
+        """
+
+        self.__phrases = self.__parse_phrases(phrases)
+        self.__objects = objects
+
+    def __parse_phrases(self, phrases: dict) -> dict:
+        """ This prepare phrasebook, by flattending dictionary keys.
+
+        Parameters
+        ----------
+        phrases : dict
+            Dictionary to parse
+
+        Returns
+        -------
+        dict
+            Flattened dictionary with phrases.
+        """
+
+        flattened = dict()
+
+        for key in phrases.keys():
+            flat_key = phrasebook.prepare(key)
+
+            if flat_key in flattened:
+                raise TypeError("Key \"" + flat_key + "\" exists twice.")
+
+            flattened[flat_key] = phrases[key]
+
+        return flattened
+
+    def tr(self, phrase: str) -> translation:
+        """ This translate phrase, from phrases or objects.
+
+        Parameters
+        ----------
+        phrase : str
+            Phrase to translate.
+
+        Returns
+        -------
+        translation
+            Translated phrase.
+        """
+
+        return self.translate(phrase)
+
+    def translate(self, phrase: str) -> translation:
+        """ This translate phrase, from phrases or objects.
+
+        Parameters
+        ----------
+        phrase : str
+            Phrase to translate.
+
+        Returns
+        -------
+        translation
+            Translated phrase.
+        """
+        
+        if self.__is_nested(phrase):
+            return self.__translate_nested(phrase)
+
+        return self.__translate_flat(phrasebook.prepare(phrase))
+
+    def __translate_nested(self, phrase: str) -> translation:
+        """ This translate nested phrase.
+        
+        This transalate nested phrase, that mean phrase in object 
+        notation. When objects is not set, then return phrase as
+        failed translation.
+
+        Parameters
+        ----------
+        phrase : str
+            Nested phrase to translate.
+
+        Raises
+        ------
+        SyntaxError
+            When two dots '..' found in phrase.
+
+        Returns
+        -------
+        translation
+            Translated nested phrase.
+        """
+
+        if phrase.find("..") != -1:
+            raise SyntaxError("Symbol \"..\" in \"" + phrase + "\".")
+
+        if self.__objects is None:
+            return translation(phrase, False)
+
+        parts = phrase.split(".")
+        current = self.__objects
+
+        for part in parts:
+            if type(current) is not dict:
+                return translation(phrase, False)
+
+            if not part in current:
+                return translation(phrase, False)
+            
+            current = current[part]
+
+        if type(current) is str:
+            return translation(current, True)
+
+        return translation(phrase, False)
+
+    def __is_nested(self, phrase: str) -> bool:
+        """ This check that phrase is nested or not. 
+        
+        When phrase contain white chars, or dot only as last chat, then
+        it is not nested. When phrase contain dot, then phrase is nested.
+
+        Parameters
+        ----------
+        phrase : str
+            Phrase to check.
+
+        Returns
+        -------
+        bool
+            True when phrase is nested, False if not.
+        """
+
+        if phrase.find(" ") != -1:
+            return False
+
+        if phrase[-1] == ".":
+            return False
+
+        return phrase.find(".") != -1
+
+    def __translate_flat(self, phrase: str) -> translation:
+        """ This translate standard flat phrase.
+
+        Parameters
+        ----------
+        phrase : str
+            Phrase to translate.
+
+        Returns
+        -------
+        translation
+            Translation of the phrase.
+        """
+
+        if phrase in self.__phrases:
+            return translation(self.__phrases[phrase], True)
+
+        return translation(phrase, False)
+
+    def set_as_default(self) -> None:   
+        """ This set phrasebook as default.
+
+        This create new builtin function named "_", which could be used to 
+        translate phrase by phrasebook on which it is called, simple by
+        use _("phrase")
+        """
+
+        import builtins
+        builtins._ = lambda phrase: self.translate(phrase) 
+
+    def prepare(phrase: str) -> str:
+        """ This prepare phrase to being phrasebook dict key.
+        
+        Parameters
+        ----------
+        phrase : str
+            Phrase to translate.
+
+        Returns
+        -------
+        str
+            Prepared phrase.
+        """
+
+        return phrase.lower().replace(" ", "_").replace(".", "")

+ 111 - 0
src/cx_libtranslate/translation.py

@@ -0,0 +1,111 @@
+class translation:
+    """ This ciass is responsible for single translation in the library.
+
+    This store single translation, and its state. Could be casted to string
+    and formated. When translated phrase must contain variables, that 
+    could be passed by format function, and `#{ name }` in the phrase.
+
+    Attributes
+    ----------
+    text : str
+        Phrase content as string.
+
+    valid : bool
+        True when phrase was translated correctly or False when not.
+
+    Methods
+    -------
+    format(params: dict) -> dict
+        That method could format translated phrase with given dict.
+    """
+
+    def __init__(self, content: str, success: bool = True) -> None:
+        """ This create new translaed phrase. 
+        
+        It require string content of the translated phrase, and also state
+        of the translation. When phrase was translated successfull, then 
+        state is True but when phrase could not being found, state is False.
+
+        Parameters
+        ----------
+        content : str
+            Content of the translated phrase.
+        success : bool, default: True
+            State of the translation. 
+        """
+
+        self.__success = success
+        self.__content = content
+
+    def __str__(self) -> str:
+        """ This returns content of the phrase.
+
+        Returns
+        -------
+        str
+            Content of the translated phrase.
+        """
+
+        return self.__content
+
+    @property
+    def text(self) -> str:
+        """ String content of the phrase.
+
+        Returns
+        -------
+        str
+            Content of the translated phrase.
+        """
+
+        return self.__content
+
+    @property
+    def valid(self) -> bool:
+        """ This returns that phrase was translated propertly.
+
+        Returns
+        -------
+        bool
+            True when phrase was translated propertly or false when not.
+        """
+
+        return self.__success
+
+    def format(self, params: dict) -> str:
+        """ This format translated phrase by inserting given items into it.
+
+        Parameters
+        ----------
+        params : str
+            Items to insert into translated string.
+
+        Returns
+        -------
+        str
+            Translated content with inserted given values.
+        """
+
+        if not self.__success:
+            return self.__content
+
+        parts = self.__content.split("#{")
+        results = parts.pop(0)
+
+        for count in parts:
+            elements = count.split("}")
+
+            if len(elements) == 1:
+                results = results + count
+                continue
+
+            name = elements.pop(0).strip()
+            rest = str("}").join(elements)
+
+            if not name in params:
+                results = results + rest
+                continue
+
+            results = results + str(params[name]) + rest
+
+        return results