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.

@@ -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 = "kivy_garden.i18n"
3
- version = "0.1.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 = 'README.md'
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.8"
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
- kivy = "^2.2.1"
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>=1.0.0"]
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 main():
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()
@@ -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
-
@@ -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