pyxpo-localization 0.1.0a1__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.
- pyxpo_localization-0.1.0a1/LICENSE +21 -0
- pyxpo_localization-0.1.0a1/PKG-INFO +85 -0
- pyxpo_localization-0.1.0a1/README.md +66 -0
- pyxpo_localization-0.1.0a1/pyproject.toml +42 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization/__init__.py +169 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization/android/src/main/AndroidManifest.xml +1 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization/android/src/main/java/dev/pyxpo/modules/localization/LocalizationModule.kt +89 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization/ios/LocalizationModule.swift +98 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization/py.typed +0 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization/pyxpo-module.json +14 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization.egg-info/PKG-INFO +85 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization.egg-info/SOURCES.txt +15 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization.egg-info/dependency_links.txt +1 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization.egg-info/entry_points.txt +2 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization.egg-info/requires.txt +1 -0
- pyxpo_localization-0.1.0a1/pyxpo_localization.egg-info/top_level.txt +1 -0
- pyxpo_localization-0.1.0a1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vishal Chaubey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyxpo-localization
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: The device's locales and calendars: the expo-localization API for pyxpo.
|
|
5
|
+
Author: Vishal Chaubey
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pyxpo/pyxpo/tree/main/packages/localization
|
|
8
|
+
Project-URL: Issues, https://github.com/pyxpo/pyxpo/issues
|
|
9
|
+
Keywords: pyxpo,pyxpo-module,mobile,android,ios
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pyxpo<0.2,>=0.1.0a2
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# pyxpo-localization
|
|
21
|
+
|
|
22
|
+
The user's languages, region formats and calendar settings for [pyxpo](https://github.com/pyxpo/pyxpo)
|
|
23
|
+
apps. The API follows [expo-localization](https://docs.expo.dev/versions/latest/sdk/localization/).
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install pyxpo-localization
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
[tool.pyxpo]
|
|
31
|
+
modules = ["localization"]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import pyxpo_localization as Localization
|
|
36
|
+
|
|
37
|
+
locale = Localization.get_locales()[0]
|
|
38
|
+
print(locale.language_tag, locale.region_code, locale.currency_code) # en-IN IN INR
|
|
39
|
+
cal = Localization.get_calendars()[0]
|
|
40
|
+
print(cal.time_zone, cal.uses_24_hour_clock, cal.first_weekday) # Asia/Kolkata False 1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every name also has its expo-localization spelling (`Localization.getLocales`, `Localization.useCalendars`).
|
|
44
|
+
Records accept Expo's camelCase keys too: `locale["languageTag"]`, and `locale.to_dict()` gives the
|
|
45
|
+
camelCase dict.
|
|
46
|
+
|
|
47
|
+
The values are read once when the app starts. On the desktop they come from `LC_ALL` / `LC_MESSAGES` /
|
|
48
|
+
`LANG` and the system time zone, and the fields the desktop cannot tell (currency, separators) are `None`.
|
|
49
|
+
|
|
50
|
+
## Functions
|
|
51
|
+
|
|
52
|
+
| Function | Returns |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `get_locales()` | `list[Locale]`, the preferred languages, most preferred first. Never empty. |
|
|
55
|
+
| `get_calendars()` | `list[Calendar]`, the user's calendars, preferred first. Never empty. |
|
|
56
|
+
| `use_locales()`, `use_calendars()` | The same, for use in components. |
|
|
57
|
+
|
|
58
|
+
## `Locale`
|
|
59
|
+
|
|
60
|
+
| Field | Example | Notes |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `language_tag` | `"en-IN"` | BCP 47 tag. |
|
|
63
|
+
| `language_code` | `"en"` | |
|
|
64
|
+
| `language_script_code` | `"Hant"` | `None` when the tag has no script. |
|
|
65
|
+
| `region_code` | `"IN"` | iOS: the region setting. Android: the locale's region. |
|
|
66
|
+
| `language_region_code` | `"IN"` | The region in the language tag. |
|
|
67
|
+
| `currency_code`, `currency_symbol` | `"INR"`, `"₹"` | iOS: from the region setting. |
|
|
68
|
+
| `language_currency_code`, `language_currency_symbol` | `"INR"`, `"₹"` | From the language tag's region. |
|
|
69
|
+
| `decimal_separator`, `digit_grouping_separator` | `"."`, `","` | |
|
|
70
|
+
| `text_direction` | `"ltr"` | `"ltr"` or `"rtl"`. |
|
|
71
|
+
| `measurement_system` | `"metric"` | `"metric"`, `"us"` or `"uk"`. |
|
|
72
|
+
| `temperature_unit` | `"celsius"` | `"celsius"` or `"fahrenheit"`. Follows the user's setting on iOS 16+ and Android 14+. |
|
|
73
|
+
|
|
74
|
+
## `Calendar`
|
|
75
|
+
|
|
76
|
+
| Field | Example | Notes |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `calendar` | `"gregory"` | CLDR name, as in JavaScript `Intl`. |
|
|
79
|
+
| `time_zone` | `"Asia/Kolkata"` | IANA id. |
|
|
80
|
+
| `uses_24_hour_clock` | `False` | |
|
|
81
|
+
| `first_weekday` | `1` | 1 is Sunday, 7 is Saturday. |
|
|
82
|
+
|
|
83
|
+
## Platforms
|
|
84
|
+
|
|
85
|
+
iOS 15+, Android 7 (API 24)+. No permissions.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# pyxpo-localization
|
|
2
|
+
|
|
3
|
+
The user's languages, region formats and calendar settings for [pyxpo](https://github.com/pyxpo/pyxpo)
|
|
4
|
+
apps. The API follows [expo-localization](https://docs.expo.dev/versions/latest/sdk/localization/).
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pip install pyxpo-localization
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```toml
|
|
11
|
+
[tool.pyxpo]
|
|
12
|
+
modules = ["localization"]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import pyxpo_localization as Localization
|
|
17
|
+
|
|
18
|
+
locale = Localization.get_locales()[0]
|
|
19
|
+
print(locale.language_tag, locale.region_code, locale.currency_code) # en-IN IN INR
|
|
20
|
+
cal = Localization.get_calendars()[0]
|
|
21
|
+
print(cal.time_zone, cal.uses_24_hour_clock, cal.first_weekday) # Asia/Kolkata False 1
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every name also has its expo-localization spelling (`Localization.getLocales`, `Localization.useCalendars`).
|
|
25
|
+
Records accept Expo's camelCase keys too: `locale["languageTag"]`, and `locale.to_dict()` gives the
|
|
26
|
+
camelCase dict.
|
|
27
|
+
|
|
28
|
+
The values are read once when the app starts. On the desktop they come from `LC_ALL` / `LC_MESSAGES` /
|
|
29
|
+
`LANG` and the system time zone, and the fields the desktop cannot tell (currency, separators) are `None`.
|
|
30
|
+
|
|
31
|
+
## Functions
|
|
32
|
+
|
|
33
|
+
| Function | Returns |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `get_locales()` | `list[Locale]`, the preferred languages, most preferred first. Never empty. |
|
|
36
|
+
| `get_calendars()` | `list[Calendar]`, the user's calendars, preferred first. Never empty. |
|
|
37
|
+
| `use_locales()`, `use_calendars()` | The same, for use in components. |
|
|
38
|
+
|
|
39
|
+
## `Locale`
|
|
40
|
+
|
|
41
|
+
| Field | Example | Notes |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| `language_tag` | `"en-IN"` | BCP 47 tag. |
|
|
44
|
+
| `language_code` | `"en"` | |
|
|
45
|
+
| `language_script_code` | `"Hant"` | `None` when the tag has no script. |
|
|
46
|
+
| `region_code` | `"IN"` | iOS: the region setting. Android: the locale's region. |
|
|
47
|
+
| `language_region_code` | `"IN"` | The region in the language tag. |
|
|
48
|
+
| `currency_code`, `currency_symbol` | `"INR"`, `"₹"` | iOS: from the region setting. |
|
|
49
|
+
| `language_currency_code`, `language_currency_symbol` | `"INR"`, `"₹"` | From the language tag's region. |
|
|
50
|
+
| `decimal_separator`, `digit_grouping_separator` | `"."`, `","` | |
|
|
51
|
+
| `text_direction` | `"ltr"` | `"ltr"` or `"rtl"`. |
|
|
52
|
+
| `measurement_system` | `"metric"` | `"metric"`, `"us"` or `"uk"`. |
|
|
53
|
+
| `temperature_unit` | `"celsius"` | `"celsius"` or `"fahrenheit"`. Follows the user's setting on iOS 16+ and Android 14+. |
|
|
54
|
+
|
|
55
|
+
## `Calendar`
|
|
56
|
+
|
|
57
|
+
| Field | Example | Notes |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `calendar` | `"gregory"` | CLDR name, as in JavaScript `Intl`. |
|
|
60
|
+
| `time_zone` | `"Asia/Kolkata"` | IANA id. |
|
|
61
|
+
| `uses_24_hour_clock` | `False` | |
|
|
62
|
+
| `first_weekday` | `1` | 1 is Sunday, 7 is Saturday. |
|
|
63
|
+
|
|
64
|
+
## Platforms
|
|
65
|
+
|
|
66
|
+
iOS 15+, Android 7 (API 24)+. No permissions.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyxpo-localization"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "The device's locales and calendars: the expo-localization API for pyxpo."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Vishal Chaubey" }]
|
|
13
|
+
keywords = ["pyxpo", "pyxpo-module", "mobile", "android", "ios"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development :: User Interfaces",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.9"
|
|
21
|
+
dependencies = ["pyxpo>=0.1.0a2,<0.2"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/pyxpo/pyxpo/tree/main/packages/localization"
|
|
25
|
+
Issues = "https://github.com/pyxpo/pyxpo/issues"
|
|
26
|
+
|
|
27
|
+
# How `pyxpo build` finds this module: the entry point name is the module name apps list in
|
|
28
|
+
# `[tool.pyxpo] modules`, its value the import package holding pyxpo-module.json.
|
|
29
|
+
[project.entry-points."pyxpo.modules"]
|
|
30
|
+
localization = "pyxpo_localization"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["pyxpo_localization"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.dynamic]
|
|
36
|
+
version = { attr = "pyxpo_localization.__version__" }
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
pyxpo_localization = ["pyxpo-module.json", "py.typed", "ios/**/*", "android/**/*"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.exclude-package-data]
|
|
42
|
+
"*" = ["__pycache__", "*.pyc", ".DS_Store"]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""The device's language, region and calendar settings: the expo-localization API.
|
|
2
|
+
|
|
3
|
+
import pyxpo_localization as Localization
|
|
4
|
+
|
|
5
|
+
locale = Localization.get_locales()[0] # or Localization.getLocales()
|
|
6
|
+
locale.language_code, locale.region_code # "en", "US"
|
|
7
|
+
locale["languageTag"] # camelCase keys work too: "en-US"
|
|
8
|
+
cal = Localization.get_calendars()[0]
|
|
9
|
+
cal.time_zone, cal.uses_24_hour_clock # "Europe/Berlin", True
|
|
10
|
+
|
|
11
|
+
The values are read once when the app starts. On the desktop they come from the `LANG`/`LC_*` environment
|
|
12
|
+
and the system time zone; fields the desktop cannot tell are None.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, fields
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
from pyxpo.module_api import constants, platform, require_linked
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0a1"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Record:
|
|
27
|
+
"""Attribute access in snake_case, item access in Expo's camelCase."""
|
|
28
|
+
_CAMEL: Dict[str, str] = {}
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, key: str) -> Any:
|
|
31
|
+
name = self._CAMEL.get(key, key)
|
|
32
|
+
if name not in {f.name for f in fields(self)}: # type: ignore[arg-type]
|
|
33
|
+
raise KeyError(key)
|
|
34
|
+
return getattr(self, name)
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
37
|
+
"""The record with Expo's camelCase keys."""
|
|
38
|
+
return {camel: getattr(self, snake) for camel, snake in self._CAMEL.items()}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Locale(_Record):
|
|
43
|
+
"""One preferred language, with the formatting rules of the user's region."""
|
|
44
|
+
language_tag: str
|
|
45
|
+
language_code: Optional[str] = None
|
|
46
|
+
language_script_code: Optional[str] = None
|
|
47
|
+
region_code: Optional[str] = None
|
|
48
|
+
language_region_code: Optional[str] = None
|
|
49
|
+
currency_code: Optional[str] = None
|
|
50
|
+
currency_symbol: Optional[str] = None
|
|
51
|
+
language_currency_code: Optional[str] = None
|
|
52
|
+
language_currency_symbol: Optional[str] = None
|
|
53
|
+
decimal_separator: Optional[str] = None
|
|
54
|
+
digit_grouping_separator: Optional[str] = None
|
|
55
|
+
text_direction: Optional[str] = None
|
|
56
|
+
measurement_system: Optional[str] = None
|
|
57
|
+
temperature_unit: Optional[str] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Calendar(_Record):
|
|
62
|
+
"""One calendar the user has set: its type, time zone, clock style and first weekday."""
|
|
63
|
+
calendar: Optional[str] = None
|
|
64
|
+
time_zone: Optional[str] = None
|
|
65
|
+
uses_24_hour_clock: Optional[bool] = None
|
|
66
|
+
first_weekday: Optional[int] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Locale._CAMEL = {
|
|
70
|
+
"languageTag": "language_tag", "languageCode": "language_code", "languageScriptCode": "language_script_code",
|
|
71
|
+
"regionCode": "region_code", "languageRegionCode": "language_region_code", "currencyCode": "currency_code",
|
|
72
|
+
"currencySymbol": "currency_symbol", "languageCurrencyCode": "language_currency_code",
|
|
73
|
+
"languageCurrencySymbol": "language_currency_symbol", "decimalSeparator": "decimal_separator",
|
|
74
|
+
"digitGroupingSeparator": "digit_grouping_separator", "textDirection": "text_direction",
|
|
75
|
+
"measurementSystem": "measurement_system", "temperatureUnit": "temperature_unit",
|
|
76
|
+
}
|
|
77
|
+
Calendar._CAMEL = {
|
|
78
|
+
"calendar": "calendar", "timeZone": "time_zone", "uses24hourClock": "uses_24_hour_clock",
|
|
79
|
+
"firstWeekday": "first_weekday",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Regions that use Fahrenheit and US units (CLDR). The desktop has no other source for these.
|
|
83
|
+
_FAHRENHEIT = {"US", "BS", "BZ", "KY", "PR", "PW", "LR", "FM", "MH"}
|
|
84
|
+
_US_UNITS = {"US", "LR", "MM"}
|
|
85
|
+
_RTL = {"ar", "arc", "ckb", "dv", "fa", "ha", "he", "iw", "khw", "ks", "ku", "ps", "sd", "ur", "yi"}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _from_native(cls: Any, raw: Dict[str, Any]) -> Any:
|
|
89
|
+
return cls(**{snake: raw.get(camel) for camel, snake in cls._CAMEL.items()})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _native(key: str) -> Optional[List[Dict[str, Any]]]:
|
|
93
|
+
if platform() == "desktop":
|
|
94
|
+
return None
|
|
95
|
+
require_linked("Localization", "localization", package="pyxpo-localization")
|
|
96
|
+
return list(constants("Localization").get(key) or [])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _desktop_locale() -> Locale:
|
|
100
|
+
raw = ""
|
|
101
|
+
for var in ("LC_ALL", "LC_MESSAGES", "LANG"):
|
|
102
|
+
raw = os.environ.get(var, "")
|
|
103
|
+
if raw:
|
|
104
|
+
break
|
|
105
|
+
raw = raw.split(".")[0].split("@")[0]
|
|
106
|
+
if not raw or raw in ("C", "POSIX"):
|
|
107
|
+
raw = "en_US"
|
|
108
|
+
parts = raw.replace("-", "_").split("_")
|
|
109
|
+
lang = parts[0].lower()
|
|
110
|
+
script = next((p.title() for p in parts[1:] if len(p) == 4), None)
|
|
111
|
+
region = next((p.upper() for p in parts[1:] if (len(p) == 2 and p.isalpha()) or (len(p) == 3 and p.isdigit())), None)
|
|
112
|
+
tag = "-".join(p for p in (lang, script, region) if p)
|
|
113
|
+
return Locale(
|
|
114
|
+
language_tag=tag, language_code=lang, language_script_code=script, region_code=region,
|
|
115
|
+
language_region_code=region, text_direction="rtl" if lang in _RTL else "ltr",
|
|
116
|
+
measurement_system=None if region is None else "us" if region in _US_UNITS else "uk" if region == "GB" else "metric",
|
|
117
|
+
temperature_unit=None if region is None else "fahrenheit" if region in _FAHRENHEIT else "celsius",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _desktop_time_zone() -> Optional[str]:
|
|
122
|
+
tz = os.environ.get("TZ", "").lstrip(":")
|
|
123
|
+
if "/" in tz and not tz.startswith("/"):
|
|
124
|
+
return tz
|
|
125
|
+
try:
|
|
126
|
+
target = os.path.realpath("/etc/localtime")
|
|
127
|
+
except OSError:
|
|
128
|
+
return None
|
|
129
|
+
marker = "zoneinfo/"
|
|
130
|
+
return target.split(marker, 1)[1] if marker in target else (time.tzname[0] or None)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_locales() -> List[Locale]:
|
|
134
|
+
"""The user's preferred languages, most preferred first. Always at least one entry."""
|
|
135
|
+
raw = _native("locales")
|
|
136
|
+
if raw is None:
|
|
137
|
+
return [_desktop_locale()]
|
|
138
|
+
out = [_from_native(Locale, r) for r in raw]
|
|
139
|
+
return out or [Locale(language_tag="en-US", language_code="en", region_code="US", language_region_code="US")]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_calendars() -> List[Calendar]:
|
|
143
|
+
"""The user's calendars, preferred first. Always at least one entry."""
|
|
144
|
+
raw = _native("calendars")
|
|
145
|
+
if raw is None:
|
|
146
|
+
return [Calendar(calendar="gregory", time_zone=_desktop_time_zone())]
|
|
147
|
+
out = [_from_native(Calendar, r) for r in raw]
|
|
148
|
+
return out or [Calendar(calendar="gregory")]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def use_locales() -> List[Locale]:
|
|
152
|
+
"""Hook form of `get_locales()`. The values do not change while the app runs."""
|
|
153
|
+
return get_locales()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def use_calendars() -> List[Calendar]:
|
|
157
|
+
"""Hook form of `get_calendars()`. The values do not change while the app runs."""
|
|
158
|
+
return get_calendars()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
getLocales = get_locales
|
|
162
|
+
getCalendars = get_calendars
|
|
163
|
+
useLocales = use_locales
|
|
164
|
+
useCalendars = use_calendars
|
|
165
|
+
|
|
166
|
+
__all__ = [
|
|
167
|
+
"Locale", "Calendar", "get_locales", "get_calendars", "use_locales", "use_calendars",
|
|
168
|
+
"getLocales", "getCalendars", "useLocales", "useCalendars",
|
|
169
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package dev.pyxpo.modules.localization
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.os.LocaleList
|
|
6
|
+
import android.text.TextUtils
|
|
7
|
+
import android.text.format.DateFormat
|
|
8
|
+
import android.util.LayoutDirection
|
|
9
|
+
import dev.pyxpo.moduleapi.PxModule
|
|
10
|
+
import dev.pyxpo.moduleapi.PxModuleContext
|
|
11
|
+
import dev.pyxpo.moduleapi.PxReply
|
|
12
|
+
import org.json.JSONArray
|
|
13
|
+
import java.text.DecimalFormatSymbols
|
|
14
|
+
import java.util.Currency
|
|
15
|
+
import java.util.Locale
|
|
16
|
+
import java.util.TimeZone
|
|
17
|
+
|
|
18
|
+
/** expo-localization on Android: `LocaleList`, `DecimalFormatSymbols`, `Currency` and the ICU calendar. */
|
|
19
|
+
class LocalizationModule : PxModule() {
|
|
20
|
+
override val name = "Localization"
|
|
21
|
+
|
|
22
|
+
override fun constants(context: Context): Map<String, Any?> =
|
|
23
|
+
mapOf("locales" to locales(), "calendars" to listOf(calendar(context)))
|
|
24
|
+
|
|
25
|
+
private fun currency(l: Locale): Currency? = try { Currency.getInstance(l) } catch (_: Exception) { null }
|
|
26
|
+
|
|
27
|
+
private fun measurementSystem(l: Locale): String? {
|
|
28
|
+
if (l.country.isEmpty()) return null
|
|
29
|
+
if (Build.VERSION.SDK_INT >= 28) {
|
|
30
|
+
return when (android.icu.util.LocaleData.getMeasurementSystem(android.icu.util.ULocale.forLocale(l))) {
|
|
31
|
+
android.icu.util.LocaleData.MeasurementSystem.US -> "us"
|
|
32
|
+
android.icu.util.LocaleData.MeasurementSystem.UK -> "uk"
|
|
33
|
+
else -> "metric"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return when (l.country) { "US", "LR", "MM" -> "us"; "GB" -> "uk"; else -> "metric" }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Android 14+ lets the user pick the unit (`-u-mu-` locale extension); else the region's CLDR default. */
|
|
40
|
+
private fun temperatureUnit(l: Locale): String? {
|
|
41
|
+
when (l.getUnicodeLocaleType("mu")) {
|
|
42
|
+
"celsius" -> return "celsius"
|
|
43
|
+
"fahrenhe" -> return "fahrenheit"
|
|
44
|
+
}
|
|
45
|
+
if (l.country.isEmpty()) return null
|
|
46
|
+
return when (l.country) { "US", "BS", "BZ", "KY", "PR", "PW", "LR", "FM", "MH" -> "fahrenheit"; else -> "celsius" }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun locales(): List<Map<String, Any?>> {
|
|
50
|
+
val list = LocaleList.getDefault()
|
|
51
|
+
return (0 until list.size()).map { i ->
|
|
52
|
+
val l = list[i]
|
|
53
|
+
val cur = currency(l)
|
|
54
|
+
val sym = DecimalFormatSymbols.getInstance(l)
|
|
55
|
+
val region = l.country.ifEmpty { null }
|
|
56
|
+
mapOf(
|
|
57
|
+
"languageTag" to l.toLanguageTag(),
|
|
58
|
+
"languageCode" to l.language.ifEmpty { null },
|
|
59
|
+
"languageScriptCode" to l.script.ifEmpty { null },
|
|
60
|
+
"regionCode" to region,
|
|
61
|
+
"languageRegionCode" to region,
|
|
62
|
+
"currencyCode" to cur?.currencyCode,
|
|
63
|
+
"currencySymbol" to cur?.getSymbol(l),
|
|
64
|
+
"languageCurrencyCode" to cur?.currencyCode,
|
|
65
|
+
"languageCurrencySymbol" to cur?.getSymbol(l),
|
|
66
|
+
"decimalSeparator" to sym.decimalSeparator.toString(),
|
|
67
|
+
"digitGroupingSeparator" to sym.groupingSeparator.toString(),
|
|
68
|
+
"textDirection" to if (TextUtils.getLayoutDirectionFromLocale(l) == LayoutDirection.RTL) "rtl" else "ltr",
|
|
69
|
+
"measurementSystem" to measurementSystem(l),
|
|
70
|
+
"temperatureUnit" to temperatureUnit(l),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun calendar(c: Context): Map<String, Any?> {
|
|
76
|
+
val cal = android.icu.util.Calendar.getInstance()
|
|
77
|
+
val type = cal.type.let { if (it == "gregorian") "gregory" else it }
|
|
78
|
+
return mapOf(
|
|
79
|
+
"calendar" to type,
|
|
80
|
+
"timeZone" to TimeZone.getDefault().id,
|
|
81
|
+
"uses24hourClock" to DateFormat.is24HourFormat(c),
|
|
82
|
+
"firstWeekday" to cal.firstDayOfWeek,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun call(ctx: PxModuleContext, method: String, args: JSONArray, reply: PxReply) {
|
|
87
|
+
reply(null, "Localization has no method '$method'")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// expo-localization on iOS. Language fields come from each preferred language; region, currency and
|
|
4
|
+
/// number formats come from the user's region setting (`Locale.current`), as in Settings.
|
|
5
|
+
final class LocalizationModule: PxModule {
|
|
6
|
+
static let moduleName = "Localization"
|
|
7
|
+
|
|
8
|
+
required init() {}
|
|
9
|
+
|
|
10
|
+
static var constants: [String: Any] {
|
|
11
|
+
["locales": locales, "calendars": [calendar]]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private static func value(_ s: String?) -> Any { s ?? NSNull() }
|
|
15
|
+
|
|
16
|
+
private static func measurementSystem(_ l: Locale) -> String {
|
|
17
|
+
if #available(iOS 16, *) {
|
|
18
|
+
switch l.measurementSystem {
|
|
19
|
+
case .us: return "us"
|
|
20
|
+
case .uk: return "uk"
|
|
21
|
+
default: return "metric"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return l.usesMetricSystem ? "metric" : "us"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// The unit `MeasurementFormatter` picks for the locale (follows the Temperature setting on iOS 16+).
|
|
28
|
+
private static func temperatureUnit(_ l: Locale) -> String {
|
|
29
|
+
let f = MeasurementFormatter()
|
|
30
|
+
f.locale = l
|
|
31
|
+
f.numberFormatter.maximumFractionDigits = 0
|
|
32
|
+
let text = f.string(from: Measurement(value: 0, unit: UnitTemperature.celsius))
|
|
33
|
+
return text.contains("F") ? "fahrenheit" : "celsius"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private static var locales: [[String: Any]] {
|
|
37
|
+
let current = Locale.current
|
|
38
|
+
let tags = Locale.preferredLanguages.isEmpty ? [current.identifier] : Locale.preferredLanguages
|
|
39
|
+
return tags.map { tag in
|
|
40
|
+
let l = Locale(identifier: tag)
|
|
41
|
+
let lang = l.languageCode
|
|
42
|
+
return [
|
|
43
|
+
"languageTag": tag,
|
|
44
|
+
"languageCode": value(lang),
|
|
45
|
+
"languageScriptCode": value(l.scriptCode),
|
|
46
|
+
"regionCode": value(current.regionCode),
|
|
47
|
+
"languageRegionCode": value(l.regionCode),
|
|
48
|
+
"currencyCode": value(current.currencyCode),
|
|
49
|
+
"currencySymbol": value(current.currencySymbol),
|
|
50
|
+
"languageCurrencyCode": value(l.currencyCode),
|
|
51
|
+
"languageCurrencySymbol": value(l.currencySymbol),
|
|
52
|
+
"decimalSeparator": value(current.decimalSeparator),
|
|
53
|
+
"digitGroupingSeparator": value(current.groupingSeparator),
|
|
54
|
+
"textDirection": lang.map { Locale.characterDirection(forLanguage: $0) == .rightToLeft ? "rtl" : "ltr" } ?? NSNull(),
|
|
55
|
+
"measurementSystem": measurementSystem(current),
|
|
56
|
+
"temperatureUnit": temperatureUnit(current),
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// CLDR names for the calendar identifiers, as on the web (`Intl`).
|
|
62
|
+
private static func cldr(_ id: Calendar.Identifier) -> String {
|
|
63
|
+
switch id {
|
|
64
|
+
case .gregorian: return "gregory"
|
|
65
|
+
case .buddhist: return "buddhist"
|
|
66
|
+
case .chinese: return "chinese"
|
|
67
|
+
case .coptic: return "coptic"
|
|
68
|
+
case .ethiopicAmeteMihret: return "ethiopic"
|
|
69
|
+
case .ethiopicAmeteAlem: return "ethioaa"
|
|
70
|
+
case .hebrew: return "hebrew"
|
|
71
|
+
case .indian: return "indian"
|
|
72
|
+
case .islamic: return "islamic"
|
|
73
|
+
case .islamicCivil: return "islamic-civil"
|
|
74
|
+
case .islamicTabular: return "islamic-tbla"
|
|
75
|
+
case .islamicUmmAlQura: return "islamic-umalqura"
|
|
76
|
+
case .iso8601: return "iso8601"
|
|
77
|
+
case .japanese: return "japanese"
|
|
78
|
+
case .persian: return "persian"
|
|
79
|
+
case .republicOfChina: return "roc"
|
|
80
|
+
default: return "gregory"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private static var calendar: [String: Any] {
|
|
85
|
+
let cal = Calendar.current
|
|
86
|
+
let hourFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current) ?? ""
|
|
87
|
+
return [
|
|
88
|
+
"calendar": cldr(cal.identifier),
|
|
89
|
+
"timeZone": TimeZone.current.identifier,
|
|
90
|
+
"uses24hourClock": !hourFormat.contains("a"),
|
|
91
|
+
"firstWeekday": cal.firstWeekday,
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func call(_ ctx: PxModuleContext, _ method: String, _ args: [Any], _ reply: @escaping PxReply) {
|
|
96
|
+
reply(nil, "Localization has no method '\(method)'")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"nativeContract": 2,
|
|
4
|
+
"name": "localization",
|
|
5
|
+
"native": "Localization",
|
|
6
|
+
"ios": {
|
|
7
|
+
"classes": ["LocalizationModule"]
|
|
8
|
+
},
|
|
9
|
+
"android": {
|
|
10
|
+
"namespace": "dev.pyxpo.modules.localization",
|
|
11
|
+
"classes": ["dev.pyxpo.modules.localization.LocalizationModule"],
|
|
12
|
+
"minSdk": 24
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyxpo-localization
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: The device's locales and calendars: the expo-localization API for pyxpo.
|
|
5
|
+
Author: Vishal Chaubey
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pyxpo/pyxpo/tree/main/packages/localization
|
|
8
|
+
Project-URL: Issues, https://github.com/pyxpo/pyxpo/issues
|
|
9
|
+
Keywords: pyxpo,pyxpo-module,mobile,android,ios
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pyxpo<0.2,>=0.1.0a2
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# pyxpo-localization
|
|
21
|
+
|
|
22
|
+
The user's languages, region formats and calendar settings for [pyxpo](https://github.com/pyxpo/pyxpo)
|
|
23
|
+
apps. The API follows [expo-localization](https://docs.expo.dev/versions/latest/sdk/localization/).
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install pyxpo-localization
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
[tool.pyxpo]
|
|
31
|
+
modules = ["localization"]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import pyxpo_localization as Localization
|
|
36
|
+
|
|
37
|
+
locale = Localization.get_locales()[0]
|
|
38
|
+
print(locale.language_tag, locale.region_code, locale.currency_code) # en-IN IN INR
|
|
39
|
+
cal = Localization.get_calendars()[0]
|
|
40
|
+
print(cal.time_zone, cal.uses_24_hour_clock, cal.first_weekday) # Asia/Kolkata False 1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every name also has its expo-localization spelling (`Localization.getLocales`, `Localization.useCalendars`).
|
|
44
|
+
Records accept Expo's camelCase keys too: `locale["languageTag"]`, and `locale.to_dict()` gives the
|
|
45
|
+
camelCase dict.
|
|
46
|
+
|
|
47
|
+
The values are read once when the app starts. On the desktop they come from `LC_ALL` / `LC_MESSAGES` /
|
|
48
|
+
`LANG` and the system time zone, and the fields the desktop cannot tell (currency, separators) are `None`.
|
|
49
|
+
|
|
50
|
+
## Functions
|
|
51
|
+
|
|
52
|
+
| Function | Returns |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `get_locales()` | `list[Locale]`, the preferred languages, most preferred first. Never empty. |
|
|
55
|
+
| `get_calendars()` | `list[Calendar]`, the user's calendars, preferred first. Never empty. |
|
|
56
|
+
| `use_locales()`, `use_calendars()` | The same, for use in components. |
|
|
57
|
+
|
|
58
|
+
## `Locale`
|
|
59
|
+
|
|
60
|
+
| Field | Example | Notes |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `language_tag` | `"en-IN"` | BCP 47 tag. |
|
|
63
|
+
| `language_code` | `"en"` | |
|
|
64
|
+
| `language_script_code` | `"Hant"` | `None` when the tag has no script. |
|
|
65
|
+
| `region_code` | `"IN"` | iOS: the region setting. Android: the locale's region. |
|
|
66
|
+
| `language_region_code` | `"IN"` | The region in the language tag. |
|
|
67
|
+
| `currency_code`, `currency_symbol` | `"INR"`, `"₹"` | iOS: from the region setting. |
|
|
68
|
+
| `language_currency_code`, `language_currency_symbol` | `"INR"`, `"₹"` | From the language tag's region. |
|
|
69
|
+
| `decimal_separator`, `digit_grouping_separator` | `"."`, `","` | |
|
|
70
|
+
| `text_direction` | `"ltr"` | `"ltr"` or `"rtl"`. |
|
|
71
|
+
| `measurement_system` | `"metric"` | `"metric"`, `"us"` or `"uk"`. |
|
|
72
|
+
| `temperature_unit` | `"celsius"` | `"celsius"` or `"fahrenheit"`. Follows the user's setting on iOS 16+ and Android 14+. |
|
|
73
|
+
|
|
74
|
+
## `Calendar`
|
|
75
|
+
|
|
76
|
+
| Field | Example | Notes |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `calendar` | `"gregory"` | CLDR name, as in JavaScript `Intl`. |
|
|
79
|
+
| `time_zone` | `"Asia/Kolkata"` | IANA id. |
|
|
80
|
+
| `uses_24_hour_clock` | `False` | |
|
|
81
|
+
| `first_weekday` | `1` | 1 is Sunday, 7 is Saturday. |
|
|
82
|
+
|
|
83
|
+
## Platforms
|
|
84
|
+
|
|
85
|
+
iOS 15+, Android 7 (API 24)+. No permissions.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pyxpo_localization/__init__.py
|
|
5
|
+
pyxpo_localization/py.typed
|
|
6
|
+
pyxpo_localization/pyxpo-module.json
|
|
7
|
+
pyxpo_localization.egg-info/PKG-INFO
|
|
8
|
+
pyxpo_localization.egg-info/SOURCES.txt
|
|
9
|
+
pyxpo_localization.egg-info/dependency_links.txt
|
|
10
|
+
pyxpo_localization.egg-info/entry_points.txt
|
|
11
|
+
pyxpo_localization.egg-info/requires.txt
|
|
12
|
+
pyxpo_localization.egg-info/top_level.txt
|
|
13
|
+
pyxpo_localization/android/src/main/AndroidManifest.xml
|
|
14
|
+
pyxpo_localization/android/src/main/java/dev/pyxpo/modules/localization/LocalizationModule.kt
|
|
15
|
+
pyxpo_localization/ios/LocalizationModule.swift
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyxpo<0.2,>=0.1.0a2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyxpo_localization
|