sys-lang 1.0.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.
- sys_lang-1.0.0/PKG-INFO +121 -0
- sys_lang-1.0.0/docs/LICENSE.md +21 -0
- sys_lang-1.0.0/docs/README.md +63 -0
- sys_lang-1.0.0/pyproject.toml +120 -0
- sys_lang-1.0.0/setup.cfg +4 -0
- sys_lang-1.0.0/src/sys_lang/__init__.py +3 -0
- sys_lang-1.0.0/src/sys_lang/api.py +22 -0
- sys_lang-1.0.0/src/sys_lang/cli/__init__.py +0 -0
- sys_lang-1.0.0/src/sys_lang/cli/__main__.py +17 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/color.py +30 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/data/__init__.py +3 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/data/csv.py +5 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/data/file.py +23 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/data/json.py +108 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/data/sns.py +8 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/env.py +4 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/init.py +13 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/jsdelivr.py +9 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/language.py +86 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/log.py +99 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/pkg.py +6 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/settings.py +103 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/string.py +2 -0
- sys_lang-1.0.0/src/sys_lang/cli/lib/url.py +33 -0
- sys_lang-1.0.0/src/sys_lang/data/_locales/en/messages.json +17 -0
- sys_lang-1.0.0/src/sys_lang/data/package_data.json +29 -0
- sys_lang-1.0.0/src/sys_lang.egg-info/PKG-INFO +121 -0
- sys_lang-1.0.0/src/sys_lang.egg-info/SOURCES.txt +30 -0
- sys_lang-1.0.0/src/sys_lang.egg-info/dependency_links.txt +1 -0
- sys_lang-1.0.0/src/sys_lang.egg-info/entry_points.txt +17 -0
- sys_lang-1.0.0/src/sys_lang.egg-info/requires.txt +13 -0
- sys_lang-1.0.0/src/sys_lang.egg-info/top_level.txt +1 -0
sys_lang-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sys-lang
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Detect the system language.
|
|
5
|
+
Author-email: Adam Lui <adam@kudoai.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Changelog, https://github.com/adamlui/python-utils/releases/tag/sys-lang-1.0.0
|
|
8
|
+
Project-URL: Documentation, https://github.com/adamlui/python-utils/tree/main/sys-lang/docs
|
|
9
|
+
Project-URL: Funding, https://github.com/sponsors/adamlui
|
|
10
|
+
Project-URL: Homepage, https://github.com/adamlui/python-utils/tree/main/sys-lang/#readme
|
|
11
|
+
Project-URL: Issues, https://github.com/adamlui/python-utils/issues
|
|
12
|
+
Project-URL: PyPI Stats, https://pepy.tech/projects/sys-lang
|
|
13
|
+
Project-URL: Releases, https://github.com/adamlui/python-utils/releases
|
|
14
|
+
Project-URL: Repository, https://github.com/adamlui/python-utils
|
|
15
|
+
Keywords: api,cli,console,detect,detection,dev-tool,environment,language,locale,shell,system-language,system-locale,terminal,utility
|
|
16
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
17
|
+
Classifier: Environment :: Console
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
20
|
+
Classifier: Intended Audience :: Information Technology
|
|
21
|
+
Classifier: Intended Audience :: System Administrators
|
|
22
|
+
Classifier: Natural Language :: English
|
|
23
|
+
Classifier: Operating System :: OS Independent
|
|
24
|
+
Classifier: Programming Language :: Python
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
35
|
+
Classifier: Topic :: Software Development
|
|
36
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
37
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
38
|
+
Classifier: Topic :: Software Development :: Localization
|
|
39
|
+
Classifier: Topic :: System
|
|
40
|
+
Classifier: Topic :: System :: Shells
|
|
41
|
+
Classifier: Topic :: Terminals
|
|
42
|
+
Classifier: Topic :: Utilities
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: <4,>=3.8
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: docs/LICENSE.md
|
|
47
|
+
Requires-Dist: colorama<1,>=0.4.6; platform_system == "Windows"
|
|
48
|
+
Requires-Dist: is_unicode_supported<2,>=1.1.2
|
|
49
|
+
Requires-Dist: json5<1,>=0.13.0
|
|
50
|
+
Requires-Dist: non-latin-locales<2,>=1.0.1
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: nox>=2026.2.9; extra == "dev"
|
|
53
|
+
Requires-Dist: remove-json-keys<2,>=1.9.2; extra == "dev"
|
|
54
|
+
Requires-Dist: tomli<3,>=2.4.0; extra == "dev"
|
|
55
|
+
Requires-Dist: tomli-w<2,>=1.2.0; extra == "dev"
|
|
56
|
+
Requires-Dist: translate-messages<2,>=1.9.1; extra == "dev"
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
|
|
59
|
+
<a id="top"></a>
|
|
60
|
+
|
|
61
|
+
# > sys-lang
|
|
62
|
+
|
|
63
|
+
<a href="https://github.com/adamlui/python-utils/releases/tag/sys-lang-1.0.0">
|
|
64
|
+
<img height=31 src="https://img.shields.io/badge/Latest_Build-1.0.0-32fcee.svg?logo=icinga&logoColor=white&labelColor=464646&style=for-the-badge"></a>
|
|
65
|
+
<a href="https://github.com/adamlui/python-utils/blob/main/sys-lang/docs/LICENSE.md">
|
|
66
|
+
<img height=31 src="https://img.shields.io/badge/License-MIT-f99b27.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
|
|
67
|
+
<a href="https://www.codefactor.io/repository/github/adamlui/python-utils">
|
|
68
|
+
<img height=31 src="https://img.shields.io/codefactor/grade/github/adamlui/python-utils?label=Code+Quality&logo=codefactor&logoColor=white&labelColor=464646&color=a0fc55&style=for-the-badge"></a>
|
|
69
|
+
<a href="https://sonarcloud.io/component_measures?metric=vulnerabilities&selected=adamlui_python-utils%3Asys-lang&id=adamlui_python-utils">
|
|
70
|
+
<img height=31 src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3Dadamlui_python-utils%26metricKeys%3Dvulnerabilities&query=%24.component.measures.0.value&style=for-the-badge&logo=sonar&logoColor=white&labelColor=464646&label=Vulnerabilities&color=fafc74"></a>
|
|
71
|
+
|
|
72
|
+
> ### _Detect the system language._
|
|
73
|
+
|
|
74
|
+
Returns ISO 639-1 (e.g. `en`) or ISO 3166-1 alpha-2-appended (`en_US`) code for user's preferred language. On Windows, queries `Get-Culture` via PowerShell. On *nix systems, reads `LC_ALL`, `LC_MESSAGES`, `LANG`, and `LANGUAGE`.
|
|
75
|
+
|
|
76
|
+
## ⚡ Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install sys-lang
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 💻 Command line usage
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
sys-lang # or syslang
|
|
86
|
+
# e.g. => 'en_US'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
CLI options:
|
|
90
|
+
|
|
91
|
+
| Option | Description
|
|
92
|
+
| ------------------- | ------------------------------------
|
|
93
|
+
| `-n`, `--no-region` | Don't include region when available
|
|
94
|
+
| `-h`, `--help` | Show help screen
|
|
95
|
+
| `-v`, `--version` | Show version
|
|
96
|
+
| `--docs` | Open docs URL
|
|
97
|
+
|
|
98
|
+
## 🔌 API usage
|
|
99
|
+
|
|
100
|
+
```py
|
|
101
|
+
from sys_lang import get_sys_lang
|
|
102
|
+
|
|
103
|
+
print(get_sys_lang()) # e.g. => 'zh_HK'
|
|
104
|
+
print(get_sys_lang(region=False)) # e.g. => 'zh'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## MIT License
|
|
108
|
+
|
|
109
|
+
Copyright © 2026 [Adam Lui](https://github.com/adamlui)
|
|
110
|
+
|
|
111
|
+
## Related
|
|
112
|
+
|
|
113
|
+
🇪🇸 [latin-locales](https://github.com/adamlui/python-utils/tree/main/latin-locales/#readme) - ISO 639-1 (2-letter) codes for Latin locales.
|
|
114
|
+
<br>🇨🇳 [non-latin-locales](https://github.com/adamlui/python-utils/tree/main/non-latin-locales/#readme) - ISO 639-1 (2-letter) codes for non-Latin locales.
|
|
115
|
+
<br>🌍 [translate-messages](https://github.com/adamlui/python-utils/tree/main/translate-messages/#readme) - Translate `en/messages.json` (chrome.i18n format) to 100+ locales automatically.
|
|
116
|
+
<br>🈶 [is-unicode-supported](https://github.com/adamlui/python-utils/tree/main/is-unicode-supported/#readme) - Detect whether the terminal supports advanced Unicode.
|
|
117
|
+
|
|
118
|
+
#
|
|
119
|
+
|
|
120
|
+
<picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/adamlui/python-utils@760599e/assets/images/icons/home/white/icon32x27.png"><img height=13 src="https://cdn.jsdelivr.net/gh/adamlui/python-utils@760599e/assets/images/icons/home/dark-gray/icon32x27.png"></picture> <a href=https://github.com/adamlui/python-utils/#readme>**More Python utilities**</a> /
|
|
121
|
+
<a href="#top">Back to top ↑</a>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 🏛️ MIT License
|
|
2
|
+
|
|
3
|
+
**Copyright © 2026 [Adam Lui](https://github.com/adamlui)**
|
|
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,63 @@
|
|
|
1
|
+
<a id="top"></a>
|
|
2
|
+
|
|
3
|
+
# > sys-lang
|
|
4
|
+
|
|
5
|
+
<a href="https://github.com/adamlui/python-utils/releases/tag/sys-lang-1.0.0">
|
|
6
|
+
<img height=31 src="https://img.shields.io/badge/Latest_Build-1.0.0-32fcee.svg?logo=icinga&logoColor=white&labelColor=464646&style=for-the-badge"></a>
|
|
7
|
+
<a href="https://github.com/adamlui/python-utils/blob/main/sys-lang/docs/LICENSE.md">
|
|
8
|
+
<img height=31 src="https://img.shields.io/badge/License-MIT-f99b27.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
|
|
9
|
+
<a href="https://www.codefactor.io/repository/github/adamlui/python-utils">
|
|
10
|
+
<img height=31 src="https://img.shields.io/codefactor/grade/github/adamlui/python-utils?label=Code+Quality&logo=codefactor&logoColor=white&labelColor=464646&color=a0fc55&style=for-the-badge"></a>
|
|
11
|
+
<a href="https://sonarcloud.io/component_measures?metric=vulnerabilities&selected=adamlui_python-utils%3Asys-lang&id=adamlui_python-utils">
|
|
12
|
+
<img height=31 src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3Dadamlui_python-utils%26metricKeys%3Dvulnerabilities&query=%24.component.measures.0.value&style=for-the-badge&logo=sonar&logoColor=white&labelColor=464646&label=Vulnerabilities&color=fafc74"></a>
|
|
13
|
+
|
|
14
|
+
> ### _Detect the system language._
|
|
15
|
+
|
|
16
|
+
Returns ISO 639-1 (e.g. `en`) or ISO 3166-1 alpha-2-appended (`en_US`) code for user's preferred language. On Windows, queries `Get-Culture` via PowerShell. On *nix systems, reads `LC_ALL`, `LC_MESSAGES`, `LANG`, and `LANGUAGE`.
|
|
17
|
+
|
|
18
|
+
## ⚡ Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install sys-lang
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 💻 Command line usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
sys-lang # or syslang
|
|
28
|
+
# e.g. => 'en_US'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
CLI options:
|
|
32
|
+
|
|
33
|
+
| Option | Description
|
|
34
|
+
| ------------------- | ------------------------------------
|
|
35
|
+
| `-n`, `--no-region` | Don't include region when available
|
|
36
|
+
| `-h`, `--help` | Show help screen
|
|
37
|
+
| `-v`, `--version` | Show version
|
|
38
|
+
| `--docs` | Open docs URL
|
|
39
|
+
|
|
40
|
+
## 🔌 API usage
|
|
41
|
+
|
|
42
|
+
```py
|
|
43
|
+
from sys_lang import get_sys_lang
|
|
44
|
+
|
|
45
|
+
print(get_sys_lang()) # e.g. => 'zh_HK'
|
|
46
|
+
print(get_sys_lang(region=False)) # e.g. => 'zh'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## MIT License
|
|
50
|
+
|
|
51
|
+
Copyright © 2026 [Adam Lui](https://github.com/adamlui)
|
|
52
|
+
|
|
53
|
+
## Related
|
|
54
|
+
|
|
55
|
+
🇪🇸 [latin-locales](https://github.com/adamlui/python-utils/tree/main/latin-locales/#readme) - ISO 639-1 (2-letter) codes for Latin locales.
|
|
56
|
+
<br>🇨🇳 [non-latin-locales](https://github.com/adamlui/python-utils/tree/main/non-latin-locales/#readme) - ISO 639-1 (2-letter) codes for non-Latin locales.
|
|
57
|
+
<br>🌍 [translate-messages](https://github.com/adamlui/python-utils/tree/main/translate-messages/#readme) - Translate `en/messages.json` (chrome.i18n format) to 100+ locales automatically.
|
|
58
|
+
<br>🈶 [is-unicode-supported](https://github.com/adamlui/python-utils/tree/main/is-unicode-supported/#readme) - Detect whether the terminal supports advanced Unicode.
|
|
59
|
+
|
|
60
|
+
#
|
|
61
|
+
|
|
62
|
+
<picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/adamlui/python-utils@760599e/assets/images/icons/home/white/icon32x27.png"><img height=13 src="https://cdn.jsdelivr.net/gh/adamlui/python-utils@760599e/assets/images/icons/home/dark-gray/icon32x27.png"></picture> <a href=https://github.com/adamlui/python-utils/#readme>**More Python utilities**</a> /
|
|
63
|
+
<a href="#top">Back to top ↑</a>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=82.0.0,<83",
|
|
4
|
+
"wheel",
|
|
5
|
+
]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "sys-lang"
|
|
10
|
+
version = "1.0.0"
|
|
11
|
+
description = "Detect the system language."
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Adam Lui", email = "adam@kudoai.com" },
|
|
14
|
+
]
|
|
15
|
+
readme = "docs/README.md"
|
|
16
|
+
license = "MIT"
|
|
17
|
+
license-files = [
|
|
18
|
+
"docs/LICENSE.md",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"colorama>=0.4.6,<1 ; platform_system == 'Windows'",
|
|
22
|
+
"is_unicode_supported>=1.1.2,<2",
|
|
23
|
+
"json5>=0.13.0,<1",
|
|
24
|
+
"non-latin-locales>=1.0.1,<2",
|
|
25
|
+
]
|
|
26
|
+
requires-python = ">=3.8,<4"
|
|
27
|
+
keywords = [
|
|
28
|
+
"api",
|
|
29
|
+
"cli",
|
|
30
|
+
"console",
|
|
31
|
+
"detect",
|
|
32
|
+
"detection",
|
|
33
|
+
"dev-tool",
|
|
34
|
+
"environment",
|
|
35
|
+
"language",
|
|
36
|
+
"locale",
|
|
37
|
+
"shell",
|
|
38
|
+
"system-language",
|
|
39
|
+
"system-locale",
|
|
40
|
+
"terminal",
|
|
41
|
+
"utility",
|
|
42
|
+
]
|
|
43
|
+
classifiers = [
|
|
44
|
+
"Development Status :: 5 - Production/Stable",
|
|
45
|
+
"Environment :: Console",
|
|
46
|
+
"Intended Audience :: Developers",
|
|
47
|
+
"Intended Audience :: End Users/Desktop",
|
|
48
|
+
"Intended Audience :: Information Technology",
|
|
49
|
+
"Intended Audience :: System Administrators",
|
|
50
|
+
"Natural Language :: English",
|
|
51
|
+
"Operating System :: OS Independent",
|
|
52
|
+
"Programming Language :: Python",
|
|
53
|
+
"Programming Language :: Python :: 3",
|
|
54
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
55
|
+
"Programming Language :: Python :: 3.8",
|
|
56
|
+
"Programming Language :: Python :: 3.9",
|
|
57
|
+
"Programming Language :: Python :: 3.10",
|
|
58
|
+
"Programming Language :: Python :: 3.11",
|
|
59
|
+
"Programming Language :: Python :: 3.12",
|
|
60
|
+
"Programming Language :: Python :: 3.13",
|
|
61
|
+
"Programming Language :: Python :: 3.14",
|
|
62
|
+
"Programming Language :: Python :: 3.15",
|
|
63
|
+
"Topic :: Software Development",
|
|
64
|
+
"Topic :: Software Development :: Libraries",
|
|
65
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
66
|
+
"Topic :: Software Development :: Localization",
|
|
67
|
+
"Topic :: System",
|
|
68
|
+
"Topic :: System :: Shells",
|
|
69
|
+
"Topic :: Terminals",
|
|
70
|
+
"Topic :: Utilities",
|
|
71
|
+
"Typing :: Typed",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[project.urls]
|
|
75
|
+
Changelog = "https://github.com/adamlui/python-utils/releases/tag/sys-lang-1.0.0"
|
|
76
|
+
Documentation = "https://github.com/adamlui/python-utils/tree/main/sys-lang/docs"
|
|
77
|
+
Funding = "https://github.com/sponsors/adamlui"
|
|
78
|
+
Homepage = "https://github.com/adamlui/python-utils/tree/main/sys-lang/#readme"
|
|
79
|
+
Issues = "https://github.com/adamlui/python-utils/issues"
|
|
80
|
+
"PyPI Stats" = "https://pepy.tech/projects/sys-lang"
|
|
81
|
+
Releases = "https://github.com/adamlui/python-utils/releases"
|
|
82
|
+
Repository = "https://github.com/adamlui/python-utils"
|
|
83
|
+
|
|
84
|
+
[project.scripts]
|
|
85
|
+
sys-lang = "sys_lang.cli.__main__:main"
|
|
86
|
+
sys-language = "sys_lang.cli.__main__:main"
|
|
87
|
+
system-lang = "sys_lang.cli.__main__:main"
|
|
88
|
+
system-language = "sys_lang.cli.__main__:main"
|
|
89
|
+
get-sys-lang = "sys_lang.cli.__main__:main"
|
|
90
|
+
get-sys-language = "sys_lang.cli.__main__:main"
|
|
91
|
+
get-system-lang = "sys_lang.cli.__main__:main"
|
|
92
|
+
get-system-language = "sys_lang.cli.__main__:main"
|
|
93
|
+
syslang = "sys_lang.cli.__main__:main"
|
|
94
|
+
syslanguage = "sys_lang.cli.__main__:main"
|
|
95
|
+
systemlang = "sys_lang.cli.__main__:main"
|
|
96
|
+
systemlanguage = "sys_lang.cli.__main__:main"
|
|
97
|
+
getsyslang = "sys_lang.cli.__main__:main"
|
|
98
|
+
getsyslanguage = "sys_lang.cli.__main__:main"
|
|
99
|
+
getsystemlang = "sys_lang.cli.__main__:main"
|
|
100
|
+
getsystemlanguage = "sys_lang.cli.__main__:main"
|
|
101
|
+
|
|
102
|
+
[project.optional-dependencies]
|
|
103
|
+
dev = [
|
|
104
|
+
"nox>=2026.2.9",
|
|
105
|
+
"remove-json-keys>=1.9.2,<2",
|
|
106
|
+
"tomli>=2.4.0,<3",
|
|
107
|
+
"tomli-w>=1.2.0,<2",
|
|
108
|
+
"translate-messages>=1.9.1,<2",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[tool.setuptools.packages.find]
|
|
112
|
+
where = [
|
|
113
|
+
"src",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[tool.setuptools.package-data]
|
|
117
|
+
sys_lang = [
|
|
118
|
+
"data/*.json",
|
|
119
|
+
"data/_locales/en/messages.json",
|
|
120
|
+
]
|
sys_lang-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
def get_sys_lang(region: bool = True) -> str: # e.g. 'en_US'
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
if sys.platform == 'win32':
|
|
5
|
+
import subprocess
|
|
6
|
+
lang_result = subprocess.run(
|
|
7
|
+
['powershell', '-Command', '(Get-Culture).Name'], capture_output=True, text=True, check=True).stdout.strip()
|
|
8
|
+
if not lang_result : raise RuntimeError('Could not detect Windows system language')
|
|
9
|
+
lang_code = lang_result.replace('-', '_')
|
|
10
|
+
return lang_code if region or '_' not in lang_code else lang_code.split('_')[0]
|
|
11
|
+
|
|
12
|
+
else: # *nix sys
|
|
13
|
+
import os
|
|
14
|
+
for lang_env_var in ('LC_ALL', 'LC_MESSAGES', 'LANG', 'LANGUAGE'):
|
|
15
|
+
lang_val = os.environ.get(lang_env_var)
|
|
16
|
+
if not lang_val : continue
|
|
17
|
+
lang_val = lang_val.split(':')[0]
|
|
18
|
+
lang_val = lang_val.split('.')[0]
|
|
19
|
+
lang_val = lang_val.split('@')[0]
|
|
20
|
+
if lang_val.upper() in ('C', 'POSIX') : return 'en'
|
|
21
|
+
return lang_val if region or '_' not in lang_val else lang_val.split('_')[0]
|
|
22
|
+
raise RuntimeError('Could not detect *nix system language')
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from ..api import get_sys_lang
|
|
4
|
+
from .lib import init, settings
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
cli = init.cli()
|
|
8
|
+
|
|
9
|
+
# Process early-exit args (e.g. --help, --version)
|
|
10
|
+
for ctrl_name, ctrl in vars(settings.controls).items():
|
|
11
|
+
if getattr(ctrl, 'exit', False) and getattr(cli.config, ctrl_name, False):
|
|
12
|
+
if hasattr(ctrl, 'handler') : ctrl.handler(cli)
|
|
13
|
+
sys.exit(0)
|
|
14
|
+
|
|
15
|
+
print(get_sys_lang(region=not cli.config.no_region))
|
|
16
|
+
|
|
17
|
+
if __name__ == '__main__' : main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
nc = '\x1b[0m'
|
|
2
|
+
hex = {
|
|
3
|
+
'br': '#ff0000', 'by': '#ffff00', 'bo': '#ffa500', 'bg': '#00ff00',
|
|
4
|
+
'bw': '#ffffff', 'dg': '#008000', 'gry': '#808080', 'blk': '#000000', 'tlBG': '#008080'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
def hex_to_ansi(hex_color: str) -> str:
|
|
8
|
+
r = int(hex_color[1:3], 16)
|
|
9
|
+
g = int(hex_color[3:5], 16)
|
|
10
|
+
b = int(hex_color[5:7], 16)
|
|
11
|
+
return f'\x1b[38;2;{r};{g};{b}m'
|
|
12
|
+
|
|
13
|
+
class _Schemes:
|
|
14
|
+
@property
|
|
15
|
+
def default(self) -> list[str]:
|
|
16
|
+
return [hex_to_ansi(hex) for hex in [
|
|
17
|
+
'#00e5bc', '#18c8ae', '#30ac9f', '#488f91', '#607383',
|
|
18
|
+
'#775674', '#8f3966', '#a71d57', '#bf0049', '#9a1b5e'
|
|
19
|
+
]]
|
|
20
|
+
@property
|
|
21
|
+
def rainbow(self) -> list[str]:
|
|
22
|
+
return [hex_to_ansi(hex) for hex in [
|
|
23
|
+
'#e41a1c', '#ff7f00', '#ffff33', '#4daf4a', '#377eb8',
|
|
24
|
+
'#984ea3', '#f781bf', '#999999', '#a65628', '#d95f02'
|
|
25
|
+
]]
|
|
26
|
+
schemes = _Schemes()
|
|
27
|
+
|
|
28
|
+
def __getattr__(hex_key: str) -> str: # add color.hex_key getters that return ANSI
|
|
29
|
+
if hex_key in hex: return hex_to_ansi(hex[hex_key])
|
|
30
|
+
raise AttributeError(f"module 'color' has no attribute '{hex_key}'")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
def atomic_write(file_path: Union[Path, str], data: str, encoding: str ='utf-8') -> None: # to prevent TOCTOU
|
|
5
|
+
import os
|
|
6
|
+
file_path = Path(file_path)
|
|
7
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
8
|
+
tmp_path = file_path.parent / f'.{file_path.name}.tmp'
|
|
9
|
+
try:
|
|
10
|
+
with open(tmp_path, 'w', encoding=encoding) as file:
|
|
11
|
+
file.write(data) ; file.flush() ; os.fsync(file.fileno())
|
|
12
|
+
os.replace(tmp_path, file_path) # atomic rename
|
|
13
|
+
except Exception:
|
|
14
|
+
if tmp_path.exists() : tmp_path.unlink()
|
|
15
|
+
raise
|
|
16
|
+
|
|
17
|
+
def read(file_path: Union[Path, str], encoding: str = 'utf-8') -> str:
|
|
18
|
+
with open(file_path, 'r', encoding=encoding) as file:
|
|
19
|
+
return file.read()
|
|
20
|
+
|
|
21
|
+
def write(file_path: Union[Path, str], data: str, encoding: str = 'utf-8') -> None:
|
|
22
|
+
with open(file_path, 'w', encoding=encoding) as file:
|
|
23
|
+
file.write(data)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, Union
|
|
4
|
+
|
|
5
|
+
import json5
|
|
6
|
+
|
|
7
|
+
def flatten(json: Dict[str, Any], key: str = 'message') -> Dict[str, Any]: # eliminate need to ref nested keys
|
|
8
|
+
flat_obj = {}
|
|
9
|
+
for json_key in json:
|
|
10
|
+
val = json[json_key]
|
|
11
|
+
flat_obj[json_key] = val[key] if isinstance(val, dict) and key in val else val
|
|
12
|
+
return flat_obj
|
|
13
|
+
|
|
14
|
+
def is_valid(file_path: Union[Path, str], format: str = 'json') -> bool:
|
|
15
|
+
file_path = Path(file_path)
|
|
16
|
+
if not file_path.exists():
|
|
17
|
+
return False
|
|
18
|
+
try : file_text = file_path.read_text(encoding='utf-8')
|
|
19
|
+
except Exception:
|
|
20
|
+
return False
|
|
21
|
+
if format == 'json':
|
|
22
|
+
try : json.loads(file_text) ; return True
|
|
23
|
+
except Exception : return False
|
|
24
|
+
elif format == 'json5':
|
|
25
|
+
try : json5.loads(file_text) ; return True
|
|
26
|
+
except Exception : return False
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError(f"Unsupported format {format!r}. Expected 'json' or 'json5'")
|
|
29
|
+
|
|
30
|
+
def read(input: Union[Path, str], encoding: str = 'utf-8') -> Any:
|
|
31
|
+
input_str = str(input)
|
|
32
|
+
if input_str.endswith(('.json', '.json5')):
|
|
33
|
+
with open(input_str, 'r', encoding=encoding) as file:
|
|
34
|
+
return json5.load(file)
|
|
35
|
+
else : return json5.loads(input_str)
|
|
36
|
+
|
|
37
|
+
def write(file_path: Union[Path, str], data: Any, encoding: str = 'utf-8', ensure_ascii: bool = False,
|
|
38
|
+
style: str = 'pretty', atomic: bool = True, max_line_length: int = 120) -> None:
|
|
39
|
+
from . import file
|
|
40
|
+
from typing import Optional, List
|
|
41
|
+
|
|
42
|
+
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
def format_compact(obj: Any, indent: int = 0, padded_key: Optional[str] = None) -> List[str]:
|
|
45
|
+
indent_spaces = ' ' * indent
|
|
46
|
+
line_prefix = padded_key if padded_key else indent_spaces
|
|
47
|
+
|
|
48
|
+
if isinstance(obj, dict):
|
|
49
|
+
|
|
50
|
+
# Try fit whole dict in 1 line
|
|
51
|
+
kv_pairs = [f'"{key}": {json.dumps(val, separators=(",",":"), ensure_ascii=ensure_ascii)}'
|
|
52
|
+
for key,val in obj.items()]
|
|
53
|
+
single_line_dict = f'{line_prefix}{{ {", ".join(kv_pairs)} }}'
|
|
54
|
+
if len(single_line_dict) <= max_line_length:
|
|
55
|
+
return [single_line_dict]
|
|
56
|
+
|
|
57
|
+
# Else split long line up
|
|
58
|
+
lines = [line_prefix + '{']
|
|
59
|
+
for idx, (key,val) in enumerate(obj.items()):
|
|
60
|
+
inner_lines = format_compact(val, indent +1, f' {indent_spaces}"{key}": ')
|
|
61
|
+
for line in inner_lines : lines.append(line)
|
|
62
|
+
if idx != len(obj) -1 : lines[-1] += ',' # append comma except last line
|
|
63
|
+
lines.append(indent_spaces + '}')
|
|
64
|
+
return lines
|
|
65
|
+
|
|
66
|
+
elif isinstance(obj, list):
|
|
67
|
+
|
|
68
|
+
# Try fit whole list in 1 line
|
|
69
|
+
single_line_list = line_prefix + json.dumps(obj, separators=(',', ':'), ensure_ascii=ensure_ascii)
|
|
70
|
+
if len(single_line_list) <= max_line_length:
|
|
71
|
+
return [single_line_list]
|
|
72
|
+
|
|
73
|
+
# Else split long list up
|
|
74
|
+
lines = [line_prefix + '[']
|
|
75
|
+
if all(not isinstance(item, (dict, list)) for item in obj): # all items primitives, pack into lines
|
|
76
|
+
list_items = [json.dumps(item, ensure_ascii=ensure_ascii) for item in obj]
|
|
77
|
+
inner_indent = ' ' * (indent + 1)
|
|
78
|
+
current_line_items = []
|
|
79
|
+
for item in list_items:
|
|
80
|
+
candidate_line = ', '.join(current_line_items + [item]) if current_line_items else item
|
|
81
|
+
if len(inner_indent + candidate_line) + 1 <= max_line_length : current_line_items.append(item)
|
|
82
|
+
else: # current line full, flush/start new line
|
|
83
|
+
if current_line_items : lines.append(inner_indent + ', '.join(current_line_items) + ',')
|
|
84
|
+
current_line_items = [item]
|
|
85
|
+
if current_line_items: # flush last line
|
|
86
|
+
lines.append(inner_indent + ', '.join(current_line_items))
|
|
87
|
+
else: # mixed/complex items, format each recursively
|
|
88
|
+
for idx, item in enumerate(obj):
|
|
89
|
+
inner_lines = format_compact(item, indent +1)
|
|
90
|
+
for line in inner_lines : lines.append(line)
|
|
91
|
+
if idx != len(obj) -1 : lines[-1] += ','
|
|
92
|
+
lines.append(indent_spaces + ']')
|
|
93
|
+
return lines
|
|
94
|
+
|
|
95
|
+
else: # primitive
|
|
96
|
+
return [line_prefix + json.dumps(obj, ensure_ascii=ensure_ascii)]
|
|
97
|
+
|
|
98
|
+
# Format JSON
|
|
99
|
+
if style == 'pretty': # single key/val spans multi-lines
|
|
100
|
+
json_str = json.dumps(data, indent=2, ensure_ascii=ensure_ascii)
|
|
101
|
+
elif style == 'compact': # single key/val per line but honors max_line_length
|
|
102
|
+
json_str = '\n'.join(format_compact(data))
|
|
103
|
+
else: # minified to single line
|
|
104
|
+
json_str = json.dumps(data, separators=(',', ':'), ensure_ascii=ensure_ascii)
|
|
105
|
+
json_str += '\n'
|
|
106
|
+
|
|
107
|
+
# Write to file
|
|
108
|
+
getattr(file, 'atomic_write' if atomic else 'write')(file_path, json_str, encoding=encoding)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from types import SimpleNamespace as sn
|
|
3
|
+
|
|
4
|
+
from ...api import get_sys_lang
|
|
5
|
+
from . import data
|
|
6
|
+
|
|
7
|
+
def cli() -> sn:
|
|
8
|
+
from . import env, language, settings
|
|
9
|
+
cli = data.sns.from_dict(data.json.read(Path(__file__).parent.parent.parent / 'data/package_data.json'))
|
|
10
|
+
cli.msgs = language.get_msgs(cli,
|
|
11
|
+
language.generate_random_lang(excludes=['en']) if env.is_debug_mode() else get_sys_lang())
|
|
12
|
+
settings.load(cli)
|
|
13
|
+
return cli
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from types import SimpleNamespace as sn
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
def create_pkg_ver_url(cli: sn, version: Optional[str] = None) -> str:
|
|
5
|
+
version = version or cli.version
|
|
6
|
+
return f'{cli.urls.jsdelivr}@{cli.name}-{cli.version}/{cli.name}'
|
|
7
|
+
|
|
8
|
+
def create_commit_url(cli: sn, hash: str = 'latest') -> str:
|
|
9
|
+
return f'{cli.urls.jsdelivr}@{hash}/{cli.name}'
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import re
|
|
3
|
+
from types import SimpleNamespace as sn
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from . import data, log
|
|
7
|
+
|
|
8
|
+
def format_code(lang_code: str) -> str: # to match locale dir name (e.g., 'zh-tw' -> 'zh_TW')
|
|
9
|
+
return re.sub(
|
|
10
|
+
r'([a-z]{2,8})[-_]([a-z]{2})',
|
|
11
|
+
lambda m: f'{m.group(1).lower()}_{m.group(2).upper()}',
|
|
12
|
+
lang_code, flags=re.IGNORECASE
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def generate_random_lang(includes: Optional[List[str]] = None,
|
|
16
|
+
excludes: Optional[List[str]] = None) -> str:
|
|
17
|
+
import random
|
|
18
|
+
|
|
19
|
+
if includes is None : includes = []
|
|
20
|
+
if excludes is None : excludes = []
|
|
21
|
+
|
|
22
|
+
def get_locales() -> List[str]:
|
|
23
|
+
|
|
24
|
+
# Read cache if found
|
|
25
|
+
cache_dir = Path(__file__).parent.parent / '_cache'
|
|
26
|
+
locale_cache = cache_dir / 'locales.json'
|
|
27
|
+
if locale_cache.exists():
|
|
28
|
+
try : return data.json.read(locale_cache)
|
|
29
|
+
except Exception : pass
|
|
30
|
+
|
|
31
|
+
# Discover pkg _locales
|
|
32
|
+
locales_dir = Path(__file__).parent.parent.parent / 'data/_locales'
|
|
33
|
+
if not locales_dir.exists() : return ['en']
|
|
34
|
+
locales = []
|
|
35
|
+
for entry in locales_dir.iterdir():
|
|
36
|
+
if entry.is_dir() and re.match(r'^\w{2}[-_]?\w{0,2}$', entry.name):
|
|
37
|
+
locales.append(entry.name)
|
|
38
|
+
|
|
39
|
+
# Cache result
|
|
40
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
data.json.write(locale_cache, locales)
|
|
42
|
+
|
|
43
|
+
return locales
|
|
44
|
+
|
|
45
|
+
locales = includes.copy() if includes else get_locales()
|
|
46
|
+
|
|
47
|
+
# Filter out excludes
|
|
48
|
+
exclude_set = set(excludes)
|
|
49
|
+
locales = [locale for locale in locales if locale not in exclude_set]
|
|
50
|
+
|
|
51
|
+
# Get random language
|
|
52
|
+
random_lang = random.choice(locales) if locales else 'en'
|
|
53
|
+
log.debug(f'Random language: {random_lang}')
|
|
54
|
+
|
|
55
|
+
return random_lang
|
|
56
|
+
|
|
57
|
+
def get_msgs(cli: sn, lang_code: str = 'en') -> sn:
|
|
58
|
+
from . import jsdelivr, url
|
|
59
|
+
|
|
60
|
+
lang_code = format_code(lang_code)
|
|
61
|
+
if getattr(get_msgs, 'cached', None) and lang_code == get_msgs.cached_lang:
|
|
62
|
+
return get_msgs.cached # don't re-fetch same msgs
|
|
63
|
+
|
|
64
|
+
msgs = data.json.flatten(data.json.read( # local ones
|
|
65
|
+
Path(__file__).parent.parent.parent / 'data/_locales/en/messages.json'))
|
|
66
|
+
|
|
67
|
+
if not lang_code.startswith('en'): # fetch non-English msgs from jsDelivr
|
|
68
|
+
import is_unicode_supported, non_latin_locales
|
|
69
|
+
if lang_code.split('_')[0] in non_latin_locales and not is_unicode_supported(): # type: ignore
|
|
70
|
+
return sn(**msgs) # EN ones cuz non-Latin not supported
|
|
71
|
+
msg_base_url = f'{jsdelivr.create_commit_url(cli, cli.commit_hashes.locales)}' \
|
|
72
|
+
'/src/sys_lang/data/_locales'
|
|
73
|
+
msg_url = f'{msg_base_url}/{lang_code}/messages.json'
|
|
74
|
+
for attempt in range(3):
|
|
75
|
+
try: # fetch remote msgs
|
|
76
|
+
msgs = data.json.flatten(data.json.read(url.get(msg_url)))
|
|
77
|
+
break
|
|
78
|
+
except Exception: # retry up to 2X (region-stripped + EN)
|
|
79
|
+
if attempt == 2 : break
|
|
80
|
+
msg_url = ( re.sub(r'([^_]*)_[^/]*(/.*)', r'\1\2', msg_url) # strip region before retrying
|
|
81
|
+
if attempt == 0 and '-' in lang_code else f'{msg_base_url}/en/messages.json') # else use EN msgs
|
|
82
|
+
|
|
83
|
+
get_msgs.cached = msgs
|
|
84
|
+
get_msgs.cached_lang = lang_code
|
|
85
|
+
|
|
86
|
+
return sn(**msgs)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import os, sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from types import SimpleNamespace as sn
|
|
4
|
+
from typing import List, Optional, Union
|
|
5
|
+
if sys.platform == 'win32' : import colorama ; colorama.init() # enable ANSI color support
|
|
6
|
+
|
|
7
|
+
from . import color as colors, data as datalib, pkg
|
|
8
|
+
|
|
9
|
+
try : terminal_width = os.get_terminal_size()[0]
|
|
10
|
+
except OSError : terminal_width = 80
|
|
11
|
+
|
|
12
|
+
current_ver = datalib.json.read(Path(__file__).parent.parent.parent / 'data/package_data.json')['version']
|
|
13
|
+
next_maj_ver = pkg.get_next_maj_ver(current_ver)
|
|
14
|
+
_warned_keys = { 'cli': set(), 'config': set() }
|
|
15
|
+
|
|
16
|
+
def data(msg: str, *args, no_newline: bool = False, **kwargs) -> None:
|
|
17
|
+
print(f'\n{colors.bw}{msg.format(*args, **kwargs)}{colors.nc}', end='' if no_newline else None)
|
|
18
|
+
def dim(msg: str, *args, no_newline: bool = False, **kwargs) -> None:
|
|
19
|
+
print(f'\n{colors.gry}{msg.format(*args, **kwargs)}{colors.nc}', end='' if no_newline else None)
|
|
20
|
+
def docs_url(cli: sn) -> None : tip(f'{cli.msgs.tip_FOR_MORE_HELP_VISIT}:\n{cli.urls.docs}')
|
|
21
|
+
def error(msg: str, *args, **kwargs) -> None : print(f'\n{colors.br}ERROR: {msg.format(*args, **kwargs)}{colors.nc}')
|
|
22
|
+
def help_cmd(cli: sn) -> None : info(f"{cli.msgs.log_TYPE} '{cli.cmds[0]} --help' {cli.msgs.log_FOR_AVAIL_OPTIONS}\n")
|
|
23
|
+
def info(msg: str, *args, end: str = '', **kwargs) -> None:
|
|
24
|
+
print(f'\n{colors.by}{msg.format(*args, **kwargs)}{colors.nc}', end=end)
|
|
25
|
+
def line_break() : print()
|
|
26
|
+
def overwrite_print(msg: str, *args, **kwargs) -> None:
|
|
27
|
+
sys.stdout.write('\r' + msg.format(*args, **kwargs).ljust(terminal_width)[:terminal_width])
|
|
28
|
+
def success(msg: str, *args, **kwargs) -> None : print(f'\n{colors.bg}{msg.format(*args, **kwargs)}{colors.nc}')
|
|
29
|
+
def tip(msg: str, *args, **kwargs) -> None : print(f'\n{colors.bc}TIP: {msg.format(*args, **kwargs)}{colors.nc}')
|
|
30
|
+
def version(cli: sn) -> None:
|
|
31
|
+
print(f'\n{colors.by}{cli.name}\n{colors.bw}{cli.msgs.log_VERSION.lower()}: {cli.version}{colors.nc}')
|
|
32
|
+
def warn(msg: str, *args, **kwargs) -> None : print(f'\n{colors.bo}WARNING: {msg.format(*args, **kwargs)}{colors.nc}')
|
|
33
|
+
|
|
34
|
+
def cmd_docs_url_exit(cli: sn, msg: str = '', cmd: str = 'help') -> None:
|
|
35
|
+
if msg : error(msg)
|
|
36
|
+
help_cmd(cli)
|
|
37
|
+
docs_url(cli)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
def debug(msg: str, cli: Optional[sn] = None, *args, **kwargs) -> None:
|
|
41
|
+
from . import env
|
|
42
|
+
if not env.is_debug_mode() : return
|
|
43
|
+
|
|
44
|
+
# Init --debug [target]
|
|
45
|
+
debug_key=None
|
|
46
|
+
debug_argidx = sys.argv.index('--debug') if '--debug' in sys.argv else sys.argv.index('-V')
|
|
47
|
+
if debug_argidx +1 < len(sys.argv) and not sys.argv[debug_argidx +1].startswith('-'):
|
|
48
|
+
debug_key = sys.argv[debug_argidx +1].replace('-', '_')
|
|
49
|
+
|
|
50
|
+
if cli: # init data line
|
|
51
|
+
if debug_key:
|
|
52
|
+
data_val = getattr(cli.config, debug_key, f'cli.config key {debug_key!r} {cli.msgs.warn_NOT_FOUND.lower()}')
|
|
53
|
+
else:
|
|
54
|
+
data_val = cli.config
|
|
55
|
+
msg += f'\n{colors.gry}{data_val}{colors.nc}'
|
|
56
|
+
|
|
57
|
+
if args: # use 'em
|
|
58
|
+
msg = msg.format(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
print(f'\n{colors.by}DEBUG: {msg}{colors.nc}')
|
|
61
|
+
|
|
62
|
+
def package_vers(
|
|
63
|
+
pkgs: List[str], results: Union[Optional[str], List[Optional[str]]], cli: sn, scheme: str = 'rainbow'
|
|
64
|
+
) -> None:
|
|
65
|
+
results = [results] if not isinstance(results, list) else results
|
|
66
|
+
scheme_colors = getattr(colors.schemes, scheme)
|
|
67
|
+
for idx, (package, version) in enumerate(zip(pkgs, results)):
|
|
68
|
+
color = scheme_colors[idx % len(scheme_colors)]
|
|
69
|
+
msg = f'Python {version}' if version else f'{colors.gry}{cli.msgs.log_NO_PY_REQ_FOUND}{colors.nc}'
|
|
70
|
+
info(f'{color}{package}:{colors.nc} {msg}')
|
|
71
|
+
line_break()
|
|
72
|
+
|
|
73
|
+
def trunc(msg: str, end: str = '\n') -> None:
|
|
74
|
+
truncated_lines = [
|
|
75
|
+
line if len(line) < terminal_width else line[:terminal_width -4] + '...' for line in msg.splitlines()]
|
|
76
|
+
print('\n'.join(truncated_lines), end=end)
|
|
77
|
+
|
|
78
|
+
def warn_legacy_option(cli: sn, flag: str, source: str) -> None:
|
|
79
|
+
from . import settings
|
|
80
|
+
warned_set = _warned_keys[source]
|
|
81
|
+
if flag in warned_set : return
|
|
82
|
+
canonical_key = settings.get_canonical_key(flag)
|
|
83
|
+
msg = f"{ cli.msgs.warn_CONFIG_FILE_KEY if source == 'config' else cli.msgs.warn_CLI_OPTION } {flag!r}"
|
|
84
|
+
if canonical_key:
|
|
85
|
+
canonical_ctrl = getattr(settings.controls, canonical_key, None)
|
|
86
|
+
if source == 'cli' and canonical_ctrl:
|
|
87
|
+
flags = [arg for arg in getattr(canonical_ctrl, 'args', []) if arg.startswith('-')]
|
|
88
|
+
if flag.startswith('-') and len(flag) == 2: # show short flag replacement
|
|
89
|
+
display_key = min(flags, key=len) if flags else f"--{canonical_key.replace('_', '-')}"
|
|
90
|
+
else: # show long flag replacement
|
|
91
|
+
long_flags = [flag for flag in flags if flag.startswith('--')]
|
|
92
|
+
display_key = long_flags[0] if long_flags else f"--{canonical_key.replace('_', '-')}"
|
|
93
|
+
else:
|
|
94
|
+
display_key = canonical_key
|
|
95
|
+
msg += f' {cli.msgs.warn_HAS_BEEN_REPLACED_BY} {display_key!r}'
|
|
96
|
+
else:
|
|
97
|
+
msg += f' {cli.msgs.warn_NO_LONGER_HAS_ANY_EFFECT}'
|
|
98
|
+
msg += f' {cli.msgs.warn_AND_WILL_BE_REMOVED} @ v{next_maj_ver}'
|
|
99
|
+
warn(msg) ; warned_set.add(flag)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from types import SimpleNamespace as sn
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from . import log, string, url
|
|
6
|
+
|
|
7
|
+
controls = sn(
|
|
8
|
+
no_region=sn(
|
|
9
|
+
args=['-n', '--no-region', '--no-locale'], action='store_true', default=None),
|
|
10
|
+
help=sn(
|
|
11
|
+
args=['-h', '--help'], action='help'),
|
|
12
|
+
version=sn(
|
|
13
|
+
args=['-v', '--version'], action='store_true', exit=True, handler=lambda cli: log.version(cli)),
|
|
14
|
+
docs=sn(
|
|
15
|
+
args=['--docs'], action='store_true', exit=True, handler=lambda cli: url.open(cli.urls.docs)),
|
|
16
|
+
debug=sn(
|
|
17
|
+
args=['-V', '--debug'], nargs='?', const=True, metavar='TARGET_KEY')
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def get_canonical_key(key: str) -> Optional[str]:
|
|
21
|
+
if key.startswith('-'): # convert CLI arg to full key name
|
|
22
|
+
for ctrl_key, ctrl in vars(controls).items():
|
|
23
|
+
if key in getattr(ctrl, 'args', []):
|
|
24
|
+
key = ctrl_key
|
|
25
|
+
break
|
|
26
|
+
legacy_key = key if key.startswith('legacy_') else f'legacy_{key}'
|
|
27
|
+
legacy_ctrl = getattr(controls, legacy_key, None)
|
|
28
|
+
stripped_key = string.removeprefix(key, 'legacy_')
|
|
29
|
+
return legacy_ctrl.replaced_by if legacy_ctrl and hasattr(legacy_ctrl, 'replaced_by') \
|
|
30
|
+
else stripped_key if hasattr(controls, stripped_key) \
|
|
31
|
+
else None
|
|
32
|
+
|
|
33
|
+
def is_neg_key(key: str) -> bool:
|
|
34
|
+
import re
|
|
35
|
+
return bool(re.match(r'^(?:no|disable|exclude)_', string.removeprefix(key, 'legacy_')))
|
|
36
|
+
|
|
37
|
+
def load(cli: sn) -> None:
|
|
38
|
+
import argparse
|
|
39
|
+
from . import data
|
|
40
|
+
|
|
41
|
+
cli.config = sn()
|
|
42
|
+
|
|
43
|
+
# Assign help tips from cli.msgs
|
|
44
|
+
for ctrl_key, ctrl in vars(controls).items():
|
|
45
|
+
if ctrl_key.startswith('legacy_') : continue
|
|
46
|
+
if not hasattr(ctrl, 'help') : ctrl.help = getattr(cli.msgs, f'help_{ctrl_key.upper()}')
|
|
47
|
+
|
|
48
|
+
# Parse CLI args (overriding config file loads)
|
|
49
|
+
argp = argparse.ArgumentParser(description=cli.description, add_help=False)
|
|
50
|
+
valid_argparse_kwargs = {
|
|
51
|
+
'action', 'choices', 'const', 'default', 'dest', 'help', 'metavar', 'nargs', 'required', 'type', 'version'}
|
|
52
|
+
for ctrl_key, ctrl in vars(controls).items(): # add args to argp
|
|
53
|
+
kwargs = ctrl.__dict__.copy()
|
|
54
|
+
args = kwargs.pop('args')
|
|
55
|
+
argparse_kwargs = { key:val for key,val in kwargs.items() if key in valid_argparse_kwargs }
|
|
56
|
+
if ctrl_key.startswith('legacy_'): # copy canonical attrs first
|
|
57
|
+
canonical_key = get_canonical_key(ctrl_key)
|
|
58
|
+
if canonical_key: # adjust argparse_kwargs
|
|
59
|
+
canonical_ctrl = getattr(controls, canonical_key)
|
|
60
|
+
argparse_kwargs.update({
|
|
61
|
+
key:val for key,val in canonical_ctrl.__dict__.items() if key in valid_argparse_kwargs })
|
|
62
|
+
argparse_kwargs['dest'] = canonical_key
|
|
63
|
+
if is_neg_key(ctrl_key) != is_neg_key(canonical_key):
|
|
64
|
+
argparse_kwargs['action'] = 'store_false' if argparse_kwargs['action'] == 'store_true' \
|
|
65
|
+
else 'store_true'
|
|
66
|
+
for arg in args:
|
|
67
|
+
if arg in sys.argv[1:]:
|
|
68
|
+
log.warn_legacy_option(cli, arg, source='cli')
|
|
69
|
+
break
|
|
70
|
+
argp.add_argument(*args, **argparse_kwargs)
|
|
71
|
+
parsed_args, unknown_args = argp.parse_known_args()
|
|
72
|
+
exempt_flags = [] # exempt valid dash-less args from validation
|
|
73
|
+
exempt_flags.extend(arg.lstrip('-') for ctrl_key, ctrl in vars(controls).items()
|
|
74
|
+
if getattr(ctrl, 'subcmd', False)
|
|
75
|
+
for arg in ctrl.args if len(arg) > 2) # skip short flags
|
|
76
|
+
if unknown_args and not all(any(arg == exempt for exempt in exempt_flags) for arg in unknown_args):
|
|
77
|
+
log.cmd_docs_url_exit(cli, f"{cli.msgs.err_UNRECOGNIZED_ARGS}: {' '.join(unknown_args)}", cmd='help')
|
|
78
|
+
for ctrl_key, ctrl in vars(controls).items(): # process subcmds
|
|
79
|
+
if getattr(ctrl, 'subcmd', False) \
|
|
80
|
+
and next(arg for arg in ctrl.args if arg.startswith('--'))[2:] in sys.argv[1:]:
|
|
81
|
+
setattr(parsed_args, ctrl_key, True)
|
|
82
|
+
applied_args = []
|
|
83
|
+
for arg in sys.argv[1:]:
|
|
84
|
+
if not arg.startswith('-') : continue
|
|
85
|
+
base_arg = arg.split('=')[0]
|
|
86
|
+
for ctrl_key, ctrl in vars(controls).items():
|
|
87
|
+
if base_arg in getattr(ctrl, 'args', []):
|
|
88
|
+
dest = get_canonical_key(ctrl_key) or ctrl_key if ctrl_key.startswith('legacy_') else ctrl_key
|
|
89
|
+
parsed_val = getattr(parsed_args, dest, None)
|
|
90
|
+
if parsed_val is not None:
|
|
91
|
+
setattr(cli.config, dest, parsed_val)
|
|
92
|
+
applied_args.append(arg)
|
|
93
|
+
break
|
|
94
|
+
log.debug(f'Args parsed! {log.colors.bg}{len(applied_args)} args applied {applied_args}', cli)
|
|
95
|
+
|
|
96
|
+
# Apply parsers/default_vals
|
|
97
|
+
for ctrl_key, ctrl in vars(controls).items():
|
|
98
|
+
if not hasattr(cli.config, ctrl_key):
|
|
99
|
+
setattr(cli.config, ctrl_key, ctrl.default_val if hasattr(ctrl, 'default_val') else None)
|
|
100
|
+
config_val = getattr(cli.config, ctrl_key)
|
|
101
|
+
if getattr(ctrl, 'parser', '') == 'csv':
|
|
102
|
+
setattr(cli.config, ctrl_key, data.csv.parse(config_val))
|
|
103
|
+
log.debug('All cli.config vals set!', cli)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
def get(url: str, timeout: int = 5, encoding: str = 'utf-8') -> str:
|
|
4
|
+
from urllib.error import URLError
|
|
5
|
+
from urllib.request import urlopen
|
|
6
|
+
url = validate(url)
|
|
7
|
+
try:
|
|
8
|
+
with urlopen(url, timeout=timeout) as resp:
|
|
9
|
+
return resp.read().decode(encoding)
|
|
10
|
+
except URLError as err:
|
|
11
|
+
raise RuntimeError(f'Failed to fetch from {url}: {err}')
|
|
12
|
+
|
|
13
|
+
def open(url: str) -> None:
|
|
14
|
+
import webbrowser
|
|
15
|
+
url = validate(url)
|
|
16
|
+
try : webbrowser.open(url)
|
|
17
|
+
except Exception as err:
|
|
18
|
+
raise RuntimeError(f'Failed to open {url} in browser: {err}')
|
|
19
|
+
|
|
20
|
+
def validate(url: str, allowed_schemes: Tuple[str, ...] = ('http', 'https'),
|
|
21
|
+
allowed_domains: Optional[List[str]] = None) -> str:
|
|
22
|
+
from urllib.parse import urlparse
|
|
23
|
+
parsed_url = urlparse(url)
|
|
24
|
+
|
|
25
|
+
if parsed_url.scheme not in allowed_schemes:
|
|
26
|
+
raise ValueError(f'URL scheme {parsed_url.scheme!r} not allowed. Allowed: {allowed_schemes}')
|
|
27
|
+
|
|
28
|
+
if allowed_domains:
|
|
29
|
+
input_domain = parsed_url.netloc.lower()
|
|
30
|
+
if not any(input_domain.endswith(allowed_domain.lower()) for allowed_domain in allowed_domains):
|
|
31
|
+
raise ValueError(f'URL domain {input_domain!r} not allowed. Allowed: {allowed_domains}')
|
|
32
|
+
|
|
33
|
+
return url
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"log_TYPE": { "message": "Type" },
|
|
3
|
+
"log_FOR_AVAIL_OPTIONS": { "message": "for available options" },
|
|
4
|
+
"log_VERSION": { "message": "Version" },
|
|
5
|
+
"tip_FOR_MORE_HELP_VISIT": { "message": "For more help, visit" },
|
|
6
|
+
"warn_NOT_FOUND": { "message": "Not found" },
|
|
7
|
+
"warn_CLI_OPTION": { "message": "CLI option" },
|
|
8
|
+
"warn_HAS_BEEN_REPLACED_BY": { "message": "has been replaced by" },
|
|
9
|
+
"warn_AND_WILL_BE_REMOVED": { "message": "and will be removed" },
|
|
10
|
+
"warn_NO_LONGER_HAS_ANY_EFFECT": { "message": "no longer has any effect" },
|
|
11
|
+
"err_UNRECOGNIZED_ARGS": { "message": "Unrecognized argument(s)" },
|
|
12
|
+
"help_NO_REGION": { "message": "Don't include region when available" },
|
|
13
|
+
"help_HELP": { "message": "Show help screen" },
|
|
14
|
+
"help_VERSION": { "message": "Show version" },
|
|
15
|
+
"help_DOCS": { "message": "Open docs URL" },
|
|
16
|
+
"help_DEBUG": { "message": "Show debug logs" }
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sys-lang",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Detect the system language.",
|
|
5
|
+
"cmds": [
|
|
6
|
+
"sys-lang", "sys-language", "system-lang", "system-language",
|
|
7
|
+
"get-sys-lang", "get-sys-language", "get-system-lang", "get-system-language",
|
|
8
|
+
"syslang", "syslanguage", "systemlang", "systemlanguage",
|
|
9
|
+
"getsyslang", "getsyslanguage", "getsystemlang", "getsystemlanguage"
|
|
10
|
+
],
|
|
11
|
+
"author": { "name": "Adam Lui", "email": "adam@kudoai.com", "url": "https://github.com/adamlui" },
|
|
12
|
+
"urls": {
|
|
13
|
+
"docs": "https://github.com/adamlui/python-utils/tree/main/sys-lang/docs",
|
|
14
|
+
"funding": {
|
|
15
|
+
"cashapp": "https://cash.app/$adamlui",
|
|
16
|
+
"github": "https://github.com/sponsors/adamlui",
|
|
17
|
+
"kofi": "https://ko-fi.com/adamlui",
|
|
18
|
+
"paypal": "https://paypal.me/adamlui"
|
|
19
|
+
},
|
|
20
|
+
"github": "https://github.com/adamlui/python-utils",
|
|
21
|
+
"jsdelivr": "https://cdn.jsdelivr.net/gh/adamlui/python-utils",
|
|
22
|
+
"pypi": "https://pypi.org/project/sys-lang/",
|
|
23
|
+
"pypistats": "https://pypistats.org/packages/sys-lang",
|
|
24
|
+
"support": "https://github.com/adamlui/python-utils/issues"
|
|
25
|
+
},
|
|
26
|
+
"commit_hashes": {
|
|
27
|
+
"locales": "c5d4a4f"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sys-lang
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Detect the system language.
|
|
5
|
+
Author-email: Adam Lui <adam@kudoai.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Changelog, https://github.com/adamlui/python-utils/releases/tag/sys-lang-1.0.0
|
|
8
|
+
Project-URL: Documentation, https://github.com/adamlui/python-utils/tree/main/sys-lang/docs
|
|
9
|
+
Project-URL: Funding, https://github.com/sponsors/adamlui
|
|
10
|
+
Project-URL: Homepage, https://github.com/adamlui/python-utils/tree/main/sys-lang/#readme
|
|
11
|
+
Project-URL: Issues, https://github.com/adamlui/python-utils/issues
|
|
12
|
+
Project-URL: PyPI Stats, https://pepy.tech/projects/sys-lang
|
|
13
|
+
Project-URL: Releases, https://github.com/adamlui/python-utils/releases
|
|
14
|
+
Project-URL: Repository, https://github.com/adamlui/python-utils
|
|
15
|
+
Keywords: api,cli,console,detect,detection,dev-tool,environment,language,locale,shell,system-language,system-locale,terminal,utility
|
|
16
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
17
|
+
Classifier: Environment :: Console
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
20
|
+
Classifier: Intended Audience :: Information Technology
|
|
21
|
+
Classifier: Intended Audience :: System Administrators
|
|
22
|
+
Classifier: Natural Language :: English
|
|
23
|
+
Classifier: Operating System :: OS Independent
|
|
24
|
+
Classifier: Programming Language :: Python
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
35
|
+
Classifier: Topic :: Software Development
|
|
36
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
37
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
38
|
+
Classifier: Topic :: Software Development :: Localization
|
|
39
|
+
Classifier: Topic :: System
|
|
40
|
+
Classifier: Topic :: System :: Shells
|
|
41
|
+
Classifier: Topic :: Terminals
|
|
42
|
+
Classifier: Topic :: Utilities
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: <4,>=3.8
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: docs/LICENSE.md
|
|
47
|
+
Requires-Dist: colorama<1,>=0.4.6; platform_system == "Windows"
|
|
48
|
+
Requires-Dist: is_unicode_supported<2,>=1.1.2
|
|
49
|
+
Requires-Dist: json5<1,>=0.13.0
|
|
50
|
+
Requires-Dist: non-latin-locales<2,>=1.0.1
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: nox>=2026.2.9; extra == "dev"
|
|
53
|
+
Requires-Dist: remove-json-keys<2,>=1.9.2; extra == "dev"
|
|
54
|
+
Requires-Dist: tomli<3,>=2.4.0; extra == "dev"
|
|
55
|
+
Requires-Dist: tomli-w<2,>=1.2.0; extra == "dev"
|
|
56
|
+
Requires-Dist: translate-messages<2,>=1.9.1; extra == "dev"
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
|
|
59
|
+
<a id="top"></a>
|
|
60
|
+
|
|
61
|
+
# > sys-lang
|
|
62
|
+
|
|
63
|
+
<a href="https://github.com/adamlui/python-utils/releases/tag/sys-lang-1.0.0">
|
|
64
|
+
<img height=31 src="https://img.shields.io/badge/Latest_Build-1.0.0-32fcee.svg?logo=icinga&logoColor=white&labelColor=464646&style=for-the-badge"></a>
|
|
65
|
+
<a href="https://github.com/adamlui/python-utils/blob/main/sys-lang/docs/LICENSE.md">
|
|
66
|
+
<img height=31 src="https://img.shields.io/badge/License-MIT-f99b27.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
|
|
67
|
+
<a href="https://www.codefactor.io/repository/github/adamlui/python-utils">
|
|
68
|
+
<img height=31 src="https://img.shields.io/codefactor/grade/github/adamlui/python-utils?label=Code+Quality&logo=codefactor&logoColor=white&labelColor=464646&color=a0fc55&style=for-the-badge"></a>
|
|
69
|
+
<a href="https://sonarcloud.io/component_measures?metric=vulnerabilities&selected=adamlui_python-utils%3Asys-lang&id=adamlui_python-utils">
|
|
70
|
+
<img height=31 src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3Dadamlui_python-utils%26metricKeys%3Dvulnerabilities&query=%24.component.measures.0.value&style=for-the-badge&logo=sonar&logoColor=white&labelColor=464646&label=Vulnerabilities&color=fafc74"></a>
|
|
71
|
+
|
|
72
|
+
> ### _Detect the system language._
|
|
73
|
+
|
|
74
|
+
Returns ISO 639-1 (e.g. `en`) or ISO 3166-1 alpha-2-appended (`en_US`) code for user's preferred language. On Windows, queries `Get-Culture` via PowerShell. On *nix systems, reads `LC_ALL`, `LC_MESSAGES`, `LANG`, and `LANGUAGE`.
|
|
75
|
+
|
|
76
|
+
## ⚡ Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install sys-lang
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 💻 Command line usage
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
sys-lang # or syslang
|
|
86
|
+
# e.g. => 'en_US'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
CLI options:
|
|
90
|
+
|
|
91
|
+
| Option | Description
|
|
92
|
+
| ------------------- | ------------------------------------
|
|
93
|
+
| `-n`, `--no-region` | Don't include region when available
|
|
94
|
+
| `-h`, `--help` | Show help screen
|
|
95
|
+
| `-v`, `--version` | Show version
|
|
96
|
+
| `--docs` | Open docs URL
|
|
97
|
+
|
|
98
|
+
## 🔌 API usage
|
|
99
|
+
|
|
100
|
+
```py
|
|
101
|
+
from sys_lang import get_sys_lang
|
|
102
|
+
|
|
103
|
+
print(get_sys_lang()) # e.g. => 'zh_HK'
|
|
104
|
+
print(get_sys_lang(region=False)) # e.g. => 'zh'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## MIT License
|
|
108
|
+
|
|
109
|
+
Copyright © 2026 [Adam Lui](https://github.com/adamlui)
|
|
110
|
+
|
|
111
|
+
## Related
|
|
112
|
+
|
|
113
|
+
🇪🇸 [latin-locales](https://github.com/adamlui/python-utils/tree/main/latin-locales/#readme) - ISO 639-1 (2-letter) codes for Latin locales.
|
|
114
|
+
<br>🇨🇳 [non-latin-locales](https://github.com/adamlui/python-utils/tree/main/non-latin-locales/#readme) - ISO 639-1 (2-letter) codes for non-Latin locales.
|
|
115
|
+
<br>🌍 [translate-messages](https://github.com/adamlui/python-utils/tree/main/translate-messages/#readme) - Translate `en/messages.json` (chrome.i18n format) to 100+ locales automatically.
|
|
116
|
+
<br>🈶 [is-unicode-supported](https://github.com/adamlui/python-utils/tree/main/is-unicode-supported/#readme) - Detect whether the terminal supports advanced Unicode.
|
|
117
|
+
|
|
118
|
+
#
|
|
119
|
+
|
|
120
|
+
<picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/adamlui/python-utils@760599e/assets/images/icons/home/white/icon32x27.png"><img height=13 src="https://cdn.jsdelivr.net/gh/adamlui/python-utils@760599e/assets/images/icons/home/dark-gray/icon32x27.png"></picture> <a href=https://github.com/adamlui/python-utils/#readme>**More Python utilities**</a> /
|
|
121
|
+
<a href="#top">Back to top ↑</a>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
docs/LICENSE.md
|
|
3
|
+
docs/README.md
|
|
4
|
+
src/sys_lang/__init__.py
|
|
5
|
+
src/sys_lang/api.py
|
|
6
|
+
src/sys_lang.egg-info/PKG-INFO
|
|
7
|
+
src/sys_lang.egg-info/SOURCES.txt
|
|
8
|
+
src/sys_lang.egg-info/dependency_links.txt
|
|
9
|
+
src/sys_lang.egg-info/entry_points.txt
|
|
10
|
+
src/sys_lang.egg-info/requires.txt
|
|
11
|
+
src/sys_lang.egg-info/top_level.txt
|
|
12
|
+
src/sys_lang/cli/__init__.py
|
|
13
|
+
src/sys_lang/cli/__main__.py
|
|
14
|
+
src/sys_lang/cli/lib/color.py
|
|
15
|
+
src/sys_lang/cli/lib/env.py
|
|
16
|
+
src/sys_lang/cli/lib/init.py
|
|
17
|
+
src/sys_lang/cli/lib/jsdelivr.py
|
|
18
|
+
src/sys_lang/cli/lib/language.py
|
|
19
|
+
src/sys_lang/cli/lib/log.py
|
|
20
|
+
src/sys_lang/cli/lib/pkg.py
|
|
21
|
+
src/sys_lang/cli/lib/settings.py
|
|
22
|
+
src/sys_lang/cli/lib/string.py
|
|
23
|
+
src/sys_lang/cli/lib/url.py
|
|
24
|
+
src/sys_lang/cli/lib/data/__init__.py
|
|
25
|
+
src/sys_lang/cli/lib/data/csv.py
|
|
26
|
+
src/sys_lang/cli/lib/data/file.py
|
|
27
|
+
src/sys_lang/cli/lib/data/json.py
|
|
28
|
+
src/sys_lang/cli/lib/data/sns.py
|
|
29
|
+
src/sys_lang/data/package_data.json
|
|
30
|
+
src/sys_lang/data/_locales/en/messages.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
get-sys-lang = sys_lang.cli.__main__:main
|
|
3
|
+
get-sys-language = sys_lang.cli.__main__:main
|
|
4
|
+
get-system-lang = sys_lang.cli.__main__:main
|
|
5
|
+
get-system-language = sys_lang.cli.__main__:main
|
|
6
|
+
getsyslang = sys_lang.cli.__main__:main
|
|
7
|
+
getsyslanguage = sys_lang.cli.__main__:main
|
|
8
|
+
getsystemlang = sys_lang.cli.__main__:main
|
|
9
|
+
getsystemlanguage = sys_lang.cli.__main__:main
|
|
10
|
+
sys-lang = sys_lang.cli.__main__:main
|
|
11
|
+
sys-language = sys_lang.cli.__main__:main
|
|
12
|
+
syslang = sys_lang.cli.__main__:main
|
|
13
|
+
syslanguage = sys_lang.cli.__main__:main
|
|
14
|
+
system-lang = sys_lang.cli.__main__:main
|
|
15
|
+
system-language = sys_lang.cli.__main__:main
|
|
16
|
+
systemlang = sys_lang.cli.__main__:main
|
|
17
|
+
systemlanguage = sys_lang.cli.__main__:main
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
is_unicode_supported<2,>=1.1.2
|
|
2
|
+
json5<1,>=0.13.0
|
|
3
|
+
non-latin-locales<2,>=1.0.1
|
|
4
|
+
|
|
5
|
+
[:platform_system == "Windows"]
|
|
6
|
+
colorama<1,>=0.4.6
|
|
7
|
+
|
|
8
|
+
[dev]
|
|
9
|
+
nox>=2026.2.9
|
|
10
|
+
remove-json-keys<2,>=1.9.2
|
|
11
|
+
tomli<3,>=2.4.0
|
|
12
|
+
tomli-w<2,>=1.2.0
|
|
13
|
+
translate-messages<2,>=1.9.1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sys_lang
|