diff --git a/i18n/si/msgs.jaml b/i18n/si/msgs.jaml index ff13f181..2b595e5f 100644 --- a/i18n/si/msgs.jaml +++ b/i18n/si/msgs.jaml @@ -2515,6 +2515,104 @@ help/provider.py: content: false utf-16: false latin-1: false +localization/__init__.py: + def `pl`: + '|': false + yY: false + aeiouAEIOU: false + ies: false + s: false + def `get_languages`: + orangecanvas: false + i18n: false + .json: false + 'Invalid language file ': false + English: false + def `language_changed`: + application/language: false + application/last-used-language: false + def `update_last_used_language`: + application/language: false + English: true + application/last-used-language: false + class `Translator`: + def `__init__`: + biolab.si: false + Orange: false + application/language: false + i18n: false + {lang_eng}.json: false + {DEFAULT_LANGUAGE}.json: false + Missing language file {path}: false + def `c`: + : false + eval: false +localization/si.py: + def `plsi`: + '|': false + a: false + i: false + e: false + ov: false + def `plsi_sz`: + {n:_}: false + _: false + 1: false + z: false + zszzzzsssssssssszzzzzz: false + s: false + 0: false + zzzssssszz: false + def `z_besedo`: + nič: false + m: false + en: false + enega: false + enemu: false + enem: false + enim: false + f: false + ena: false + ene: false + eni: false + eno: false + n: false + dva: false + dveh: false + dvema: false + dve: false + tri: false + treh: false + trem: false + tremi: false + štiri: false + štirih: false + štirim: false + štirimi: false + pet: false + petih: false + petim: false + petimi: false + šest: false + šestih: false + šestim: false + šestimi: false + sedem: false + sedmih: false + sedmim: false + sedmimi: false + osem: false + osmih: false + osmim: false + osmimi: false + devet: false + devetih: false + devetim: false + devetimi: false + deset: false + desetih: false + desetim: false + desetimi: false preview/previewbrowser.py: ' @@ -3653,100 +3751,6 @@ utils/shtools.py: utf-8: false wt: false utils/localization/__init__.py: - def `pl`: - '|': false - yY: false - aeiouAEIOU: false - ies: false - s: false - def `get_languages`: - orangecanvas: false - i18n: false - .json: false - 'Invalid language file ': false - English: false - def `language_changed`: - application/language: false - application/last-used-language: false - def `update_last_used_language`: - application/language: false - English: true - application/last-used-language: false - class `Translator`: - def `__init__`: - biolab.si: false - Orange: false - application/language: false - i18n: false - {lang_eng}.json: false - {DEFAULT_LANGUAGE}.json: false - Missing language file {path}: false - def `c`: - : false - eval: false + import 'orangecanvas.localization', not 'orangecanvas.utils.localization': false utils/localization/si.py: - def `plsi`: - '|': false - a: false - i: false - e: false - ov: false - def `plsi_sz`: - {n:_}: false - _: false - 1: false - z: false - zszzzzsssssssssszzzzzz: false - s: false - 0: false - zzzssssszz: false - def `z_besedo`: - nič: false - m: false - en: false - enega: false - enemu: false - enem: false - enim: false - f: false - ena: false - ene: false - eni: false - eno: false - n: false - dva: false - dveh: false - dvema: false - dve: false - tri: false - treh: false - trem: false - tremi: false - štiri: false - štirih: false - štirim: false - štirimi: false - pet: false - petih: false - petim: false - petimi: false - šest: false - šestih: false - šestim: false - šestimi: false - sedem: false - sedmih: false - sedmim: false - sedmimi: false - osem: false - osmih: false - osmim: false - osmimi: false - devet: false - devetih: false - devetim: false - devetimi: false - deset: false - desetih: false - desetim: false - desetimi: false + import 'orangecanvas.localization.si', not 'orangecanvas.utils.localization.si': false diff --git a/i18n/trubar-config.yaml b/i18n/trubar-config.yaml index 105066b7..9ce33786 100644 --- a/i18n/trubar-config.yaml +++ b/i18n/trubar-config.yaml @@ -5,9 +5,9 @@ languages: si: name: Slovenščina international-name: Slovenian - auto-import: from orangecanvas.utils.localization.si import plsi, plsi_sz, z_besedo # pylint: disable=wrong-import-order + auto-import: from orangecanvas.localization.si import plsi, plsi_sz, z_besedo # pylint: disable=wrong-import-order auto-import: |2 - from orangecanvas.utils.localization import Translator # pylint: disable=wrong-import-order + from orangecanvas.localization import Translator # pylint: disable=wrong-import-order _tr = Translator("orangecanvas", "biolab.si", "Orange") del Translator encoding: "utf-8" \ No newline at end of file diff --git a/orangecanvas/application/settings.py b/orangecanvas/application/settings.py index 236c48ea..9d427d10 100644 --- a/orangecanvas/application/settings.py +++ b/orangecanvas/application/settings.py @@ -21,7 +21,7 @@ Signal) from .. import config -from ..utils.localization import get_languages +from ..localization import get_languages from ..utils.settings import SettingChangedEvent from ..utils.propertybindings import ( AbstractBoundProperty, PropertyBinding, BindingManager diff --git a/orangecanvas/localization/__init__.py b/orangecanvas/localization/__init__.py new file mode 100644 index 00000000..2b35e988 --- /dev/null +++ b/orangecanvas/localization/__init__.py @@ -0,0 +1,126 @@ +from functools import lru_cache +import warnings + +import os +import json +import importlib + +try: + from AnyQt.QtCore import QSettings, QLocale +except ImportError: + QSettings = QLocale = None + + +def pl(n: int, forms: str) -> str: # pylint: disable=invalid-name + """ + Choose a singular/plural form for English - or create one, for regular nouns + + `forms` can be a string containing the singular and plural form, separated + by "|", for instance `pl(n, "leaf|leaves")`. + + For nouns that are formed by adding an -s (e.g. tree -> trees), + and for nouns that end with -y that is replaced by -ies + (dictionary -> dictionaries), it suffices to pass the noun, + e.g. `pl(n, "tree")`, `pl(n, "dictionary")`. + + Args: + n: number + forms: plural forms, separated by "|", or a single (regular) noun + + Returns: + form corresponding to the given number + """ + plural = int(n != 1) + + if "|" in forms: + return forms.split("|")[plural] + + if forms[-1] in "yY" and forms[-2] not in "aeiouAEIOU": + word = [forms, forms[:-1] + "ies"][plural] + else: + word = forms + "s" * plural + if forms.isupper(): + word = word.upper() + return word + +def _load_json(path): + with open(path) as handle: + return json.load(handle) + +@lru_cache +def get_languages(package=None): + if package is None: + package = "orangecanvas" + package_path = os.path.dirname(importlib.import_module(package).__file__) + msgs_path = os.path.join(package_path, "i18n") + if not os.path.exists(msgs_path): + return {} + names = {} + for name, ext in map(os.path.splitext, os.listdir(msgs_path)): + if ext == ".json": + try: + msgs = _load_json(os.path.join(msgs_path, name + ext)) + except json.JSONDecodeError: + warnings.warn("Invalid language file " + + os.path.join(msgs_path, name + ext)) + else: + names[msgs[0]] = name + return names + + +if QLocale is not None: + DEFAULT_LANGUAGE = QLocale().languageToString(QLocale().language()) + if DEFAULT_LANGUAGE not in get_languages(): + DEFAULT_LANGUAGE = "English" +else: + DEFAULT_LANGUAGE = "English" + + +def language_changed(): + assert QSettings is not None + + s = QSettings() + lang = s.value("application/language", DEFAULT_LANGUAGE) + last_lang = s.value("application/last-used-language", DEFAULT_LANGUAGE) + return lang != last_lang + + +def update_last_used_language(): + assert QSettings is not None + + s = QSettings() + lang = s.value("application/language", "English") + s.setValue("application/last-used-language", lang) + + +class _list(list): + # Accept extra argument to allow for the original string + def __getitem__(self, item): + if isinstance(item, tuple): + item = item[0] + return super().__getitem__(item) + + +class Translator: + e = eval + + def __init__(self, package, organization="biolab.si", application="Orange"): + if QSettings is not None: + s = QSettings(QSettings.IniFormat, QSettings.UserScope, + organization, application) + lang = s.value("application/language", DEFAULT_LANGUAGE) + else: + lang = DEFAULT_LANGUAGE + # For testing purposes (and potential fallback) + # lang = os.environ.get("ORANGE_LANG", "English") + package_path = os.path.dirname(importlib.import_module(package).__file__) + lang_eng = get_languages().get(lang, lang) + path = os.path.join(package_path, "i18n", f"{lang_eng}.json") + if not os.path.exists(path): + path = os.path.join(package_path, "i18n", f"{DEFAULT_LANGUAGE}.json") + assert os.path.exists(path), f"Missing language file {path}" + self.m = _list(_load_json(path)) + + # Extra argument(s) can give the original string or any other relevant data + def c(self, idx, *_): + return compile(self.m[idx], '', 'eval') diff --git a/orangecanvas/localization/si.py b/orangecanvas/localization/si.py new file mode 100644 index 00000000..63a045d9 --- /dev/null +++ b/orangecanvas/localization/si.py @@ -0,0 +1,117 @@ +def plsi(n: int, forms: str) -> str: + """ + Choose a plural form for Slovenian - or create one, for some rare cases + + `forms` can be a string containing the singular and plural form, separated + by "|", for instance `"okno|okni|okna|oken". + + The number of forms must be 4 or 3. + - The four forms are singular, dual, plural for 3 or 4, plural for >= 5 + - Three forms are used for cases other than genitive, where plural is the + same for all numbers >= 3 + + A single form can be given for nouns in genitive that conform to one of + the following rules: + - miza/mizi/mize/miz + - korak/koraka/koraki/korakov + The function does not speak Slovenian and cannot verify the conformance. :) + + Examples: + + - Four plural forms: + f'Aktiven {nfilt} {plsi(n, "filter|filtra|filtri|filtrov")}' + + - Four forms, multiple words conjugated: + f'V tabeli je {n} {plsi(n, "učni primer|učna primera|učni primeri|učnih primerov")}' + + - Three forms (non-nominative): + f'Datoteka z {n} {plsi(n, "primerom|primeroma|primeri")}' + + - Single form, feminine, using pattern + f'Najdena {nvars} {plsi(nvars, "spremenljivka")}' + + - Single form, masculine, using pattern + f'Vsebina: {n} {plsi(n, "primer")' + + - Plural form used twice + f'{plsi(n, "Ostalo je|Ostala sta|Ostali so")} še {n} {plsi(n, "primer")}' + + Args: + n: number + forms: plural forms, separated by "|", or a single (regular) noun + + Returns: + form corresponding to the given number + """ + n = abs(n) % 100 + if n == 4: + n = 3 + elif n == 0 or n >= 5: + n = 4 + n -= 1 + + if "|" in forms: + forms = forms.split("|") + # Don't use max: we want it to fail if there are just two forms + if n == 3 and len(forms) == 3: + n -= 1 + return forms[n] + + if forms[-1] == "a": + return forms[:-1] + ("a", "i", "e", "")[n] + else: + return forms + ("", "a", "i", "ov")[n] + + +def plsi_sz(n: int) -> str: + """ + Returns proposition "s" or "z", depending on the number that will follow it. + + Args: + n (int): number + + Returns: + Proposition s or z + """ + # Cut of all groups of three, except the first one + lead3 = f"{n:_}".split("_")[0] + + # handle 1, 1_XXX, 1_XXX_XXX ... because "ena" is not pronounced and we need + # to match "tisoč", "milijon", ... "trilijarda" + # https://sl.wikipedia.org/wiki/Imena_velikih_%C5%A1tevil + if lead3 == "1": + if n > 10 ** 63: # nobody knows their names + return "z" + return "zszzzzsssssssssszzzzzz"[len(str(n)) // 3] + + # This is pronounced sto...something + if len(lead3) == 3 and lead3[0] == "1": + return "s" + + # Take the first digit, or the second for two-digit number not divisible by 10 + lead = lead3[len(lead3) == 2 and lead3[1] != "0"] + return "zzzssssszz"[int(lead)] + + +def z_besedo(n, case, gender, zero="nič"): + if not 0 <= n <= 10: + return str(n) + if n == 0: + return zero + elif n == 1: + return {"m": ("", "en", "enega", "enemu", "en", "enem", "enim"), + "f": ("", "ena", "ene", "eni", "eno", "eni", "eno"), + "n": ("", "eno", "enega", "enemu", "eno", "enem", "enim")}[gender][case] + elif n == 2: + return {"m": ("", "dva", "dveh", "dvema", "dva", "dveh", "dvema"), + "f": ("", "dve", "dveh", "dvema", "dve", "dveh", "dvema"), + "n": ("", "dve", "dveh", "dvema", "dve", "dveh", "dvema")}[gender][case] + return (None, None, None, + ("", "tri", "treh", "trem", "tri", "treh", "tremi"), + ("", "štiri", "štirih", "štirim", "štiri", "štirih", "štirimi"), + ("", "pet", "petih", "petim", "pet", "petih", "petimi"), + ("", "šest", "šestih", "šestim", "šest", "šestih", "šestimi"), + ("", "sedem", "sedmih", "sedmim", "sedem", "sedmih", "sedmimi"), + ("", "osem", "osmih", "osmim", "osem", "osmih", "osmimi"), + ("", "devet", "devetih", "devetim", "devet", "devetih", "devetimi"), + ("", "deset", "desetih", "desetim", "deset", "desetih", "desetimi"))[n][case] diff --git a/orangecanvas/utils/localization/tests/__init__.py b/orangecanvas/localization/tests/__init__.py similarity index 100% rename from orangecanvas/utils/localization/tests/__init__.py rename to orangecanvas/localization/tests/__init__.py diff --git a/orangecanvas/utils/localization/tests/test_localization.py b/orangecanvas/localization/tests/test_localization.py similarity index 53% rename from orangecanvas/utils/localization/tests/test_localization.py rename to orangecanvas/localization/tests/test_localization.py index 0ac9e2e6..afb10e99 100644 --- a/orangecanvas/utils/localization/tests/test_localization.py +++ b/orangecanvas/localization/tests/test_localization.py @@ -1,6 +1,8 @@ +import importlib import unittest +import warnings -from orangecanvas.utils.localization import pl +from orangecanvas.localization import pl class TestLocalization(unittest.TestCase): @@ -16,6 +18,18 @@ def test_pl(self): for n in (2, 5, 101, -1): self.assertEqual(pl(n, forms), plural, msg=f"for n={n}") + def test_deprecated_import(self): + warnings.simplefilter("always") + # Imports must work, but with warning + with self.assertWarns(DeprecationWarning): + # unittest discovery may have already imported this file -> reload + import orangecanvas.utils.localization + importlib.reload(orangecanvas.utils.localization) + self.assertIs(orangecanvas.utils.localization.pl, pl) + with self.assertWarns(DeprecationWarning): + # pylint: disable=unused-import + from orangecanvas.utils.localization.si import plsi + if __name__ == "__main__": unittest.main() diff --git a/orangecanvas/utils/localization/tests/test_si.py b/orangecanvas/localization/tests/test_si.py similarity index 100% rename from orangecanvas/utils/localization/tests/test_si.py rename to orangecanvas/localization/tests/test_si.py diff --git a/orangecanvas/main.py b/orangecanvas/main.py index 4d9aea40..6216f556 100644 --- a/orangecanvas/main.py +++ b/orangecanvas/main.py @@ -16,7 +16,7 @@ from AnyQt.QtGui import QFont, QColor, QPalette from AnyQt.QtCore import Qt, QSettings, QTimer, QUrl, QDir -from orangecanvas.utils import localization +from orangecanvas import localization from .utils.after_exit import run_after_exit from .styles import style_sheet, breeze_dark as _breeze_dark from .application.application import CanvasApplication diff --git a/orangecanvas/utils/localization/__init__.py b/orangecanvas/utils/localization/__init__.py index aa8a722a..acc62c41 100644 --- a/orangecanvas/utils/localization/__init__.py +++ b/orangecanvas/utils/localization/__init__.py @@ -1,112 +1,7 @@ -from functools import lru_cache import warnings -import os -import json -import importlib +from orangecanvas.localization import * # pylint: disable=unused-import -from AnyQt.QtCore import QSettings, QLocale - -def pl(n: int, forms: str) -> str: # pylint: disable=invalid-name - """ - Choose a singular/plural form for English - or create one, for regular nouns - - `forms` can be a string containing the singular and plural form, separated - by "|", for instance `pl(n, "leaf|leaves")`. - - For nouns that are formed by adding an -s (e.g. tree -> trees), - and for nouns that end with -y that is replaced by -ies - (dictionary -> dictionaries), it suffices to pass the noun, - e.g. `pl(n, "tree")`, `pl(n, "dictionary")`. - - Args: - n: number - forms: plural forms, separated by "|", or a single (regular) noun - - Returns: - form corresponding to the given number - """ - plural = int(n != 1) - - if "|" in forms: - return forms.split("|")[plural] - - if forms[-1] in "yY" and forms[-2] not in "aeiouAEIOU": - word = [forms, forms[:-1] + "ies"][plural] - else: - word = forms + "s" * plural - if forms.isupper(): - word = word.upper() - return word - -def _load_json(path): - with open(path) as handle: - return json.load(handle) - -@lru_cache -def get_languages(package=None): - if package is None: - package = "orangecanvas" - package_path = os.path.dirname(importlib.import_module(package).__file__) - msgs_path = os.path.join(package_path, "i18n") - if not os.path.exists(msgs_path): - return {} - names = {} - for name, ext in map(os.path.splitext, os.listdir(msgs_path)): - if ext == ".json": - try: - msgs = _load_json(os.path.join(msgs_path, name + ext)) - except json.JSONDecodeError: - warnings.warn("Invalid language file " - + os.path.join(msgs_path, name + ext)) - else: - names[msgs[0]] = name - return names - - -DEFAULT_LANGUAGE = QLocale().languageToString(QLocale().language()) -if DEFAULT_LANGUAGE not in get_languages(): - DEFAULT_LANGUAGE = "English" - - -def language_changed(): - s = QSettings() - lang = s.value("application/language", DEFAULT_LANGUAGE) - last_lang = s.value("application/last-used-language", DEFAULT_LANGUAGE) - return lang != last_lang - - -def update_last_used_language(): - s = QSettings() - lang = s.value("application/language", "English") - s.setValue("application/last-used-language", lang) - - -class _list(list): - # Accept extra argument to allow for the original string - def __getitem__(self, item): - if isinstance(item, tuple): - item = item[0] - return super().__getitem__(item) - - -class Translator: - e = eval - - def __init__(self, package, organization="biolab.si", application="Orange"): - s = QSettings(QSettings.IniFormat, QSettings.UserScope, - organization, application) - lang = s.value("application/language", DEFAULT_LANGUAGE) - # For testing purposes (and potential fallback) - # lang = os.environ.get("ORANGE_LANG", "English") - package_path = os.path.dirname(importlib.import_module(package).__file__) - lang_eng = get_languages().get(lang, lang) - path = os.path.join(package_path, "i18n", f"{lang_eng}.json") - if not os.path.exists(path): - path = os.path.join(package_path, "i18n", f"{DEFAULT_LANGUAGE}.json") - assert os.path.exists(path), f"Missing language file {path}" - self.m = _list(_load_json(path)) - - # Extra argument(s) can give the original string or any other relevant data - def c(self, idx, *_): - return compile(self.m[idx], '', 'eval') +warnings.warn( + "import 'orangecanvas.localization', not 'orangecanvas.utils.localization'", + DeprecationWarning) diff --git a/orangecanvas/utils/localization/si.py b/orangecanvas/utils/localization/si.py index 63a045d9..a341e888 100644 --- a/orangecanvas/utils/localization/si.py +++ b/orangecanvas/utils/localization/si.py @@ -1,117 +1,8 @@ -def plsi(n: int, forms: str) -> str: - """ - Choose a plural form for Slovenian - or create one, for some rare cases +import warnings - `forms` can be a string containing the singular and plural form, separated - by "|", for instance `"okno|okni|okna|oken". +from orangecanvas.localization.si import * # pylint: disable=unused-import - The number of forms must be 4 or 3. - - The four forms are singular, dual, plural for 3 or 4, plural for >= 5 - - Three forms are used for cases other than genitive, where plural is the - same for all numbers >= 3 +warnings.warn( + "import 'orangecanvas.localization.si', not 'orangecanvas.utils.localization.si'", + DeprecationWarning) - A single form can be given for nouns in genitive that conform to one of - the following rules: - - miza/mizi/mize/miz - - korak/koraka/koraki/korakov - The function does not speak Slovenian and cannot verify the conformance. :) - - Examples: - - - Four plural forms: - f'Aktiven {nfilt} {plsi(n, "filter|filtra|filtri|filtrov")}' - - - Four forms, multiple words conjugated: - f'V tabeli je {n} {plsi(n, "učni primer|učna primera|učni primeri|učnih primerov")}' - - - Three forms (non-nominative): - f'Datoteka z {n} {plsi(n, "primerom|primeroma|primeri")}' - - - Single form, feminine, using pattern - f'Najdena {nvars} {plsi(nvars, "spremenljivka")}' - - - Single form, masculine, using pattern - f'Vsebina: {n} {plsi(n, "primer")' - - - Plural form used twice - f'{plsi(n, "Ostalo je|Ostala sta|Ostali so")} še {n} {plsi(n, "primer")}' - - Args: - n: number - forms: plural forms, separated by "|", or a single (regular) noun - - Returns: - form corresponding to the given number - """ - n = abs(n) % 100 - if n == 4: - n = 3 - elif n == 0 or n >= 5: - n = 4 - n -= 1 - - if "|" in forms: - forms = forms.split("|") - # Don't use max: we want it to fail if there are just two forms - if n == 3 and len(forms) == 3: - n -= 1 - return forms[n] - - if forms[-1] == "a": - return forms[:-1] + ("a", "i", "e", "")[n] - else: - return forms + ("", "a", "i", "ov")[n] - - -def plsi_sz(n: int) -> str: - """ - Returns proposition "s" or "z", depending on the number that will follow it. - - Args: - n (int): number - - Returns: - Proposition s or z - """ - # Cut of all groups of three, except the first one - lead3 = f"{n:_}".split("_")[0] - - # handle 1, 1_XXX, 1_XXX_XXX ... because "ena" is not pronounced and we need - # to match "tisoč", "milijon", ... "trilijarda" - # https://sl.wikipedia.org/wiki/Imena_velikih_%C5%A1tevil - if lead3 == "1": - if n > 10 ** 63: # nobody knows their names - return "z" - return "zszzzzsssssssssszzzzzz"[len(str(n)) // 3] - - # This is pronounced sto...something - if len(lead3) == 3 and lead3[0] == "1": - return "s" - - # Take the first digit, or the second for two-digit number not divisible by 10 - lead = lead3[len(lead3) == 2 and lead3[1] != "0"] - return "zzzssssszz"[int(lead)] - - -def z_besedo(n, case, gender, zero="nič"): - if not 0 <= n <= 10: - return str(n) - if n == 0: - return zero - elif n == 1: - return {"m": ("", "en", "enega", "enemu", "en", "enem", "enim"), - "f": ("", "ena", "ene", "eni", "eno", "eni", "eno"), - "n": ("", "eno", "enega", "enemu", "eno", "enem", "enim")}[gender][case] - elif n == 2: - return {"m": ("", "dva", "dveh", "dvema", "dva", "dveh", "dvema"), - "f": ("", "dve", "dveh", "dvema", "dve", "dveh", "dvema"), - "n": ("", "dve", "dveh", "dvema", "dve", "dveh", "dvema")}[gender][case] - return (None, None, None, - ("", "tri", "treh", "trem", "tri", "treh", "tremi"), - ("", "štiri", "štirih", "štirim", "štiri", "štirih", "štirimi"), - ("", "pet", "petih", "petim", "pet", "petih", "petimi"), - ("", "šest", "šestih", "šestim", "šest", "šestih", "šestimi"), - ("", "sedem", "sedmih", "sedmim", "sedem", "sedmih", "sedmimi"), - ("", "osem", "osmih", "osmim", "osem", "osmih", "osmimi"), - ("", "devet", "devetih", "devetim", "devet", "devetih", "devetimi"), - ("", "deset", "desetih", "desetim", "deset", "desetih", "desetimi"))[n][case]