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.
@@ -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,2 @@
1
+ [pyxpo.modules]
2
+ localization = pyxpo_localization
@@ -0,0 +1 @@
1
+ pyxpo<0.2,>=0.1.0a2
@@ -0,0 +1 @@
1
+ pyxpo_localization
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+