kivy-garden-i18n 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kivy-garden-i18n might be problematic. Click here for more details.
- kivy_garden_i18n-0.2.0/PKG-INFO +44 -0
- kivy_garden_i18n-0.2.0/README.md +21 -0
- {kivy_garden_i18n-0.1.0 → kivy_garden_i18n-0.2.0}/pyproject.toml +16 -14
- kivy_garden_i18n-0.2.0/src/kivy_garden/i18n/_exceptions.py +14 -0
- kivy_garden_i18n-0.2.0/src/kivy_garden/i18n/fontfinder.py +154 -0
- kivy_garden_i18n-0.2.0/src/kivy_garden/i18n/localizer.py +204 -0
- kivy_garden_i18n-0.2.0/src/kivy_garden/i18n/utils/__init__.py +1 -0
- kivy_garden_i18n-0.1.0/kivy_garden/i18n/extract_msgids_from_string_literals.py → kivy_garden_i18n-0.2.0/src/kivy_garden/i18n/utils/_extract_msgids_from_string_literals.py +22 -10
- kivy_garden_i18n-0.1.0/PKG-INFO +0 -269
- kivy_garden_i18n-0.1.0/README.md +0 -243
- kivy_garden_i18n-0.1.0/kivy_garden/i18n/fontfinder.py +0 -98
- kivy_garden_i18n-0.1.0/kivy_garden/i18n/localizer.py +0 -94
- {kivy_garden_i18n-0.1.0 → kivy_garden_i18n-0.2.0/src}/kivy_garden/i18n/__init__.py +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: kivy-garden-i18n
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: i18n for Kivy
|
|
5
|
+
Home-page: https://github.com/gottadiveintopython/i18n
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: kivy
|
|
8
|
+
Author: Nattōsai Mitō
|
|
9
|
+
Author-email: flow4re2c@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Project-URL: Repository, https://github.com/gottadiveintopython/i18n
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# kivy_garden.i18n
|
|
24
|
+
|
|
25
|
+
A library that assists in creating a Kivy app with internationalization support.
|
|
26
|
+
|
|
27
|
+
It's composed of the following two primary components:
|
|
28
|
+
|
|
29
|
+
- The **Font Finder** helps you find fonts.
|
|
30
|
+
If your app doesn't need i18n support, and you just want to use pre-installed fonts to saving space, you probably only need this.
|
|
31
|
+
- The **Localizer** helps you switch texts and fonts according to "current language".
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
pip install "kivy-garden-i18n>=0.2<0.3"
|
|
37
|
+
poetry add kivy-garden-i18n@~0.2
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
# Tested on
|
|
41
|
+
|
|
42
|
+
- CPython 3.10 + Kivy 2.2.1
|
|
43
|
+
- CPython 3.11 + Kivy 2.2.1
|
|
44
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# kivy_garden.i18n
|
|
2
|
+
|
|
3
|
+
A library that assists in creating a Kivy app with internationalization support.
|
|
4
|
+
|
|
5
|
+
It's composed of the following two primary components:
|
|
6
|
+
|
|
7
|
+
- The **Font Finder** helps you find fonts.
|
|
8
|
+
If your app doesn't need i18n support, and you just want to use pre-installed fonts to saving space, you probably only need this.
|
|
9
|
+
- The **Localizer** helps you switch texts and fonts according to "current language".
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
pip install "kivy-garden-i18n>=0.2<0.3"
|
|
15
|
+
poetry add kivy-garden-i18n@~0.2
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
# Tested on
|
|
19
|
+
|
|
20
|
+
- CPython 3.10 + Kivy 2.2.1
|
|
21
|
+
- CPython 3.11 + Kivy 2.2.1
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
|
-
name = "
|
|
3
|
-
version = "0.
|
|
2
|
+
name = "kivy-garden-i18n"
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "i18n for Kivy"
|
|
5
5
|
authors = ["Nattōsai Mitō <flow4re2c@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
7
|
-
readme =
|
|
7
|
+
readme = "README.md"
|
|
8
8
|
repository = 'https://github.com/gottadiveintopython/i18n'
|
|
9
9
|
homepage = 'https://github.com/gottadiveintopython/i18n'
|
|
10
10
|
keywords = ['kivy']
|
|
@@ -13,28 +13,30 @@ classifiers=[
|
|
|
13
13
|
'License :: OSI Approved :: MIT License',
|
|
14
14
|
'Intended Audience :: Developers',
|
|
15
15
|
'Programming Language :: Python',
|
|
16
|
-
'Programming Language :: Python :: 3.8',
|
|
17
|
-
'Programming Language :: Python :: 3.9',
|
|
18
16
|
'Programming Language :: Python :: 3.10',
|
|
19
17
|
'Programming Language :: Python :: 3.11',
|
|
20
|
-
'Programming Language :: Python :: 3.12',
|
|
21
18
|
'Topic :: Software Development :: Libraries',
|
|
22
19
|
'Operating System :: OS Independent',
|
|
23
20
|
]
|
|
24
|
-
|
|
25
21
|
packages = [
|
|
26
|
-
{ include = "kivy_garden" },
|
|
22
|
+
{ include = "kivy_garden", from = "src" },
|
|
27
23
|
]
|
|
28
24
|
|
|
29
25
|
[tool.poetry.dependencies]
|
|
30
|
-
python = "^3.
|
|
31
|
-
|
|
32
|
-
[tool.poetry.dev-dependencies]
|
|
33
|
-
pytest = "^6.2.5"
|
|
26
|
+
python = "^3.10"
|
|
34
27
|
|
|
35
28
|
[tool.poetry.group.dev.dependencies]
|
|
36
|
-
|
|
29
|
+
pytest = "^7.4.3"
|
|
30
|
+
Kivy = "^2.2.1"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
[tool.poetry.group.doc.dependencies]
|
|
34
|
+
sphinx = "^7.2.6"
|
|
35
|
+
sphinx-autobuild = "^2021.3.14"
|
|
37
36
|
|
|
38
37
|
[build-system]
|
|
39
|
-
requires = ["poetry-core
|
|
38
|
+
requires = ["poetry-core"]
|
|
40
39
|
build-backend = "poetry.core.masonry.api"
|
|
40
|
+
|
|
41
|
+
[tool.poetry.scripts]
|
|
42
|
+
extract-msgids = 'kivy_garden.i18n.utils._extract_msgids_from_string_literals:cli_main'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
__all__ = (
|
|
2
|
+
'I18nError', 'UnsupportedLanguageError',
|
|
3
|
+
)
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class I18nError(Exception):
|
|
8
|
+
'''Base class of all the module-specific exceptions'''
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UnsupportedLanguageError(I18nError):
|
|
12
|
+
@cached_property
|
|
13
|
+
def lang(self):
|
|
14
|
+
return self.args[0]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
__all__ =(
|
|
2
|
+
'enum_pre_installed_fonts', 'can_render_text', 'can_render_lang', 'default_filter', 'enum_langs', 'register_lang',
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
from typing import Union
|
|
6
|
+
from collections.abc import Callable, Iterator
|
|
7
|
+
from pathlib import Path, PurePath
|
|
8
|
+
from kivy.core.text import LabelBase, Label as CoreLabel
|
|
9
|
+
|
|
10
|
+
from ._exceptions import UnsupportedLanguageError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def default_filter(font: PurePath, suffixes=('.ttf', '.otf', '.ttc', )) -> bool:
|
|
14
|
+
'''
|
|
15
|
+
The default *filter* of :func:`enum_pre_installed_fonts`.
|
|
16
|
+
If the suffix of the ``font`` is one of ``.ttf``, ``.otf`` and ``.ttc`` as well as its filename doesn't contain
|
|
17
|
+
``'fallback'``, this function returns True.
|
|
18
|
+
'''
|
|
19
|
+
return font.suffix in suffixes and 'fallback' not in font.name.lower()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def enum_pre_installed_fonts(*, filter: Callable[[Path], bool]=default_filter) -> Iterator[Path]:
|
|
23
|
+
for dir in LabelBase.get_system_fonts_dir():
|
|
24
|
+
for child in Path(dir).iterdir():
|
|
25
|
+
if filter(child):
|
|
26
|
+
yield child
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def can_render_text(font: Union[str, Path], text: str) -> bool:
|
|
30
|
+
'''
|
|
31
|
+
Whether a specified ``font`` is capable of rendering a specified ``text``.
|
|
32
|
+
|
|
33
|
+
:param text: must consist of more than two characters without repetition.
|
|
34
|
+
|
|
35
|
+
.. code-block::
|
|
36
|
+
|
|
37
|
+
from kivy_garden.i18n.fontfinder import can_render_text as f
|
|
38
|
+
|
|
39
|
+
# Roboto, the default font, lacks CJK characters.
|
|
40
|
+
assert f("Roboto", "ABC")
|
|
41
|
+
assert not f("Roboto", "漢字한글ひら")
|
|
42
|
+
assert not f("Roboto", "漢字한글ひらABC")
|
|
43
|
+
|
|
44
|
+
assert f("NotoSerifCJK-Regular.ttc", "ABC")
|
|
45
|
+
assert f("NotoSerifCJK-Regular.ttc", "漢字한글ひら")
|
|
46
|
+
assert f("NotoSerifCJK-Regular.ttc", "漢字한글ひらABC")
|
|
47
|
+
|
|
48
|
+
# Fallback-fonts may lack ASCII characters.
|
|
49
|
+
assert not f("DroidSansFallbackFull.ttf", "ABC")
|
|
50
|
+
assert f("DroidSansFallbackFull.ttf", "漢字한글ひら")
|
|
51
|
+
assert not f("DroidSansFallbackFull.ttf", "漢字한글ひらABC")
|
|
52
|
+
|
|
53
|
+
.. note::
|
|
54
|
+
|
|
55
|
+
The longer the ``text`` is, the more accurate the result will be, but the more time it would take.
|
|
56
|
+
'''
|
|
57
|
+
if not _validate_discriminant(text):
|
|
58
|
+
raise ValueError(f"'text' must consist of more than two characters without repetition (was {text!r})")
|
|
59
|
+
label = CoreLabel()
|
|
60
|
+
label._size = (16, 16, )
|
|
61
|
+
label.options['font_name'] = str(font)
|
|
62
|
+
label.resolve_font_name()
|
|
63
|
+
rendered_text = set()
|
|
64
|
+
for i, c in enumerate(text, start=1):
|
|
65
|
+
label.text = c
|
|
66
|
+
label._render_begin()
|
|
67
|
+
label._render_text(c, 0, 0)
|
|
68
|
+
data = label._render_end().data
|
|
69
|
+
if data in rendered_text:
|
|
70
|
+
return False
|
|
71
|
+
rendered_text.add(data)
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
DISCRIMINANTS = {
|
|
76
|
+
'ar': 'الجزيرةAB',
|
|
77
|
+
'hi': 'भारतAB',
|
|
78
|
+
'ja': (v := '経伝説あAB'),
|
|
79
|
+
'ja-JP': v,
|
|
80
|
+
'ja_JP': v,
|
|
81
|
+
'ko': (v := '안녕조AB'),
|
|
82
|
+
'ko-KR': v,
|
|
83
|
+
'ko_KR': v,
|
|
84
|
+
'zh-Hans': (v := '哪经传说AB'),
|
|
85
|
+
'zh_Hans': v,
|
|
86
|
+
'zh-CN': v,
|
|
87
|
+
'zh_CN': v,
|
|
88
|
+
'zh-SG': v,
|
|
89
|
+
'zh_SG': v,
|
|
90
|
+
'zh-Hant': (v := '哪經傳說AB'),
|
|
91
|
+
'zh_Hant': v,
|
|
92
|
+
'zh-TW': v,
|
|
93
|
+
'zh_TW': v,
|
|
94
|
+
'zh-HK': v,
|
|
95
|
+
'zh_HK': v,
|
|
96
|
+
'zh-MO': v,
|
|
97
|
+
'zh_MO': v,
|
|
98
|
+
'zh': '哪經傳說经传说AB',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def can_render_lang(font: Union[str, Path], lang: str) -> bool:
|
|
103
|
+
'''
|
|
104
|
+
Whether a specified ``font`` is capable of rendering a specified ``lang``.
|
|
105
|
+
|
|
106
|
+
.. code-block::
|
|
107
|
+
|
|
108
|
+
from kivy_garden.i18n.fontfinder import can_render_lang as f
|
|
109
|
+
|
|
110
|
+
assert not f("Roboto", "zh")
|
|
111
|
+
assert not f("Roboto", "ko")
|
|
112
|
+
assert not f("Roboto", "ja")
|
|
113
|
+
|
|
114
|
+
assert f("NotoSerifCJK-Regular.ttc", "zh")
|
|
115
|
+
assert f("NotoSerifCJK-Regular.ttc", "ko")
|
|
116
|
+
assert f("NotoSerifCJK-Regular.ttc", "ja")
|
|
117
|
+
|
|
118
|
+
# Font that lacks ASCII characters is considered unable to render any language.
|
|
119
|
+
assert not f("DroidSansFallbackFull.ttf", "zh")
|
|
120
|
+
assert not f("DroidSansFallbackFull.ttf", "ko")
|
|
121
|
+
assert not f("DroidSansFallbackFull.ttf", "ja")
|
|
122
|
+
|
|
123
|
+
:raise UnsupportedLanguageError: if the given ``lang`` is not supported.
|
|
124
|
+
'''
|
|
125
|
+
try:
|
|
126
|
+
text = DISCRIMINANTS[lang]
|
|
127
|
+
except KeyError:
|
|
128
|
+
raise UnsupportedLanguageError(lang)
|
|
129
|
+
return can_render_text(font, text)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def enum_langs() -> Iterator[str]:
|
|
133
|
+
'''
|
|
134
|
+
Available languages for :func:`can_render_lang`.
|
|
135
|
+
'''
|
|
136
|
+
return DISCRIMINANTS.keys()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def register_lang(lang: str, discriminant: str):
|
|
140
|
+
'''
|
|
141
|
+
Enable a language in the :func:`can_render_lang`.
|
|
142
|
+
|
|
143
|
+
.. code-block::
|
|
144
|
+
|
|
145
|
+
register_lang('th', "ราชอAB") # Thai language
|
|
146
|
+
'''
|
|
147
|
+
if not _validate_discriminant(discriminant):
|
|
148
|
+
raise ValueError(f"'discriminant' must consist of more than two characters without repetition (was {discriminant!r})")
|
|
149
|
+
DISCRIMINANTS[lang] = discriminant
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _validate_discriminant(discriminant: str, len=len, set=set) -> bool:
|
|
153
|
+
l = len(discriminant)
|
|
154
|
+
return l >= 3 and len(set(discriminant)) == l
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
# type hints
|
|
5
|
+
'Translator', 'TranslatorFactory', 'FontPicker',
|
|
6
|
+
|
|
7
|
+
# exceptions
|
|
8
|
+
'I18nError', 'FontNotFoundError',
|
|
9
|
+
|
|
10
|
+
# concrete TranslatorFactory
|
|
11
|
+
'GettextBasedTranslatorFactory', 'MappingBasedTranslatorFactory',
|
|
12
|
+
|
|
13
|
+
# concrete FontPicker
|
|
14
|
+
'DefaultFontPicker',
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
'Localizer',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from collections.abc import Callable, Mapping
|
|
21
|
+
from typing import TypeAlias, Union
|
|
22
|
+
import itertools
|
|
23
|
+
from functools import cached_property
|
|
24
|
+
|
|
25
|
+
from kivy.properties import StringProperty, ObjectProperty
|
|
26
|
+
from kivy.event import EventDispatcher
|
|
27
|
+
from kivy.logger import Logger
|
|
28
|
+
from kivy.uix.label import Label
|
|
29
|
+
|
|
30
|
+
Msgid: TypeAlias = str
|
|
31
|
+
Msgstr: TypeAlias = str
|
|
32
|
+
Lang: TypeAlias = str
|
|
33
|
+
Translator: TypeAlias = Callable[[Msgid], Msgstr]
|
|
34
|
+
TranslatorFactory : TypeAlias = Callable[[Lang], Translator]
|
|
35
|
+
Font: TypeAlias = str
|
|
36
|
+
FontPicker: TypeAlias = Callable[[Lang], Font]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class I18nError(Exception):
|
|
40
|
+
'''Base class of all the module-specific exceptions'''
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FontNotFoundError(I18nError):
|
|
44
|
+
'''Failed to find a font.'''
|
|
45
|
+
|
|
46
|
+
@cached_property
|
|
47
|
+
def lang(self) -> Lang:
|
|
48
|
+
'''The language for which a localizer couldn't find a suitable font.'''
|
|
49
|
+
return self.args[0]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Localizer(EventDispatcher):
|
|
53
|
+
lang: Lang = StringProperty()
|
|
54
|
+
'''
|
|
55
|
+
The "current language" of this localizer.
|
|
56
|
+
'''
|
|
57
|
+
|
|
58
|
+
_: Translator = ObjectProperty(lambda msgid: msgid)
|
|
59
|
+
'''
|
|
60
|
+
(read-only)
|
|
61
|
+
A callable object that takes a ``msgid``, and returns the corresponging ``msgstr`` according to the "current language".
|
|
62
|
+
|
|
63
|
+
.. code-block::
|
|
64
|
+
|
|
65
|
+
loc = Localizer(...)
|
|
66
|
+
loc.lang = "en"
|
|
67
|
+
print(loc._("app title")) # => "My First Kivy Program"
|
|
68
|
+
loc.lang = "ja"
|
|
69
|
+
print(loc._("app title")) # => "初めてのKivyプログラム"
|
|
70
|
+
|
|
71
|
+
:meta public:
|
|
72
|
+
'''
|
|
73
|
+
|
|
74
|
+
font_name: Font = StringProperty(Label.font_name.defaultvalue)
|
|
75
|
+
'''
|
|
76
|
+
(read-only)
|
|
77
|
+
A font that is capable of rendering the "current language".
|
|
78
|
+
|
|
79
|
+
.. code-block::
|
|
80
|
+
|
|
81
|
+
loc = Localizer(...)
|
|
82
|
+
loc.lang = "en"
|
|
83
|
+
print(loc.font_name) # => "Roboto"
|
|
84
|
+
loc.lang = "ko"
|
|
85
|
+
print(loc.font_name) # => "/../NotoSerifCJK-Regular.ttc"
|
|
86
|
+
'''
|
|
87
|
+
|
|
88
|
+
def __init__(self, *, lang: Lang='en', translator_factory: TranslatorFactory=None, font_picker: FontPicker=None):
|
|
89
|
+
if translator_factory is None:
|
|
90
|
+
Logger.warning(
|
|
91
|
+
f"kivy_garden.i18n: No translator_factory was provided. ``msgid``s themselves will be displaed.")
|
|
92
|
+
translator_factory = lambda lang: lambda msgid: msgid
|
|
93
|
+
if font_picker is None:
|
|
94
|
+
font_picker = DefaultFontPicker()
|
|
95
|
+
self.translator_factory = translator_factory
|
|
96
|
+
self.font_picker = font_picker
|
|
97
|
+
super().__init__(lang=lang)
|
|
98
|
+
|
|
99
|
+
def install(self, *, name):
|
|
100
|
+
'''
|
|
101
|
+
Makes the localizer accessble from kv without any import-statements.
|
|
102
|
+
|
|
103
|
+
.. code-block::
|
|
104
|
+
|
|
105
|
+
loc = Localizer(...)
|
|
106
|
+
loc.install(name='l')
|
|
107
|
+
|
|
108
|
+
.. code-block:: yaml
|
|
109
|
+
|
|
110
|
+
Label:
|
|
111
|
+
font_name: l.font_name
|
|
112
|
+
text: l._("msgid")
|
|
113
|
+
|
|
114
|
+
:raises I18nError: if the ``name`` has already been used.
|
|
115
|
+
'''
|
|
116
|
+
from kivy.lang import global_idmap
|
|
117
|
+
if name in global_idmap:
|
|
118
|
+
raise I18nError(f"The name {name!r} has already been used.")
|
|
119
|
+
global_idmap[name] = self
|
|
120
|
+
|
|
121
|
+
def uninstall(self, *, name):
|
|
122
|
+
from kivy.lang import global_idmap
|
|
123
|
+
if name not in global_idmap:
|
|
124
|
+
raise I18nError(f"The name {name!r} not found.")
|
|
125
|
+
if global_idmap[name] is not self:
|
|
126
|
+
raise I18nError(f"The object referenced by {name!r} is not me.")
|
|
127
|
+
del global_idmap[name]
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def on_lang(self, lang):
|
|
131
|
+
''':meta private:'''
|
|
132
|
+
self._ = self.translator_factory(lang)
|
|
133
|
+
self.font_name = self.font_picker(lang)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DefaultFontPicker:
|
|
137
|
+
PRESET = {
|
|
138
|
+
'en': (v := 'Roboto'),
|
|
139
|
+
'fr': v,
|
|
140
|
+
'it': v,
|
|
141
|
+
'pt': v,
|
|
142
|
+
'ru': v,
|
|
143
|
+
}
|
|
144
|
+
''':meta private:'''
|
|
145
|
+
|
|
146
|
+
del v
|
|
147
|
+
|
|
148
|
+
def __init__(self, *, fallback: Union[Lang, None]='Roboto'):
|
|
149
|
+
self._lang2font = self.PRESET.copy()
|
|
150
|
+
self._fallback = fallback
|
|
151
|
+
|
|
152
|
+
def __call__(self, lang: Lang) -> Font:
|
|
153
|
+
try:
|
|
154
|
+
return self._lang2font[lang]
|
|
155
|
+
except KeyError:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
from .fontfinder import enum_pre_installed_fonts, can_render_lang
|
|
159
|
+
name = None
|
|
160
|
+
for font in enum_pre_installed_fonts():
|
|
161
|
+
if can_render_lang(font, lang):
|
|
162
|
+
name = font.name
|
|
163
|
+
break
|
|
164
|
+
if name is None:
|
|
165
|
+
fallback = self._fallback
|
|
166
|
+
if fallback is None:
|
|
167
|
+
raise FontNotFoundError(lang)
|
|
168
|
+
Logger.warning(f"kivy_garden.i18n: Couldn't find a font for lang '{lang}'. Use {fallback} as a fallback.")
|
|
169
|
+
name = fallback
|
|
170
|
+
self._lang2font[lang] = name
|
|
171
|
+
return name
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class GettextBasedTranslatorFactory:
|
|
175
|
+
def __init__(self, domain, localedir):
|
|
176
|
+
self.domain = domain
|
|
177
|
+
self.localedir = localedir
|
|
178
|
+
|
|
179
|
+
def __call__(self, lang: Lang) -> Translator:
|
|
180
|
+
from gettext import translation
|
|
181
|
+
return translation(domain=self.domain, localedir=self.localedir, languages=(lang, )).gettext
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class MappingBasedTranslatorFactory:
|
|
185
|
+
def __init__(self, table: Mapping[Msgid, Mapping[Lang, Msgstr]], /):
|
|
186
|
+
self._table = _reverse_mapping(table, nullable=False)
|
|
187
|
+
|
|
188
|
+
def __call__(self, lang: Lang) -> Translator:
|
|
189
|
+
return self._table[lang].__getitem__
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _reverse_mapping(d: Mapping[Msgid, Mapping[Lang, Msgstr]], *, nullable=True) -> dict[Lang, dict[Msgid, Msgstr]]:
|
|
193
|
+
msgids = tuple(d.keys())
|
|
194
|
+
langs = set(itertools.chain.from_iterable(d.values()))
|
|
195
|
+
if nullable:
|
|
196
|
+
return {
|
|
197
|
+
lang: {msgid: d[msgid].get(lang, None) for msgid in msgids}
|
|
198
|
+
for lang in langs
|
|
199
|
+
}
|
|
200
|
+
else:
|
|
201
|
+
return {
|
|
202
|
+
lang: {msgid: d[msgid].get(lang, msgid) for msgid in msgids}
|
|
203
|
+
for lang in langs
|
|
204
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._extract_msgids_from_string_literals import extract_msgids_from_string_literals
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
__all__ = ('extract_msgids_from_string_literals', )
|
|
2
2
|
|
|
3
|
-
'''xgettextは文字列literalからは抽出してくれないのでこのscriptを使って抽出する'''
|
|
4
|
-
|
|
5
3
|
import re
|
|
6
4
|
from typing import Iterator
|
|
7
5
|
|
|
@@ -16,9 +14,9 @@ def extract_string_literals(python_code: str) -> Iterator[str]:
|
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
PATTERN = re.compile(r"""
|
|
19
|
-
(^|\W)
|
|
17
|
+
(^|\W)_\("(.*?)"\) # _("")で括られた文字列
|
|
20
18
|
| # もしくは
|
|
21
|
-
(^|\W)
|
|
19
|
+
(^|\W)_\('(.*?)'\) # _('')で括られた文字列
|
|
22
20
|
""", re.VERBOSE | re.MULTILINE)
|
|
23
21
|
|
|
24
22
|
|
|
@@ -28,6 +26,16 @@ def extract_msgid(s:str) -> Iterator[str]:
|
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
def extract_msgids_from_string_literals(python_code: str) -> Iterator[str]:
|
|
29
|
+
'''
|
|
30
|
+
.. code-block::
|
|
31
|
+
|
|
32
|
+
msgids = extract_msgids_from_string_literals("""
|
|
33
|
+
Label:
|
|
34
|
+
font_name: _("AAA")
|
|
35
|
+
text: _("BBB")
|
|
36
|
+
""")
|
|
37
|
+
assert list(msgids) == ["AAA", "BBB"]
|
|
38
|
+
'''
|
|
31
39
|
return (
|
|
32
40
|
ls
|
|
33
41
|
for s in extract_string_literals(python_code)
|
|
@@ -35,18 +43,22 @@ def extract_msgids_from_string_literals(python_code: str) -> Iterator[str]:
|
|
|
35
43
|
)
|
|
36
44
|
|
|
37
45
|
|
|
38
|
-
def
|
|
46
|
+
def cli_main():
|
|
47
|
+
from textwrap import dedent
|
|
39
48
|
import sys
|
|
40
49
|
from pathlib import Path
|
|
41
50
|
from io import StringIO
|
|
51
|
+
|
|
52
|
+
if len(sys.argv) == 1:
|
|
53
|
+
print(dedent("""
|
|
54
|
+
Usage:
|
|
55
|
+
extract-msgids filename1.py filename2.py ...
|
|
56
|
+
"""),
|
|
57
|
+
file=sys.stderr)
|
|
58
|
+
return
|
|
42
59
|
output = StringIO()
|
|
43
|
-
write = output.write
|
|
44
60
|
for file in sys.argv[1:]:
|
|
45
61
|
for ls in extract_msgids_from_string_literals(Path(file).read_text(encoding='utf-8')):
|
|
46
62
|
print(ls, file=output)
|
|
47
63
|
print(output.getvalue())
|
|
48
64
|
output.close()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if __name__ == "__main__":
|
|
52
|
-
main()
|
kivy_garden_i18n-0.1.0/PKG-INFO
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: kivy-garden-i18n
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: i18n for Kivy
|
|
5
|
-
Home-page: https://github.com/gottadiveintopython/i18n
|
|
6
|
-
License: MIT
|
|
7
|
-
Keywords: kivy
|
|
8
|
-
Author: Nattōsai Mitō
|
|
9
|
-
Author-email: flow4re2c@gmail.com
|
|
10
|
-
Requires-Python: >=3.8,<4.0
|
|
11
|
-
Classifier: Development Status :: 4 - Beta
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Programming Language :: Python
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
-
Project-URL: Repository, https://github.com/gottadiveintopython/i18n
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# i18n
|
|
27
|
-
|
|
28
|
-
Kivyアプリの多言語化は面倒くさいです。
|
|
29
|
-
というのも只表示する文字列を切り替えればいいわけではないからです。
|
|
30
|
-
**Kivyは文字列の描画に必要なフォントを自動で選んでくれはしないので、アプリ側がKivyに教えてあげなければいけません。**
|
|
31
|
-
それを怠れば画面には文字に代わって豆腐が表示されてしまいます。
|
|
32
|
-
|
|
33
|
-
また多くのKivyアプリはフォントをアプリに詰め込むという方法を採っていますが、これも多言語化の際には問題となります。
|
|
34
|
-
フォントを幾つも詰め込んでアプリのサイズが数十MB膨れ上がればアプリの利用者は喜ばないでしょう。
|
|
35
|
-
|
|
36
|
-
そこでこのmoduleの出番です。
|
|
37
|
-
このmoduleは文字列を切り替える機能に加えて、**OSにinstall済のフォントの中から各言語用の物を自動で選んでくれます。**
|
|
38
|
-
なのでアプリにフォントを詰め込む必要はもうありません。
|
|
39
|
-
|
|
40
|
-
## 使い方
|
|
41
|
-
|
|
42
|
-
### フォントの検索
|
|
43
|
-
|
|
44
|
-
表示する文字列の切り替えとフォントの切り替えは別々の機能であり、どちらか片方だけ使うこともできます。
|
|
45
|
-
また多言語化には興味無いもののOSにinstall済のフォントを利用して少しでもアプリの容量を減らしたいという人も居るでしょう。
|
|
46
|
-
そういった人には以下のような使い方がおすすめです。
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```python
|
|
50
|
-
# OSにinstall済の日本語フォントの内 最初に見つけた物を利用する例
|
|
51
|
-
|
|
52
|
-
from kivy_garden.i18n.fontfinder import enum_fonts_from_lang
|
|
53
|
-
|
|
54
|
-
font = next(enum_fonts_from_lang('ja'), None)
|
|
55
|
-
if font is None:
|
|
56
|
-
print("日本語フォントが見つかりませんでした")
|
|
57
|
-
else:
|
|
58
|
-
print("日本語フォントが見つかりました:", font.name)
|
|
59
|
-
label.font_name = font.name
|
|
60
|
-
textinput.font_name = font.name
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### フォントの切り替え
|
|
64
|
-
|
|
65
|
-
そして多言語化させたい場合は以下のようにします。
|
|
66
|
-
|
|
67
|
-
```python
|
|
68
|
-
from kivy_garden.i18n.localizer import KXLocalizer
|
|
69
|
-
|
|
70
|
-
loc = KXLocalizer()
|
|
71
|
-
loc.lang = 'ja'
|
|
72
|
-
print(loc.font_name) # => 何かの日本語フォント名が出力される
|
|
73
|
-
loc.lang = 'zh'
|
|
74
|
-
print(loc.font_name) # => 何かの中文フォント名が出力される
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
重要なのが`KXLocalizer`が`EventDispatcher`な事で
|
|
78
|
-
|
|
79
|
-
```python
|
|
80
|
-
class KXLocalizer(EventDispatcher):
|
|
81
|
-
lang = StringProperty(...)
|
|
82
|
-
font_name = StringProperty(...)
|
|
83
|
-
_ = ObjectProperty(...)
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
bindingを利かせれば`lang`を切り替えた時に`Label`の`font_name`も自動で切り替えられます。
|
|
87
|
-
|
|
88
|
-
```python
|
|
89
|
-
from kivy.app import App
|
|
90
|
-
from kivy.lang import Builder
|
|
91
|
-
from kivy_garden.i18n.localizer import KXLocalizer
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
KV_CODE = '''
|
|
95
|
-
Label:
|
|
96
|
-
font_name: app.loc.font_name
|
|
97
|
-
'''
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class SampleApp(App):
|
|
101
|
-
def build(self):
|
|
102
|
-
self.loc = KXLocalizer()
|
|
103
|
-
return Builder.load_string(KV_CODE)
|
|
104
|
-
|
|
105
|
-
def on_start(self):
|
|
106
|
-
self.loc.lang = 'ja' # bindingによりLabelには自動で日本語フォントが適用される
|
|
107
|
-
self.loc.lang = 'ko' # bindingによりLabelには自動で韓国語フォントが適用される
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
また`KXLocalizer.install()`を用いる事で`#:import`無しで`KXLocalizer`をKv言語内で直接参照できます。
|
|
111
|
-
ただしglobal変数を書き換える行為なので使用は自己責任で。
|
|
112
|
-
|
|
113
|
-
```python
|
|
114
|
-
# 上のcodeど同等のcode
|
|
115
|
-
from kivy.app import App
|
|
116
|
-
from kivy.lang import Builder
|
|
117
|
-
from kivy_garden.i18n.localizer import KXLocalizer
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
KV_CODE = '''
|
|
121
|
-
Label:
|
|
122
|
-
font_name: l.font_name # import無しで参照 !!
|
|
123
|
-
'''
|
|
124
|
-
|
|
125
|
-
class SampleApp(App):
|
|
126
|
-
def build(self):
|
|
127
|
-
self.loc = KXLocalizer()
|
|
128
|
-
self.loc.install(name='l') # install
|
|
129
|
-
return Builder.load_string(KV_CODE)
|
|
130
|
-
|
|
131
|
-
def on_start(self):
|
|
132
|
-
self.loc.lang = 'ja'
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### 翻訳(文字列の切り替え)
|
|
136
|
-
|
|
137
|
-
そして肝心の翻訳ですが、これは`KXLocalizer`に翻訳者(Translator)を与える事で実現できます。
|
|
138
|
-
現在ある翻訳者は
|
|
139
|
-
|
|
140
|
-
- `gettext`を利用する`GettextTranslator`と
|
|
141
|
-
- 辞書を用いた単純な仕組みの`DictBasedTranslator`
|
|
142
|
-
|
|
143
|
-
の二種で、ここでは`DictBasedTranslator`を用いた方法を解説します。
|
|
144
|
-
`GettextTranslator`に関しては[gettext_example](https://github.com/gottadiveintopython/i18n/blob/main/examples/gettext_example.py)を参照して下さい。
|
|
145
|
-
|
|
146
|
-
まず必要となるのは以下のような辞書型の翻訳表です。
|
|
147
|
-
|
|
148
|
-
```python
|
|
149
|
-
翻訳表 = {
|
|
150
|
-
'tiger': {
|
|
151
|
-
'zh': '老虎',
|
|
152
|
-
'ja': '虎',
|
|
153
|
-
'en': 'tiger',
|
|
154
|
-
},
|
|
155
|
-
'apple': {
|
|
156
|
-
'zh': '蘋果',
|
|
157
|
-
'ja': 'りんご',
|
|
158
|
-
'en': 'apple',
|
|
159
|
-
},
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
pythonの辞書literalは書きづらいので実際にはYAML形式で翻訳表を作り、それをpythonの辞書に変換した方が良いかもしれません。
|
|
164
|
-
|
|
165
|
-
```yaml
|
|
166
|
-
tiger:
|
|
167
|
-
zh: 老虎
|
|
168
|
-
ja: 虎
|
|
169
|
-
en: tiger
|
|
170
|
-
apple:
|
|
171
|
-
zh: 蘋果
|
|
172
|
-
ja: りんご
|
|
173
|
-
en: apple
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
```python
|
|
177
|
-
# pip install pyyaml
|
|
178
|
-
import yaml
|
|
179
|
-
翻訳表 = yaml.safe_load(YAML文字列)
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
翻訳表ができたら後は以下のようにして`KXLocalizer`に与えるだけです。
|
|
183
|
-
|
|
184
|
-
```python
|
|
185
|
-
from kivy_garden.i18n.localizer import KXLocalizer, DictBasedTranslator
|
|
186
|
-
loc = KXLocalizer(translator=DictBasedTranslator(翻訳表))
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
これで既に`loc.lang`(現在の言語)に基づいて適切な翻訳結果が得られるようになっています。
|
|
190
|
-
|
|
191
|
-
```python
|
|
192
|
-
loc.lang = 'ja'
|
|
193
|
-
print(loc._("tiger")) # => 虎
|
|
194
|
-
print(loc._("apple")) # => りんご
|
|
195
|
-
print(loc.font_name) # => 何かの日本語フォント名
|
|
196
|
-
loc.lang = 'zh'
|
|
197
|
-
print(loc._("tiger")) # => 老虎
|
|
198
|
-
print(loc._("apple")) # => 蘋果
|
|
199
|
-
print(loc.font_name) # => 何かの中文フォント名
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
そして当然bindingを利かせてあげれば`loc.lang`(現在の言語)に連動して`Label`の`text`と`font_name`が自動で更新されます。
|
|
203
|
-
|
|
204
|
-
```python
|
|
205
|
-
from kivy.app import App
|
|
206
|
-
from kivy.lang import Builder
|
|
207
|
-
from kivy_garden.i18n.localizer import KXLocalizer, DictBasedTranslator
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
KV_CODE = '''
|
|
211
|
-
Label:
|
|
212
|
-
font_name: l.font_name
|
|
213
|
-
text: l._("apple")
|
|
214
|
-
'''
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
class SampleApp(App):
|
|
218
|
-
def build(self):
|
|
219
|
-
self.loc = KXLocalizer(translator=DictBasedTranslator(翻訳表))
|
|
220
|
-
self.loc.install(name='l')
|
|
221
|
-
return Builder.load_string(KV_CODE)
|
|
222
|
-
|
|
223
|
-
def on_start(self):
|
|
224
|
-
# Labelのfont_nameに日本語フォントが、textには りんご が入る
|
|
225
|
-
self.loc.lang = 'ja'
|
|
226
|
-
|
|
227
|
-
# Labelのfont_nameに英文フォントが、textには apple が入る
|
|
228
|
-
self.loc.lang = 'en'
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
このように`EventDispacther`のbindingの仕組みを利用すると**アプリの実行中に表示言語を自由に切り替えられるのがこのmoduleの強みとなります**。
|
|
232
|
-
もちろん言語切替時にアプリの再起動を求めるつもりなのであれば無理にbindingを利かせる必要はありません。
|
|
233
|
-
|
|
234
|
-
以上がこのmoduleの基本的な使い方となります。
|
|
235
|
-
|
|
236
|
-
## 小道具
|
|
237
|
-
|
|
238
|
-
`xgettext`は翻訳の必要性のある文字列をpythonソース中から抜き出してくれる素晴らしい道具ですが、
|
|
239
|
-
文字列literalの中までは見てくれないという欠点があります。
|
|
240
|
-
すなわち以下のcodeにおいて
|
|
241
|
-
|
|
242
|
-
```python
|
|
243
|
-
KV_CODE = '''
|
|
244
|
-
BoxLayout:
|
|
245
|
-
Label:
|
|
246
|
-
text: _("ABC")
|
|
247
|
-
Label:
|
|
248
|
-
text: _("DEF")
|
|
249
|
-
'''
|
|
250
|
-
|
|
251
|
-
_("123")
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
`123`は抜き出してくれても`ABC`と`DEF`は抜き出してくれません。
|
|
255
|
-
というわけでそういったことをしてくれる物を作りました。
|
|
256
|
-
|
|
257
|
-
```
|
|
258
|
-
python -m kivy_gardem.i18n.extract_msgids_from_string_literals 上記のpythonファイル > ./output.py
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
```python
|
|
262
|
-
# output.pyの中身
|
|
263
|
-
_("ABC")
|
|
264
|
-
_("DEF")
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
このように文字列literal内の翻訳対象文字列をその外に抜き出してくれるので、
|
|
268
|
-
それを`xgettext`に喰わせてあげれば取りこぼさずに済みます。
|
|
269
|
-
|
kivy_garden_i18n-0.1.0/README.md
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
# i18n
|
|
2
|
-
|
|
3
|
-
Kivyアプリの多言語化は面倒くさいです。
|
|
4
|
-
というのも只表示する文字列を切り替えればいいわけではないからです。
|
|
5
|
-
**Kivyは文字列の描画に必要なフォントを自動で選んでくれはしないので、アプリ側がKivyに教えてあげなければいけません。**
|
|
6
|
-
それを怠れば画面には文字に代わって豆腐が表示されてしまいます。
|
|
7
|
-
|
|
8
|
-
また多くのKivyアプリはフォントをアプリに詰め込むという方法を採っていますが、これも多言語化の際には問題となります。
|
|
9
|
-
フォントを幾つも詰め込んでアプリのサイズが数十MB膨れ上がればアプリの利用者は喜ばないでしょう。
|
|
10
|
-
|
|
11
|
-
そこでこのmoduleの出番です。
|
|
12
|
-
このmoduleは文字列を切り替える機能に加えて、**OSにinstall済のフォントの中から各言語用の物を自動で選んでくれます。**
|
|
13
|
-
なのでアプリにフォントを詰め込む必要はもうありません。
|
|
14
|
-
|
|
15
|
-
## 使い方
|
|
16
|
-
|
|
17
|
-
### フォントの検索
|
|
18
|
-
|
|
19
|
-
表示する文字列の切り替えとフォントの切り替えは別々の機能であり、どちらか片方だけ使うこともできます。
|
|
20
|
-
また多言語化には興味無いもののOSにinstall済のフォントを利用して少しでもアプリの容量を減らしたいという人も居るでしょう。
|
|
21
|
-
そういった人には以下のような使い方がおすすめです。
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```python
|
|
25
|
-
# OSにinstall済の日本語フォントの内 最初に見つけた物を利用する例
|
|
26
|
-
|
|
27
|
-
from kivy_garden.i18n.fontfinder import enum_fonts_from_lang
|
|
28
|
-
|
|
29
|
-
font = next(enum_fonts_from_lang('ja'), None)
|
|
30
|
-
if font is None:
|
|
31
|
-
print("日本語フォントが見つかりませんでした")
|
|
32
|
-
else:
|
|
33
|
-
print("日本語フォントが見つかりました:", font.name)
|
|
34
|
-
label.font_name = font.name
|
|
35
|
-
textinput.font_name = font.name
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### フォントの切り替え
|
|
39
|
-
|
|
40
|
-
そして多言語化させたい場合は以下のようにします。
|
|
41
|
-
|
|
42
|
-
```python
|
|
43
|
-
from kivy_garden.i18n.localizer import KXLocalizer
|
|
44
|
-
|
|
45
|
-
loc = KXLocalizer()
|
|
46
|
-
loc.lang = 'ja'
|
|
47
|
-
print(loc.font_name) # => 何かの日本語フォント名が出力される
|
|
48
|
-
loc.lang = 'zh'
|
|
49
|
-
print(loc.font_name) # => 何かの中文フォント名が出力される
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
重要なのが`KXLocalizer`が`EventDispatcher`な事で
|
|
53
|
-
|
|
54
|
-
```python
|
|
55
|
-
class KXLocalizer(EventDispatcher):
|
|
56
|
-
lang = StringProperty(...)
|
|
57
|
-
font_name = StringProperty(...)
|
|
58
|
-
_ = ObjectProperty(...)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
bindingを利かせれば`lang`を切り替えた時に`Label`の`font_name`も自動で切り替えられます。
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
from kivy.app import App
|
|
65
|
-
from kivy.lang import Builder
|
|
66
|
-
from kivy_garden.i18n.localizer import KXLocalizer
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
KV_CODE = '''
|
|
70
|
-
Label:
|
|
71
|
-
font_name: app.loc.font_name
|
|
72
|
-
'''
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class SampleApp(App):
|
|
76
|
-
def build(self):
|
|
77
|
-
self.loc = KXLocalizer()
|
|
78
|
-
return Builder.load_string(KV_CODE)
|
|
79
|
-
|
|
80
|
-
def on_start(self):
|
|
81
|
-
self.loc.lang = 'ja' # bindingによりLabelには自動で日本語フォントが適用される
|
|
82
|
-
self.loc.lang = 'ko' # bindingによりLabelには自動で韓国語フォントが適用される
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
また`KXLocalizer.install()`を用いる事で`#:import`無しで`KXLocalizer`をKv言語内で直接参照できます。
|
|
86
|
-
ただしglobal変数を書き換える行為なので使用は自己責任で。
|
|
87
|
-
|
|
88
|
-
```python
|
|
89
|
-
# 上のcodeど同等のcode
|
|
90
|
-
from kivy.app import App
|
|
91
|
-
from kivy.lang import Builder
|
|
92
|
-
from kivy_garden.i18n.localizer import KXLocalizer
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
KV_CODE = '''
|
|
96
|
-
Label:
|
|
97
|
-
font_name: l.font_name # import無しで参照 !!
|
|
98
|
-
'''
|
|
99
|
-
|
|
100
|
-
class SampleApp(App):
|
|
101
|
-
def build(self):
|
|
102
|
-
self.loc = KXLocalizer()
|
|
103
|
-
self.loc.install(name='l') # install
|
|
104
|
-
return Builder.load_string(KV_CODE)
|
|
105
|
-
|
|
106
|
-
def on_start(self):
|
|
107
|
-
self.loc.lang = 'ja'
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### 翻訳(文字列の切り替え)
|
|
111
|
-
|
|
112
|
-
そして肝心の翻訳ですが、これは`KXLocalizer`に翻訳者(Translator)を与える事で実現できます。
|
|
113
|
-
現在ある翻訳者は
|
|
114
|
-
|
|
115
|
-
- `gettext`を利用する`GettextTranslator`と
|
|
116
|
-
- 辞書を用いた単純な仕組みの`DictBasedTranslator`
|
|
117
|
-
|
|
118
|
-
の二種で、ここでは`DictBasedTranslator`を用いた方法を解説します。
|
|
119
|
-
`GettextTranslator`に関しては[gettext_example](https://github.com/gottadiveintopython/i18n/blob/main/examples/gettext_example.py)を参照して下さい。
|
|
120
|
-
|
|
121
|
-
まず必要となるのは以下のような辞書型の翻訳表です。
|
|
122
|
-
|
|
123
|
-
```python
|
|
124
|
-
翻訳表 = {
|
|
125
|
-
'tiger': {
|
|
126
|
-
'zh': '老虎',
|
|
127
|
-
'ja': '虎',
|
|
128
|
-
'en': 'tiger',
|
|
129
|
-
},
|
|
130
|
-
'apple': {
|
|
131
|
-
'zh': '蘋果',
|
|
132
|
-
'ja': 'りんご',
|
|
133
|
-
'en': 'apple',
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
pythonの辞書literalは書きづらいので実際にはYAML形式で翻訳表を作り、それをpythonの辞書に変換した方が良いかもしれません。
|
|
139
|
-
|
|
140
|
-
```yaml
|
|
141
|
-
tiger:
|
|
142
|
-
zh: 老虎
|
|
143
|
-
ja: 虎
|
|
144
|
-
en: tiger
|
|
145
|
-
apple:
|
|
146
|
-
zh: 蘋果
|
|
147
|
-
ja: りんご
|
|
148
|
-
en: apple
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
```python
|
|
152
|
-
# pip install pyyaml
|
|
153
|
-
import yaml
|
|
154
|
-
翻訳表 = yaml.safe_load(YAML文字列)
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
翻訳表ができたら後は以下のようにして`KXLocalizer`に与えるだけです。
|
|
158
|
-
|
|
159
|
-
```python
|
|
160
|
-
from kivy_garden.i18n.localizer import KXLocalizer, DictBasedTranslator
|
|
161
|
-
loc = KXLocalizer(translator=DictBasedTranslator(翻訳表))
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
これで既に`loc.lang`(現在の言語)に基づいて適切な翻訳結果が得られるようになっています。
|
|
165
|
-
|
|
166
|
-
```python
|
|
167
|
-
loc.lang = 'ja'
|
|
168
|
-
print(loc._("tiger")) # => 虎
|
|
169
|
-
print(loc._("apple")) # => りんご
|
|
170
|
-
print(loc.font_name) # => 何かの日本語フォント名
|
|
171
|
-
loc.lang = 'zh'
|
|
172
|
-
print(loc._("tiger")) # => 老虎
|
|
173
|
-
print(loc._("apple")) # => 蘋果
|
|
174
|
-
print(loc.font_name) # => 何かの中文フォント名
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
そして当然bindingを利かせてあげれば`loc.lang`(現在の言語)に連動して`Label`の`text`と`font_name`が自動で更新されます。
|
|
178
|
-
|
|
179
|
-
```python
|
|
180
|
-
from kivy.app import App
|
|
181
|
-
from kivy.lang import Builder
|
|
182
|
-
from kivy_garden.i18n.localizer import KXLocalizer, DictBasedTranslator
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
KV_CODE = '''
|
|
186
|
-
Label:
|
|
187
|
-
font_name: l.font_name
|
|
188
|
-
text: l._("apple")
|
|
189
|
-
'''
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
class SampleApp(App):
|
|
193
|
-
def build(self):
|
|
194
|
-
self.loc = KXLocalizer(translator=DictBasedTranslator(翻訳表))
|
|
195
|
-
self.loc.install(name='l')
|
|
196
|
-
return Builder.load_string(KV_CODE)
|
|
197
|
-
|
|
198
|
-
def on_start(self):
|
|
199
|
-
# Labelのfont_nameに日本語フォントが、textには りんご が入る
|
|
200
|
-
self.loc.lang = 'ja'
|
|
201
|
-
|
|
202
|
-
# Labelのfont_nameに英文フォントが、textには apple が入る
|
|
203
|
-
self.loc.lang = 'en'
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
このように`EventDispacther`のbindingの仕組みを利用すると**アプリの実行中に表示言語を自由に切り替えられるのがこのmoduleの強みとなります**。
|
|
207
|
-
もちろん言語切替時にアプリの再起動を求めるつもりなのであれば無理にbindingを利かせる必要はありません。
|
|
208
|
-
|
|
209
|
-
以上がこのmoduleの基本的な使い方となります。
|
|
210
|
-
|
|
211
|
-
## 小道具
|
|
212
|
-
|
|
213
|
-
`xgettext`は翻訳の必要性のある文字列をpythonソース中から抜き出してくれる素晴らしい道具ですが、
|
|
214
|
-
文字列literalの中までは見てくれないという欠点があります。
|
|
215
|
-
すなわち以下のcodeにおいて
|
|
216
|
-
|
|
217
|
-
```python
|
|
218
|
-
KV_CODE = '''
|
|
219
|
-
BoxLayout:
|
|
220
|
-
Label:
|
|
221
|
-
text: _("ABC")
|
|
222
|
-
Label:
|
|
223
|
-
text: _("DEF")
|
|
224
|
-
'''
|
|
225
|
-
|
|
226
|
-
_("123")
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
`123`は抜き出してくれても`ABC`と`DEF`は抜き出してくれません。
|
|
230
|
-
というわけでそういったことをしてくれる物を作りました。
|
|
231
|
-
|
|
232
|
-
```
|
|
233
|
-
python -m kivy_gardem.i18n.extract_msgids_from_string_literals 上記のpythonファイル > ./output.py
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
```python
|
|
237
|
-
# output.pyの中身
|
|
238
|
-
_("ABC")
|
|
239
|
-
_("DEF")
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
このように文字列literal内の翻訳対象文字列をその外に抜き出してくれるので、
|
|
243
|
-
それを`xgettext`に喰わせてあげれば取りこぼさずに済みます。
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
__all__ =(
|
|
2
|
-
'enum_all_fonts', 'get_all_fonts', 'enum_fonts_from_text', 'enum_fonts_from_lang',
|
|
3
|
-
)
|
|
4
|
-
|
|
5
|
-
from typing import Tuple, Iterator
|
|
6
|
-
from functools import lru_cache
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
SUFFIXES = {'.ttf', '.otf', '.ttc', }
|
|
10
|
-
EXCLUDES = {
|
|
11
|
-
# SDL2 text provider cannot render this font. Don't know about other providers.
|
|
12
|
-
'NotoColorEmoji.ttf',
|
|
13
|
-
|
|
14
|
-
# This doesn't contain English letters.
|
|
15
|
-
'DroidSansFallbackFull.ttf',
|
|
16
|
-
}
|
|
17
|
-
DISCRIMINANT = {
|
|
18
|
-
'ar': 'الجزيرةAB',
|
|
19
|
-
'hi': 'भारतAB',
|
|
20
|
-
'ja': (v := '経伝説あAB'),
|
|
21
|
-
'ja-JP': v,
|
|
22
|
-
'ja_JP': v,
|
|
23
|
-
'ko': (v := '안녕조AB'),
|
|
24
|
-
'ko-KR': v,
|
|
25
|
-
'ko_KR': v,
|
|
26
|
-
'zh-Hans': (v := '哪经传说AB'),
|
|
27
|
-
'zh_Hans': v,
|
|
28
|
-
'zh-CN': v,
|
|
29
|
-
'zh_CN': v,
|
|
30
|
-
'zh-SG': v,
|
|
31
|
-
'zh_SG': v,
|
|
32
|
-
'zh-Hant': (v := '哪經傳說AB'),
|
|
33
|
-
'zh_Hant': v,
|
|
34
|
-
'zh-TW': v,
|
|
35
|
-
'zh_TW': v,
|
|
36
|
-
'zh-HK': v,
|
|
37
|
-
'zh_HK': v,
|
|
38
|
-
'zh-MO': v,
|
|
39
|
-
'zh_MO': v,
|
|
40
|
-
'zh': '哪經傳說经传说AB',
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
def enum_all_fonts() -> Iterator[Path]:
|
|
44
|
-
'''Enumerates pre-installed fonts'''
|
|
45
|
-
from kivy.core.text import LabelBase
|
|
46
|
-
suffixes = SUFFIXES
|
|
47
|
-
excludes = EXCLUDES
|
|
48
|
-
for dir in LabelBase.get_system_fonts_dir():
|
|
49
|
-
for child in Path(dir).iterdir():
|
|
50
|
-
if child.suffix in suffixes and child.name not in excludes:
|
|
51
|
-
yield child
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@lru_cache(maxsize=1)
|
|
55
|
-
def get_all_fonts() -> Tuple[Path]:
|
|
56
|
-
'''Returns a tuple of pre-installed fonts. Caches the return-value.'''
|
|
57
|
-
return tuple(enum_all_fonts())
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def enum_fonts_from_text(text: str) -> Iterator[Path]:
|
|
61
|
-
'''Enumerates pre-installed fonts that are capable of rendering the given
|
|
62
|
-
``text``. The ``text`` must contain more than two characters without
|
|
63
|
-
duplication.
|
|
64
|
-
|
|
65
|
-
.. note::
|
|
66
|
-
|
|
67
|
-
The longer the ``text`` is, the more accurate the result would be,
|
|
68
|
-
but the more time it'll take.
|
|
69
|
-
'''
|
|
70
|
-
from kivy.core.text import Label as CoreLabel
|
|
71
|
-
if len(text) < 3:
|
|
72
|
-
raise ValueError(f"'text' must contain more than two characters")
|
|
73
|
-
if len(set(text)) < len(text):
|
|
74
|
-
raise ValueError(f"'text' should not contain duplicated characters")
|
|
75
|
-
label = CoreLabel()
|
|
76
|
-
label._size = (16, 16)
|
|
77
|
-
for path in get_all_fonts():
|
|
78
|
-
label.options['font_name'] = str(path)
|
|
79
|
-
label.resolve_font_name()
|
|
80
|
-
pixels_set = set()
|
|
81
|
-
for i, c in enumerate(text, start=1):
|
|
82
|
-
label.text = c
|
|
83
|
-
label._render_begin()
|
|
84
|
-
label._render_text(c, 0, 0)
|
|
85
|
-
pixels_set.add(label._render_end().data)
|
|
86
|
-
if len(pixels_set) != i:
|
|
87
|
-
break
|
|
88
|
-
else:
|
|
89
|
-
yield path
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def enum_fonts_from_lang(lang: str) -> Iterator[Path]:
|
|
93
|
-
'''Enumerates pre-installed fonts supporting the given language. '''
|
|
94
|
-
try:
|
|
95
|
-
text = DISCRIMINANT[lang]
|
|
96
|
-
except KeyError:
|
|
97
|
-
return iter('')
|
|
98
|
-
return enum_fonts_from_text(text)
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
__all__ = ('KXLocalizer', 'GettextTranslator', 'DictBasedTranslator', )
|
|
2
|
-
from typing import Callable, Dict
|
|
3
|
-
|
|
4
|
-
from kivy.properties import StringProperty, ObjectProperty
|
|
5
|
-
from kivy.event import EventDispatcher
|
|
6
|
-
from kivy.uix.label import Label
|
|
7
|
-
|
|
8
|
-
# type hints
|
|
9
|
-
Lang = str
|
|
10
|
-
Msgid = str
|
|
11
|
-
Fontname = str
|
|
12
|
-
FuncTranslate = Callable[[Msgid], str]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class KXLocalizer(EventDispatcher):
|
|
16
|
-
lang = StringProperty()
|
|
17
|
-
|
|
18
|
-
_ = ObjectProperty(lambda v: v)
|
|
19
|
-
'''(read-only) translator'''
|
|
20
|
-
|
|
21
|
-
font_name = StringProperty(Label.font_name.defaultvalue)
|
|
22
|
-
'''(read-only)'''
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self, *,
|
|
26
|
-
lang: Lang='en',
|
|
27
|
-
translator: Callable[[Lang], FuncTranslate]=None,
|
|
28
|
-
fontfinder: Callable[[Lang], Fontname]=None,
|
|
29
|
-
):
|
|
30
|
-
if translator is None:
|
|
31
|
-
translator = lambda lang: lambda msgid: msgid
|
|
32
|
-
if fontfinder is None:
|
|
33
|
-
fontfinder = DefaultFontFinder()
|
|
34
|
-
self._translator = translator
|
|
35
|
-
self._fontfinder = fontfinder
|
|
36
|
-
super().__init__(lang=lang)
|
|
37
|
-
|
|
38
|
-
def install(self, *, name):
|
|
39
|
-
from kivy.lang import global_idmap
|
|
40
|
-
global_idmap[name] = self
|
|
41
|
-
|
|
42
|
-
def on_lang(self, __, lang):
|
|
43
|
-
self.font_name = self._fontfinder(lang)
|
|
44
|
-
self._ = self._translator(lang)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class DefaultFontFinder:
|
|
48
|
-
PRESET = {
|
|
49
|
-
'en': (v := 'Roboto'),
|
|
50
|
-
'fr': v,
|
|
51
|
-
'ru': v,
|
|
52
|
-
}
|
|
53
|
-
def __init__(self):
|
|
54
|
-
self._font_names = self.PRESET.copy()
|
|
55
|
-
|
|
56
|
-
def __call__(self, lang: Lang) -> Fontname:
|
|
57
|
-
try:
|
|
58
|
-
return self._font_names[lang]
|
|
59
|
-
except KeyError:
|
|
60
|
-
pass
|
|
61
|
-
from .fontfinder import enum_fonts_from_lang
|
|
62
|
-
try:
|
|
63
|
-
font_name = next(enum_fonts_from_lang(lang)).name
|
|
64
|
-
except StopIteration:
|
|
65
|
-
from kivy.logger import Logger
|
|
66
|
-
Logger.warning(
|
|
67
|
-
f"kivy_garden.i18n: Couldn't find a font for lang'{lang}'. "
|
|
68
|
-
"Use Roboto as a fallback.")
|
|
69
|
-
font_name = 'Roboto'
|
|
70
|
-
self._font_names[lang] = font_name
|
|
71
|
-
return font_name
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class GettextTranslator:
|
|
75
|
-
def __init__(self, domain, localedir):
|
|
76
|
-
self.domain = domain
|
|
77
|
-
self.localedir = localedir
|
|
78
|
-
|
|
79
|
-
def __call__(self, lang: Lang) -> FuncTranslate:
|
|
80
|
-
from gettext import translation
|
|
81
|
-
return translation(domain=self.domain, localedir=self.localedir, languages=(lang, )).gettext
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class DictBasedTranslator:
|
|
85
|
-
def __init__(self, table: Dict[Msgid, Dict[Lang, str]]):
|
|
86
|
-
self._table = table
|
|
87
|
-
|
|
88
|
-
def __call__(self, lang: Lang) -> FuncTranslate:
|
|
89
|
-
def func(msgid):
|
|
90
|
-
try:
|
|
91
|
-
return self._table[msgid][lang]
|
|
92
|
-
except KeyError:
|
|
93
|
-
return msgid
|
|
94
|
-
return func
|
|
File without changes
|