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.
Files changed (32) hide show
  1. sys_lang-1.0.0/PKG-INFO +121 -0
  2. sys_lang-1.0.0/docs/LICENSE.md +21 -0
  3. sys_lang-1.0.0/docs/README.md +63 -0
  4. sys_lang-1.0.0/pyproject.toml +120 -0
  5. sys_lang-1.0.0/setup.cfg +4 -0
  6. sys_lang-1.0.0/src/sys_lang/__init__.py +3 -0
  7. sys_lang-1.0.0/src/sys_lang/api.py +22 -0
  8. sys_lang-1.0.0/src/sys_lang/cli/__init__.py +0 -0
  9. sys_lang-1.0.0/src/sys_lang/cli/__main__.py +17 -0
  10. sys_lang-1.0.0/src/sys_lang/cli/lib/color.py +30 -0
  11. sys_lang-1.0.0/src/sys_lang/cli/lib/data/__init__.py +3 -0
  12. sys_lang-1.0.0/src/sys_lang/cli/lib/data/csv.py +5 -0
  13. sys_lang-1.0.0/src/sys_lang/cli/lib/data/file.py +23 -0
  14. sys_lang-1.0.0/src/sys_lang/cli/lib/data/json.py +108 -0
  15. sys_lang-1.0.0/src/sys_lang/cli/lib/data/sns.py +8 -0
  16. sys_lang-1.0.0/src/sys_lang/cli/lib/env.py +4 -0
  17. sys_lang-1.0.0/src/sys_lang/cli/lib/init.py +13 -0
  18. sys_lang-1.0.0/src/sys_lang/cli/lib/jsdelivr.py +9 -0
  19. sys_lang-1.0.0/src/sys_lang/cli/lib/language.py +86 -0
  20. sys_lang-1.0.0/src/sys_lang/cli/lib/log.py +99 -0
  21. sys_lang-1.0.0/src/sys_lang/cli/lib/pkg.py +6 -0
  22. sys_lang-1.0.0/src/sys_lang/cli/lib/settings.py +103 -0
  23. sys_lang-1.0.0/src/sys_lang/cli/lib/string.py +2 -0
  24. sys_lang-1.0.0/src/sys_lang/cli/lib/url.py +33 -0
  25. sys_lang-1.0.0/src/sys_lang/data/_locales/en/messages.json +17 -0
  26. sys_lang-1.0.0/src/sys_lang/data/package_data.json +29 -0
  27. sys_lang-1.0.0/src/sys_lang.egg-info/PKG-INFO +121 -0
  28. sys_lang-1.0.0/src/sys_lang.egg-info/SOURCES.txt +30 -0
  29. sys_lang-1.0.0/src/sys_lang.egg-info/dependency_links.txt +1 -0
  30. sys_lang-1.0.0/src/sys_lang.egg-info/entry_points.txt +17 -0
  31. sys_lang-1.0.0/src/sys_lang.egg-info/requires.txt +13 -0
  32. sys_lang-1.0.0/src/sys_lang.egg-info/top_level.txt +1 -0
@@ -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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .api import get_sys_lang
2
+
3
+ __all__ = ['get_sys_lang']
@@ -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,3 @@
1
+ from . import csv, file, json, sns
2
+
3
+ __all__ = ['csv', 'file', 'json', 'sns']
@@ -0,0 +1,5 @@
1
+ from typing import List
2
+
3
+ def parse(val: str) -> List[str]:
4
+ if not val : return []
5
+ return [item.strip() for item in val.split(',') if item.strip()]
@@ -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,8 @@
1
+ from types import SimpleNamespace as sn
2
+ from typing import Any, Dict
3
+
4
+ def from_dict(obj: Dict[str, Any]) -> sn:
5
+ for key, val in obj.items():
6
+ if isinstance(val, dict):
7
+ obj[key] = from_dict(val)
8
+ return sn(**obj)
@@ -0,0 +1,4 @@
1
+ import sys
2
+
3
+ def is_debug_mode() -> bool:
4
+ return any(arg in ('--debug', '-V') for arg in sys.argv[1:])
@@ -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,6 @@
1
+ import re
2
+
3
+ def get_next_maj_ver(version: str) -> str: # e.g. '1.2.3' -> '2.0.0'
4
+ major = re.match(r'^(\d+)\..*', version)
5
+ if not major : raise ValueError(f'Invalid version string: {version!r}')
6
+ return f'{ int(major.group(1)) +1 }.0.0'
@@ -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,2 @@
1
+ def removeprefix(str: str, prefix: str) -> str:
2
+ return str[len(prefix):] if str.startswith(prefix) else str
@@ -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,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