autolang 0.1.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.
autolang-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Heerozh
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,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: autolang
3
+ Version: 0.1.0
4
+ Summary: Transparent i18n for Python f-strings.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Dist: babel>=2.18.0
8
+ Requires-Dist: executing>=2.2.1
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Autolang
13
+
14
+ `Autolang` is an experimental i18n library for Python `f-string` call sites.
15
+ It lets you bind a module-level `tt` function, write `tt(f"...")`, reconstruct the original template at runtime, look up a translated template from TOML, and re-evaluate the translated placeholders with Babel-aware formatting.
16
+
17
+ ## Status
18
+
19
+ This project is under active development.
20
+
21
+ Stable today:
22
+ - `install(locale, locale_dir)` to create an isolated translator instance
23
+ - `TransparentTranslator` for explicit instances
24
+ - TOML-backed translation lookup
25
+ - Babel `fmt.*` placeholder support inside translated templates
26
+ - Per-instance call-site cache with `reload()` and `clear_cache()`
27
+ - `tt init` and `tt sync` for locale template bootstrapping and synchronization
28
+
29
+ Planned:
30
+ - AI-assisted translation generation
31
+ - Better extraction and review workflows
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install autolang
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ Project layout:
42
+
43
+ ```text
44
+ your_app/
45
+ app.py
46
+ locales/
47
+ es.toml
48
+ ```
49
+
50
+ `locales/es.toml`
51
+
52
+ ```toml
53
+ "Hello {name}" = "Hola {name}"
54
+ "Today is {fmt.date(now, format='short')}" = "Hoy es {fmt.date(now, format='short')}"
55
+ ```
56
+
57
+ `app.py`
58
+
59
+ ```python
60
+ from datetime import datetime
61
+
62
+ from babel import Locale
63
+ from babel.support import Format
64
+
65
+ from autolang import install
66
+
67
+ translator = install("es", "locales")
68
+ tt = translator.translate
69
+
70
+ # A local fmt object keeps the original f-string valid before translation happens.
71
+ fmt = Format(Locale.parse("en"))
72
+
73
+ name = "Alice"
74
+ now = datetime(2026, 3, 11)
75
+
76
+ print(tt(f"Hello {name}"))
77
+ print(tt(f"Today is {fmt.date(now, format='short')}"))
78
+ ```
79
+
80
+ Example output:
81
+
82
+ ```text
83
+ Hola Alice
84
+ Hoy es 11/3/26
85
+ ```
86
+
87
+ ## Public API
88
+
89
+ ```python
90
+ from autolang import (
91
+ TransparentTranslator,
92
+ install,
93
+ )
94
+ ```
95
+
96
+ - `install(locale_str, locale_dir="locales")` creates and returns a translator instance.
97
+ - `TransparentTranslator(locale_str, locale_dir="locales")` creates an explicit translator instance with its own cache.
98
+ - `translator.translate(text)` translates the current call site.
99
+ - `translator.reload()` reloads the instance locale file and clears its cached call-site entries.
100
+ - `translator.clear_cache()` clears the instance cache without reloading files.
101
+
102
+ ## Module Setup
103
+
104
+ The recommended pattern is to initialize a module-level `tt` variable once and then call `tt(...)` everywhere in that module.
105
+
106
+ ```python
107
+ from autolang import install
108
+
109
+ translator = install("es", "locales")
110
+ tt = translator.translate
111
+ ```
112
+
113
+ ## CLI
114
+
115
+ The project also ships a short developer CLI command: `tt`.
116
+
117
+ Translate all locale TOML files by filling entries still marked as `NO_TRANSLATION` through an OpenAI-compatible API:
118
+
119
+ ```bash
120
+ tt translate \
121
+ --locale-dir locales \
122
+ --model gpt-4.1-mini \
123
+ --api-key "$OPENAI_API_KEY"
124
+ ```
125
+
126
+ By default, the command:
127
+ - discovers every `*.toml` file under the locale directory as a translation target
128
+ - uses each TOML key as the source template text
129
+ - reads cue text from `.<locale-dir>_cue/*.toml` when available
130
+ - assumes keys may be mixed-language and lets the model decide per item whether translation is needed
131
+ - only translates locale entries whose current value is `NO_TRANSLATION`
132
+ - sends translation requests in batches and can execute multiple batches concurrently
133
+ - validates returned JSON and placeholder compatibility before writing files
134
+
135
+ Useful flags:
136
+ - `--overwrite` to force re-translation of existing target values
137
+ - `--dry-run` to preview work without writing files
138
+ - `--batch-size 20` to control how many entries are sent in one model request
139
+ - `--workers 4` to control concurrent batch requests
140
+ - `--base-url` to point at any OpenAI-compatible endpoint
141
+
142
+ The CLI also reads these environment variables:
143
+ - `TT_API_KEY` or `OPENAI_API_KEY`
144
+ - `TT_BASE_URL` or `OPENAI_BASE_URL`
145
+ - `TT_MODEL` or `OPENAI_MODEL`
146
+
147
+ Initialize locale TOML files from collected `tt(...)` templates:
148
+
149
+ ```bash
150
+ tt init \
151
+ --source src \
152
+ --locale-dir locales \
153
+ --locales en es
154
+ ```
155
+
156
+ By default, the command:
157
+ - scans Python files under `--source`
158
+ - extracts `tt("...")` and `tt(f"...")` call sites through a Babel-compatible extractor
159
+ - skips hidden and cache/build directories such as `.git`, `.venv`, and `__pycache__`
160
+ - creates one TOML file per requested locale
161
+ - writes every collected key as `"source" = "NO_TRANSLATION"`
162
+ - writes static cue entries into matching files under `.locales_cue/`
163
+ - includes per-placeholder context such as the nearest assignment, parameter annotation, allowed `fmt.*` candidates, and a recommended candidate when confidence is high
164
+
165
+ Sync collected templates across existing locale TOML files:
166
+
167
+ ```bash
168
+ tt sync \
169
+ --source src \
170
+ --locale-dir locales
171
+ ```
172
+
173
+ By default, the command:
174
+ - scans Python files under `--source`
175
+ - requires at least one existing `*.toml` locale file under `--locale-dir`
176
+ - keeps existing translated values for keys that are still present in source
177
+ - writes missing keys as `"source" = "NO_TRANSLATION"`
178
+ - removes stale keys that are no longer collected from source
179
+ - rewrites cue files under `.locales_cue/` to match the current template set
180
+
181
+ ## How It Works
182
+
183
+ At a high level:
184
+
185
+ 1. You call `tt(f"...")`.
186
+ Here `tt = translator.translate`.
187
+ 2. The library inspects the caller frame and maps it back to the AST node for that exact call site.
188
+ 3. It rebuilds the source template, for example `Hello {name}`.
189
+ 4. It loads the translated template from TOML.
190
+ 5. It compiles the translated template as an f-string expression and caches it per translator instance.
191
+ 6. Later calls from the same bytecode location reuse the cached compiled expression.
192
+
193
+ ## Constraints
194
+
195
+ This is not a drop-in replacement for mature gettext tooling yet.
196
+
197
+ - The main path is designed for direct `tt(f"...")` usage after binding `tt = translator.translate`.
198
+ - Library code should keep its own translator instance and bind its own `tt` function in module scope.
199
+ - Translation lookup is currently a flat TOML key-value map.
200
+ - Static cue collection writes analysis data into a sibling hidden directory such as `.locales_cue`.
201
+ - If translated placeholder expressions are invalid or fail at evaluation time, the library falls back to the original rendered text.
202
+ - The library currently relies on runtime frame inspection and AST recovery, so unusual execution environments may behave differently.
203
+
204
+ ## Development
205
+
206
+ Run tests:
207
+
208
+ ```bash
209
+ uv run pytest -q
210
+ ```
211
+
212
+ ## Roadmap
213
+
214
+ - AI-generated translations from source templates and static cues
215
+ - Locale file generation and diffing
216
+ - Better developer tooling around extraction, validation, and review
@@ -0,0 +1,205 @@
1
+ # Autolang
2
+
3
+ `Autolang` is an experimental i18n library for Python `f-string` call sites.
4
+ It lets you bind a module-level `tt` function, write `tt(f"...")`, reconstruct the original template at runtime, look up a translated template from TOML, and re-evaluate the translated placeholders with Babel-aware formatting.
5
+
6
+ ## Status
7
+
8
+ This project is under active development.
9
+
10
+ Stable today:
11
+ - `install(locale, locale_dir)` to create an isolated translator instance
12
+ - `TransparentTranslator` for explicit instances
13
+ - TOML-backed translation lookup
14
+ - Babel `fmt.*` placeholder support inside translated templates
15
+ - Per-instance call-site cache with `reload()` and `clear_cache()`
16
+ - `tt init` and `tt sync` for locale template bootstrapping and synchronization
17
+
18
+ Planned:
19
+ - AI-assisted translation generation
20
+ - Better extraction and review workflows
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install autolang
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ Project layout:
31
+
32
+ ```text
33
+ your_app/
34
+ app.py
35
+ locales/
36
+ es.toml
37
+ ```
38
+
39
+ `locales/es.toml`
40
+
41
+ ```toml
42
+ "Hello {name}" = "Hola {name}"
43
+ "Today is {fmt.date(now, format='short')}" = "Hoy es {fmt.date(now, format='short')}"
44
+ ```
45
+
46
+ `app.py`
47
+
48
+ ```python
49
+ from datetime import datetime
50
+
51
+ from babel import Locale
52
+ from babel.support import Format
53
+
54
+ from autolang import install
55
+
56
+ translator = install("es", "locales")
57
+ tt = translator.translate
58
+
59
+ # A local fmt object keeps the original f-string valid before translation happens.
60
+ fmt = Format(Locale.parse("en"))
61
+
62
+ name = "Alice"
63
+ now = datetime(2026, 3, 11)
64
+
65
+ print(tt(f"Hello {name}"))
66
+ print(tt(f"Today is {fmt.date(now, format='short')}"))
67
+ ```
68
+
69
+ Example output:
70
+
71
+ ```text
72
+ Hola Alice
73
+ Hoy es 11/3/26
74
+ ```
75
+
76
+ ## Public API
77
+
78
+ ```python
79
+ from autolang import (
80
+ TransparentTranslator,
81
+ install,
82
+ )
83
+ ```
84
+
85
+ - `install(locale_str, locale_dir="locales")` creates and returns a translator instance.
86
+ - `TransparentTranslator(locale_str, locale_dir="locales")` creates an explicit translator instance with its own cache.
87
+ - `translator.translate(text)` translates the current call site.
88
+ - `translator.reload()` reloads the instance locale file and clears its cached call-site entries.
89
+ - `translator.clear_cache()` clears the instance cache without reloading files.
90
+
91
+ ## Module Setup
92
+
93
+ The recommended pattern is to initialize a module-level `tt` variable once and then call `tt(...)` everywhere in that module.
94
+
95
+ ```python
96
+ from autolang import install
97
+
98
+ translator = install("es", "locales")
99
+ tt = translator.translate
100
+ ```
101
+
102
+ ## CLI
103
+
104
+ The project also ships a short developer CLI command: `tt`.
105
+
106
+ Translate all locale TOML files by filling entries still marked as `NO_TRANSLATION` through an OpenAI-compatible API:
107
+
108
+ ```bash
109
+ tt translate \
110
+ --locale-dir locales \
111
+ --model gpt-4.1-mini \
112
+ --api-key "$OPENAI_API_KEY"
113
+ ```
114
+
115
+ By default, the command:
116
+ - discovers every `*.toml` file under the locale directory as a translation target
117
+ - uses each TOML key as the source template text
118
+ - reads cue text from `.<locale-dir>_cue/*.toml` when available
119
+ - assumes keys may be mixed-language and lets the model decide per item whether translation is needed
120
+ - only translates locale entries whose current value is `NO_TRANSLATION`
121
+ - sends translation requests in batches and can execute multiple batches concurrently
122
+ - validates returned JSON and placeholder compatibility before writing files
123
+
124
+ Useful flags:
125
+ - `--overwrite` to force re-translation of existing target values
126
+ - `--dry-run` to preview work without writing files
127
+ - `--batch-size 20` to control how many entries are sent in one model request
128
+ - `--workers 4` to control concurrent batch requests
129
+ - `--base-url` to point at any OpenAI-compatible endpoint
130
+
131
+ The CLI also reads these environment variables:
132
+ - `TT_API_KEY` or `OPENAI_API_KEY`
133
+ - `TT_BASE_URL` or `OPENAI_BASE_URL`
134
+ - `TT_MODEL` or `OPENAI_MODEL`
135
+
136
+ Initialize locale TOML files from collected `tt(...)` templates:
137
+
138
+ ```bash
139
+ tt init \
140
+ --source src \
141
+ --locale-dir locales \
142
+ --locales en es
143
+ ```
144
+
145
+ By default, the command:
146
+ - scans Python files under `--source`
147
+ - extracts `tt("...")` and `tt(f"...")` call sites through a Babel-compatible extractor
148
+ - skips hidden and cache/build directories such as `.git`, `.venv`, and `__pycache__`
149
+ - creates one TOML file per requested locale
150
+ - writes every collected key as `"source" = "NO_TRANSLATION"`
151
+ - writes static cue entries into matching files under `.locales_cue/`
152
+ - includes per-placeholder context such as the nearest assignment, parameter annotation, allowed `fmt.*` candidates, and a recommended candidate when confidence is high
153
+
154
+ Sync collected templates across existing locale TOML files:
155
+
156
+ ```bash
157
+ tt sync \
158
+ --source src \
159
+ --locale-dir locales
160
+ ```
161
+
162
+ By default, the command:
163
+ - scans Python files under `--source`
164
+ - requires at least one existing `*.toml` locale file under `--locale-dir`
165
+ - keeps existing translated values for keys that are still present in source
166
+ - writes missing keys as `"source" = "NO_TRANSLATION"`
167
+ - removes stale keys that are no longer collected from source
168
+ - rewrites cue files under `.locales_cue/` to match the current template set
169
+
170
+ ## How It Works
171
+
172
+ At a high level:
173
+
174
+ 1. You call `tt(f"...")`.
175
+ Here `tt = translator.translate`.
176
+ 2. The library inspects the caller frame and maps it back to the AST node for that exact call site.
177
+ 3. It rebuilds the source template, for example `Hello {name}`.
178
+ 4. It loads the translated template from TOML.
179
+ 5. It compiles the translated template as an f-string expression and caches it per translator instance.
180
+ 6. Later calls from the same bytecode location reuse the cached compiled expression.
181
+
182
+ ## Constraints
183
+
184
+ This is not a drop-in replacement for mature gettext tooling yet.
185
+
186
+ - The main path is designed for direct `tt(f"...")` usage after binding `tt = translator.translate`.
187
+ - Library code should keep its own translator instance and bind its own `tt` function in module scope.
188
+ - Translation lookup is currently a flat TOML key-value map.
189
+ - Static cue collection writes analysis data into a sibling hidden directory such as `.locales_cue`.
190
+ - If translated placeholder expressions are invalid or fail at evaluation time, the library falls back to the original rendered text.
191
+ - The library currently relies on runtime frame inspection and AST recovery, so unusual execution environments may behave differently.
192
+
193
+ ## Development
194
+
195
+ Run tests:
196
+
197
+ ```bash
198
+ uv run pytest -q
199
+ ```
200
+
201
+ ## Roadmap
202
+
203
+ - AI-generated translations from source templates and static cues
204
+ - Locale file generation and diffing
205
+ - Better developer tooling around extraction, validation, and review
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["uv_build >= 0.10.7, <0.11.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "autolang"
7
+ version = "0.1.0"
8
+ description = "Transparent i18n for Python f-strings."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "babel>=2.18.0",
13
+ "executing>=2.2.1",
14
+ ]
15
+ license = "MIT"
16
+ license-files = ["LICEN[CS]E*"]
17
+
18
+ [project.scripts]
19
+ tt = "autolang.cli:main"
20
+ autolang = "autolang.cli:main"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+
25
+ [tool.pyright]
26
+ typeCheckingMode = "standard"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "basedpyright>=1.38.2",
31
+ "ipykernel>=7.2.0",
32
+ "pytest>=9.0.2",
33
+ "pytest-benchmark>=5.2.3",
34
+ "ruff>=0.15.6",
35
+ ]
@@ -0,0 +1,6 @@
1
+ from .translator import TransparentTranslator, install
2
+
3
+ __all__ = [
4
+ "TransparentTranslator",
5
+ "install",
6
+ ]
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from . import init as _init
6
+ from . import sync as _sync
7
+ from . import translate as _translate
8
+
9
+ BatchTranslationItem = _translate.BatchTranslationItem
10
+ BatchTranslationOutcome = _translate.BatchTranslationOutcome
11
+ BatchTranslationRequest = _translate.BatchTranslationRequest
12
+ OpenAICompatibleClient = _translate.OpenAICompatibleClient
13
+ PlaceholderSpec = _translate.PlaceholderSpec
14
+ TranslationResult = _translate.TranslationResult
15
+ TranslationTask = _translate.TranslationTask
16
+ validate_translated_text = _translate.validate_translated_text
17
+
18
+
19
+ def handle_translate_command(args: argparse.Namespace) -> int:
20
+ _translate.OpenAICompatibleClient = OpenAICompatibleClient
21
+ return _translate.handle_translate_command(args)
22
+
23
+
24
+ def handle_sync_command(args: argparse.Namespace) -> int:
25
+ return _sync.handle_sync_command(args)
26
+
27
+
28
+ def handle_init_command(args: argparse.Namespace) -> int:
29
+ return _init.handle_init_command(args)
30
+
31
+
32
+ def build_parser() -> argparse.ArgumentParser:
33
+ parser = argparse.ArgumentParser(prog="tt", description="Autolang developer tools.")
34
+ subparsers = parser.add_subparsers(dest="command", required=True)
35
+
36
+ translate_parser = subparsers.add_parser(
37
+ "translate",
38
+ help="Translate locale TOML files through an OpenAI-compatible API.",
39
+ )
40
+ translate_parser.add_argument("--locale-dir", default="locales")
41
+ translate_parser.add_argument("--model", default=None)
42
+ translate_parser.add_argument("--base-url", default=None)
43
+ translate_parser.add_argument("--api-key", default=None)
44
+ translate_parser.add_argument("--timeout", type=float, default=60.0)
45
+ translate_parser.add_argument("--workers", type=int, default=4)
46
+ translate_parser.add_argument("--batch-size", type=int, default=20)
47
+ translate_parser.add_argument("--overwrite", action="store_true")
48
+ translate_parser.add_argument("--dry-run", action="store_true")
49
+ translate_parser.set_defaults(handler=handle_translate_command)
50
+
51
+ sync_parser = subparsers.add_parser(
52
+ "sync",
53
+ help="Sync tt()-wrapped source templates across all locale TOML files.",
54
+ )
55
+ sync_parser.add_argument("--source", default=".")
56
+ sync_parser.add_argument("--locale-dir", default="locales")
57
+ sync_parser.add_argument("--dry-run", action="store_true")
58
+ sync_parser.set_defaults(handler=handle_sync_command)
59
+
60
+ init_parser = subparsers.add_parser(
61
+ "init",
62
+ help="Initialize locale TOML files from collected tt()-wrapped source templates.",
63
+ )
64
+ init_parser.add_argument("--source", default=".")
65
+ init_parser.add_argument("--locale-dir", default="locales")
66
+ init_parser.add_argument("--locales", nargs="+", required=True)
67
+ init_parser.add_argument("--force", action="store_true")
68
+ init_parser.add_argument("--dry-run", action="store_true")
69
+ init_parser.set_defaults(handler=handle_init_command)
70
+
71
+ return parser
72
+
73
+
74
+ def main(argv: list[str] | None = None) -> int:
75
+ parser = build_parser()
76
+ args = parser.parse_args(argv)
77
+ return args.handler(args)
78
+
79
+
80
+ __all__ = [
81
+ "BatchTranslationItem",
82
+ "BatchTranslationOutcome",
83
+ "BatchTranslationRequest",
84
+ "OpenAICompatibleClient",
85
+ "PlaceholderSpec",
86
+ "TranslationResult",
87
+ "TranslationTask",
88
+ "build_parser",
89
+ "handle_init_command",
90
+ "handle_sync_command",
91
+ "handle_translate_command",
92
+ "main",
93
+ "validate_translated_text",
94
+ ]
@@ -0,0 +1,5 @@
1
+ from . import main
2
+
3
+
4
+ if __name__ == "__main__": # pragma: no cover
5
+ raise SystemExit(main())
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from babel import Locale
6
+
7
+ from ..toml_io import load_string_table
8
+
9
+ SKIPPED_SOURCE_DIR_NAMES = {
10
+ "__pycache__",
11
+ "build",
12
+ "dist",
13
+ "node_modules",
14
+ }
15
+ NO_TRANSLATION = "NO_TRANSLATION"
16
+
17
+
18
+ def load_shared_cues(locale_dir: Path) -> dict[str, str]:
19
+ cue_dir = locale_dir.parent / f".{locale_dir.name}_cue"
20
+ merged_cues: dict[str, str] = {}
21
+ for cue_path in sorted(cue_dir.glob("*.toml")):
22
+ if not cue_path.is_file():
23
+ continue
24
+ for key, value in load_string_table(str(cue_path)).items():
25
+ merged_cues.setdefault(key, value)
26
+ return merged_cues
27
+
28
+
29
+ def build_source_cue_path(locale_dir: Path, locale_name: str) -> Path:
30
+ cue_dir = locale_dir.parent / f".{locale_dir.name}_cue"
31
+ return cue_dir / f"{locale_name}.toml"
32
+
33
+
34
+ def list_locale_files(locale_dir: Path) -> list[Path]:
35
+ return sorted(path for path in locale_dir.glob("*.toml") if path.is_file())
36
+
37
+
38
+ def should_recurse_into_directory(path: str) -> bool:
39
+ name = Path(path).name
40
+ return not name.startswith(".") and name not in SKIPPED_SOURCE_DIR_NAMES
41
+
42
+
43
+ def normalize_language(locale_name: str) -> str:
44
+ return Locale.parse(locale_name).language
45
+
46
+
47
+ def locale_display_name(locale_name: str) -> str:
48
+ locale = Locale.parse(locale_name)
49
+ return locale.get_display_name("en").title()