akademtranslit 0.2.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.
- akademtranslit-0.2.0/CHANGELOG.md +101 -0
- akademtranslit-0.2.0/LICENSE +21 -0
- akademtranslit-0.2.0/MANIFEST.in +5 -0
- akademtranslit-0.2.0/PKG-INFO +411 -0
- akademtranslit-0.2.0/README.md +379 -0
- akademtranslit-0.2.0/golden/SHA256SUMS +3 -0
- akademtranslit-0.2.0/golden/golden-0.1.1.json +7854 -0
- akademtranslit-0.2.0/golden/golden-0.2.0.json +39559 -0
- akademtranslit-0.2.0/golden/inputs.json +8393 -0
- akademtranslit-0.2.0/pyproject.toml +63 -0
- akademtranslit-0.2.0/setup.cfg +4 -0
- akademtranslit-0.2.0/src/akademtranslit/__init__.py +195 -0
- akademtranslit-0.2.0/src/akademtranslit/apostrophes.py +83 -0
- akademtranslit-0.2.0/src/akademtranslit/collation.py +136 -0
- akademtranslit-0.2.0/src/akademtranslit/detection.py +83 -0
- akademtranslit-0.2.0/src/akademtranslit/normalize.py +149 -0
- akademtranslit-0.2.0/src/akademtranslit/py.typed +0 -0
- akademtranslit-0.2.0/src/akademtranslit/rules.py +310 -0
- akademtranslit-0.2.0/src/akademtranslit/tables.py +241 -0
- akademtranslit-0.2.0/src/akademtranslit.egg-info/PKG-INFO +411 -0
- akademtranslit-0.2.0/src/akademtranslit.egg-info/SOURCES.txt +38 -0
- akademtranslit-0.2.0/src/akademtranslit.egg-info/dependency_links.txt +1 -0
- akademtranslit-0.2.0/src/akademtranslit.egg-info/requires.txt +3 -0
- akademtranslit-0.2.0/src/akademtranslit.egg-info/top_level.txt +1 -0
- akademtranslit-0.2.0/tests/test_alphabets.py +75 -0
- akademtranslit-0.2.0/tests/test_apostrophes.py +134 -0
- akademtranslit-0.2.0/tests/test_changes_0_2_0.py +226 -0
- akademtranslit-0.2.0/tests/test_collation.py +168 -0
- akademtranslit-0.2.0/tests/test_detection.py +98 -0
- akademtranslit-0.2.0/tests/test_golden.py +45 -0
- akademtranslit-0.2.0/tests/test_normalize.py +138 -0
- akademtranslit-0.2.0/tests/test_public_api_compat.py +102 -0
- akademtranslit-0.2.0/tests/test_readme.py +14 -0
- akademtranslit-0.2.0/tests/test_regressions.py +459 -0
- akademtranslit-0.2.0/tests/test_roundtrip.py +201 -0
- akademtranslit-0.2.0/tests/test_to_cyrillic.py +146 -0
- akademtranslit-0.2.0/tests/test_to_latin.py +129 -0
- akademtranslit-0.2.0/tools/bench.py +80 -0
- akademtranslit-0.2.0/tools/check_classifiers.py +74 -0
- akademtranslit-0.2.0/tools/golden.py +213 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-11-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `sort_key(text, orthography)`: alphabetical order in `cyrillic`,
|
|
8
|
+
`latin_1995` or `latin_2026`. In `latin_2026`, `ç` comes after `ş`. In
|
|
9
|
+
`latin_1995`, `oʻ gʻ sh ch` are single letters after `z`. In Cyrillic,
|
|
10
|
+
`Ў Қ Ғ Ҳ` come after `Я`. The orthography is required; there is no
|
|
11
|
+
detection.
|
|
12
|
+
- `normalize_apostrophes(text, style="unicode")`: rewrites the marks in Latin
|
|
13
|
+
words to `ʻ`/`ʼ` (`"unicode"`) or `'` (`"ascii"`) without converting anything
|
|
14
|
+
else.
|
|
15
|
+
- `script_class(text)` (`latin`, `cyrillic` or `mixed`) and
|
|
16
|
+
`cyrillic_language(text)` (`uz`, `ru`, `mixed` or `unknown`): closed-set
|
|
17
|
+
answers for routing. `detect_script` is unchanged.
|
|
18
|
+
- `LATIN_1995_ALPHABET` (28 units: 24 letters, then `oʻ gʻ sh ch`) and
|
|
19
|
+
`CYRILLIC_ALPHABET` (35 letters).
|
|
20
|
+
- Golden vectors in `golden/`: 1121 inputs, the 0.1.1 and 0.2.0 outputs and
|
|
21
|
+
their SHA-256 sums, with `tools/golden.py` to write, check and diff them.
|
|
22
|
+
- The README examples and the docstring examples run as tests. A test pins the
|
|
23
|
+
module-level names that downstream code imports.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `LATIN_2026_ALPHABET` is in the order of Law No. O'RQ-1184: `ç` moved from
|
|
28
|
+
after `b` to after `ş`. No conversion output depends on it.
|
|
29
|
+
- `__version__` is the only place the version is written.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Cyrillic → Latin: `ъ` is dropped after a consonant before `е ё ю я`
|
|
34
|
+
(`объект` → `obyekt`, `съёмка` → `syomka`) and after `ў` (`мўъжиза` →
|
|
35
|
+
`moʻjiza`), as the 1995 spelling writes them; it was `ʼ` everywhere
|
|
36
|
+
(`obʼyekt`, `moʻʼjiza`). Elsewhere it is still `ʼ` (`маъно` → `maʼno`). The
|
|
37
|
+
reverse direction is unchanged (`obyekt` → `обект`), so this is a documented
|
|
38
|
+
lossy mapping.
|
|
39
|
+
- Latin → Cyrillic: `ts` is `т` + `с` before the particles `-gina`, `-kina`,
|
|
40
|
+
`-yu`, `-u`, `-ku`, `-yoq` and `-mikan` after a suffix: `aytsangizgina` →
|
|
41
|
+
`айтсангизгина`, `ketsayu` → `кетсаю`, `tutsinmikan` → `тутсинмикан`; they
|
|
42
|
+
came out with `ц`.
|
|
43
|
+
- Latin → Cyrillic: `oksitotsin`, `rifampitsin` and the `-rubitsin` drugs keep
|
|
44
|
+
`ц` again (`окситоцин`, `рифампицин`, `доксорубицин`), as in 0.1.0; 0.1.1
|
|
45
|
+
gave `окситотсин`. Verbs with the same ending (`bitsin`, `ketsin`) keep
|
|
46
|
+
`т` + `с`.
|
|
47
|
+
- `normalize`: `´` (U+00B4) reads as an apostrophe, like the other seven
|
|
48
|
+
marks. Unicode normalization (NFKC) used to turn it into a space and a
|
|
49
|
+
combining accent, so `normalize("o´zbek")` was `"o zbek"`; it is now
|
|
50
|
+
`"ozbek"`, the same key as `oʻzbek` and `o'zbek`. No other input's key
|
|
51
|
+
changes.
|
|
52
|
+
|
|
53
|
+
### Output changes
|
|
54
|
+
|
|
55
|
+
Golden diff, 0.1.1 → 0.2.0, over 1121 inputs
|
|
56
|
+
(`python tools/golden.py --diff golden/golden-0.1.1.json golden/golden-0.2.0.json`):
|
|
57
|
+
|
|
58
|
+
| Field | Changed |
|
|
59
|
+
| --- | ---: |
|
|
60
|
+
| `normalize` | 5 |
|
|
61
|
+
| `detect_script` | 0 |
|
|
62
|
+
| `to_cyrillic` | 13 |
|
|
63
|
+
| `to_latin_1995` | 9 |
|
|
64
|
+
| `to_latin_2026` | 9 |
|
|
65
|
+
|
|
66
|
+
- `to_latin_1995` and `to_latin_2026`: the nine inputs with a dropped `ъ`, e.g.
|
|
67
|
+
`объект` `obʼyekt` → `obyekt`, `мўъжиза` `moʻʼjiza` → `moʻjiza` (2026:
|
|
68
|
+
`möʼjiza` → `möjiza`).
|
|
69
|
+
- `to_cyrillic`: seven particle forms (`aytsangizgina` `айцангизгина` →
|
|
70
|
+
`айтсангизгина`) and six drug names (`oksitotsin` `окситотсин` →
|
|
71
|
+
`окситоцин`).
|
|
72
|
+
- `normalize`: the five inputs with `´` inside a word: `o´zbek` `o zbek` →
|
|
73
|
+
`ozbek`, `g´alaba`, `ma´no`, `jur´at` and `O´ZBEKISTON` likewise. A search
|
|
74
|
+
index built with 0.1.x needs these keys rebuilt.
|
|
75
|
+
|
|
76
|
+
### Packaging
|
|
77
|
+
|
|
78
|
+
- License metadata in the PEP 639 form (`license = "MIT"`,
|
|
79
|
+
`license-files`); the license classifier is gone, as PEP 639 asks.
|
|
80
|
+
- `py.typed` is shipped. The sdist includes `CHANGELOG.md`, `tests/`, `golden/`
|
|
81
|
+
and `tools/`, so the test suite can run from it.
|
|
82
|
+
- The version is read from `akademtranslit.__version__`; the build needs
|
|
83
|
+
`setuptools>=77`.
|
|
84
|
+
- Version classifiers name only the Python versions the test suite has run on.
|
|
85
|
+
- Dropped the `Natural Language :: Uzbek` classifier: it is not in the PyPI
|
|
86
|
+
classifier list, and PyPI rejects uploads that carry it.
|
|
87
|
+
`tools/check_classifiers.py` now checks the list in CI and before every
|
|
88
|
+
release.
|
|
89
|
+
- Still Python 3.9 or newer, and still no dependencies.
|
|
90
|
+
|
|
91
|
+
## 0.1.1 — 2026-10-16
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- Latin → Cyrillic: `y` before `oʻ` is `й` + `ў` (`yoʻq` → `йўқ`), not `ё` and a stray mark (`ёʻқ`).
|
|
96
|
+
- Latin → Cyrillic: `ts` is `т` + `с` before an Uzbek suffix (`ketsa` → `кетса`, `hayotsiz` → `ҳаётсиз`) and `т` + `ш` before `h` (`adabiyotshunos` → `адабиётшунос`); loanwords keep `ц`, with the misses listed in the README. A short list of loanwords whose tail reads like a suffix keeps `ц` as well (`aktsiz`, `tablitsa`, `levofloksatsin`, `leytsin`).
|
|
97
|
+
- Words in all caps that contain `ʻ` or `ʼ` stay in all caps (`OʻZBEKISTON` → `ЎЗБЕКИСТОН`, `ÖZBEKISTON`); they came out as `Ўзбекистон`.
|
|
98
|
+
|
|
99
|
+
## 0.1.0 — 2026-08-15
|
|
100
|
+
|
|
101
|
+
First release.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AkademTech
|
|
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,411 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: akademtranslit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Uzbek transliteration across three orthographies: Cyrillic, the 1995 Latin alphabet, and the 2026 reform.
|
|
5
|
+
Author-email: AkademTech <contact@akademtech.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://akademtech.org
|
|
8
|
+
Project-URL: Source, https://github.com/AkademTech/akademtranslit
|
|
9
|
+
Project-URL: Changelog, https://github.com/AkademTech/akademtranslit/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/AkademTech/akademtranslit/issues
|
|
11
|
+
Keywords: uzbek,transliteration,cyrillic,latin,romanization,normalization,alphabet-reform
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# AkademTranslit
|
|
34
|
+
|
|
35
|
+
Uzbek transliteration across three orthographies — Cyrillic, the 1995 Latin
|
|
36
|
+
alphabet, and the 2026 Latin alphabet — plus a normalization key that makes one
|
|
37
|
+
word searchable across all three, and alphabetical sorting in each.
|
|
38
|
+
|
|
39
|
+
Pure Python, standard library only, no dependencies.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
>>> from akademtranslit import to_latin, to_cyrillic, convert, normalize
|
|
43
|
+
>>> to_latin("Тошкент давлат университети")
|
|
44
|
+
'Toşkent davlat universiteti'
|
|
45
|
+
>>> to_latin("Тошкент давлат университети", variant="latin_1995")
|
|
46
|
+
'Toshkent davlat universiteti'
|
|
47
|
+
>>> to_cyrillic("Oʻzbekiston Respublikasi")
|
|
48
|
+
'Ўзбекистон Республикаси'
|
|
49
|
+
>>> normalize("Şoşimova") == normalize("Shoshimova") == normalize("Шошимова")
|
|
50
|
+
True
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
Uzbek is written three ways at once, and will be for years.
|
|
57
|
+
|
|
58
|
+
Cyrillic carries the archives, the older scholarly record and a large share of
|
|
59
|
+
the press. The Latin alphabet adopted in 1993 and revised in 1995 carries
|
|
60
|
+
official documents and most of the web; it writes four sounds with digraphs and
|
|
61
|
+
modifier marks — `sh`, `ch`, `gʻ`, `oʻ`. The 2026 alphabet, set by Law No.
|
|
62
|
+
O'RQ-1184 (signed 21 October 2026, in force from 22 April 2027), replaces those
|
|
63
|
+
four with single letters — `ş`, `ç`, `ğ`, `ö` — giving 28 letters plus the
|
|
64
|
+
tutuq belgisi.
|
|
65
|
+
|
|
66
|
+
For a reader this is an inconvenience. For a search index it is a correctness
|
|
67
|
+
problem: the same author, journal or institution arrives spelled three
|
|
68
|
+
different ways in three different records, and string equality finds none of
|
|
69
|
+
the matches. `Шошимова`, `Shoshimova` and `Şoşimova` are one person.
|
|
70
|
+
|
|
71
|
+
This library does separate jobs, and keeping them separate is the point:
|
|
72
|
+
|
|
73
|
+
- **Transliteration** (`to_latin`, `to_cyrillic`, `convert`) — display-grade,
|
|
74
|
+
preserves case and punctuation. Use it to show text to people and to migrate
|
|
75
|
+
a corpus from one alphabet to another.
|
|
76
|
+
- **Normalization** (`normalize`) — deliberately lossy, produces a match key
|
|
77
|
+
nobody should ever display. Use it for search, deduplication and equality.
|
|
78
|
+
- **Sorting** (`sort_key`) — the alphabetical order of each orthography, which
|
|
79
|
+
code-point order gets wrong in all three.
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install akademtranslit
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Requires Python 3.9 or newer. No dependencies.
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### Convert between the three orthographies
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
>>> from akademtranslit import convert
|
|
95
|
+
>>> convert("Тошкент", source="cyrillic", target="latin_2026")
|
|
96
|
+
'Toşkent'
|
|
97
|
+
>>> convert("Toshkent", source="latin_1995", target="latin_2026")
|
|
98
|
+
'Toşkent'
|
|
99
|
+
>>> convert("Toşkent", source="latin_2026", target="cyrillic")
|
|
100
|
+
'Тошкент'
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`source` defaults to `"auto"`, which runs script detection first, and `target`
|
|
105
|
+
defaults to `"latin_2026"`.
|
|
106
|
+
|
|
107
|
+
### Migrate an existing corpus to the 2026 alphabet
|
|
108
|
+
|
|
109
|
+
The 1995 ↔ 2026 conversion is lossless in both directions, so a migration can
|
|
110
|
+
be verified and, if needed, reversed.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
>>> old = "Mirzo Ulugʻbek nomidagi Oʻzbekiston Milliy universiteti"
|
|
114
|
+
>>> new = convert(old, source="latin_1995", target="latin_2026")
|
|
115
|
+
>>> new
|
|
116
|
+
'Mirzo Uluğbek nomidagi Özbekiston Milliy universiteti'
|
|
117
|
+
>>> convert(new, source="latin_2026", target="latin_1995") == old
|
|
118
|
+
True
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Build a cross-script search key
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
>>> from akademtranslit import normalize
|
|
126
|
+
>>> index = {}
|
|
127
|
+
>>> for record in ["Şoşimova N.", "Shoshimova N.", "Шошимова Н."]:
|
|
128
|
+
... index.setdefault(normalize(record), []).append(record)
|
|
129
|
+
>>> index
|
|
130
|
+
{'shoshimova n': ['Şoşimova N.', 'Shoshimova N.', 'Шошимова Н.']}
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`normalize` is idempotent and folds orthography, case, punctuation and every
|
|
135
|
+
apostrophe style in use: all eight accepted marks (see below) give the same
|
|
136
|
+
key, so `o'zbek`, `o’zbek`, `o´zbek` and `oʻzbek` all match.
|
|
137
|
+
|
|
138
|
+
### Sort in alphabetical order
|
|
139
|
+
|
|
140
|
+
`sort_key(text, orthography)` sorts by the alphabet of the orthography you
|
|
141
|
+
name. It has to be named — there is no detection — because the same string
|
|
142
|
+
sorts differently in different orthographies.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
>>> from akademtranslit import sort_key
|
|
146
|
+
>>> sorted(["tabiat", "çaqmoq", "sabr", "şahar"], key=lambda w: sort_key(w, "latin_2026"))
|
|
147
|
+
['sabr', 'şahar', 'çaqmoq', 'tabiat']
|
|
148
|
+
>>> sorted(["choy", "shamol", "zarar", "oʻrik", "gʻoz"], key=lambda w: sort_key(w, "latin_1995"))
|
|
149
|
+
['zarar', 'oʻrik', 'gʻoz', 'shamol', 'choy']
|
|
150
|
+
>>> sorted(["ҳаво", "ғоз", "ялпиз", "қовун", "ўрик"], key=lambda w: sort_key(w, "cyrillic"))
|
|
151
|
+
['ялпиз', 'ўрик', 'қовун', 'ғоз', 'ҳаво']
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
In `latin_1995`, `oʻ`, `gʻ`, `sh` and `ch` are single letters after `z`, while
|
|
156
|
+
a separated `sʼh` is two: `Isʼhoq` sorts before `Ishonch`. Whitespace sorts
|
|
157
|
+
before digits, digits before letters, and letters outside the alphabet after
|
|
158
|
+
it; punctuation and the tutuq belgisi are not counted. When two keys tie on
|
|
159
|
+
letters, the text itself decides, so the order is total.
|
|
160
|
+
|
|
161
|
+
### Put the apostrophes in one form
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
>>> from akademtranslit import normalize_apostrophes
|
|
165
|
+
>>> normalize_apostrophes("O'zbekiston, ma'no, bog'")
|
|
166
|
+
'Oʻzbekiston, maʼno, bogʻ'
|
|
167
|
+
>>> normalize_apostrophes("Oʻzbekiston, maʼno", style="ascii")
|
|
168
|
+
"O'zbekiston, ma'no"
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Only marks inside Latin words change; quotes around a word, digits,
|
|
173
|
+
punctuation and words with a Cyrillic letter are left as they are. It does not
|
|
174
|
+
convert anything, so it is safe on text whose orthography is unknown.
|
|
175
|
+
|
|
176
|
+
### Detect what you were handed
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
>>> from akademtranslit import detect_script, script_class, cyrillic_language
|
|
180
|
+
>>> detect_script("Самарқанд"), detect_script("Oʻzbekiston"), detect_script("Özbekiston")
|
|
181
|
+
('cyrillic', 'latin_1995', 'latin_2026')
|
|
182
|
+
>>> script_class("Тошкент"), script_class("Toshkent"), script_class("Tоshkent")
|
|
183
|
+
('cyrillic', 'latin', 'mixed')
|
|
184
|
+
>>> cyrillic_language("Ўзбекистон"), cyrillic_language("Мы здесь"), cyrillic_language("бир")
|
|
185
|
+
('uz', 'ru', 'unknown')
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`detect_script` names an orthography. `script_class` answers a narrower
|
|
190
|
+
question with a closed set — `latin`, `cyrillic` or `mixed` — and is the one to
|
|
191
|
+
use for catching homoglyphs: the `о` in the third example is Cyrillic.
|
|
192
|
+
`cyrillic_language` tells Uzbek Cyrillic from Russian by the letters only one
|
|
193
|
+
of them has (`ў қ ғ ҳ` against `щ ы`); it is a routing heuristic, not a
|
|
194
|
+
language identifier, and short text is often `unknown`.
|
|
195
|
+
|
|
196
|
+
## Supported orthographies
|
|
197
|
+
|
|
198
|
+
The three alphabets, in their alphabetical order, are exposed as
|
|
199
|
+
`LATIN_2026_ALPHABET`, `LATIN_1995_ALPHABET` and `CYRILLIC_ALPHABET`:
|
|
200
|
+
|
|
201
|
+
| Orthography | Letters, in order |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `latin_2026` (28) | A B D E F G Ğ H I J K L M N O Ö P Q R S Ş Ç T U V X Y Z |
|
|
204
|
+
| `latin_1995` (28) | A B D E F G H I J K L M N O P Q R S T U V X Y Z Oʻ Gʻ Sh Ch |
|
|
205
|
+
| `cyrillic` (35) | А Б В Г Д Е Ё Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Ъ Ь Э Ю Я Ў Қ Ғ Ҳ |
|
|
206
|
+
|
|
207
|
+
The tutuq belgisi `ʼ` belongs to both Latin alphabets but is not a letter: it
|
|
208
|
+
is a separate sign, `TUTUQ`, and is in neither list.
|
|
209
|
+
|
|
210
|
+
| Sound | `cyrillic` | `latin_1995` | `latin_2026` |
|
|
211
|
+
| --- | --- | --- | --- |
|
|
212
|
+
| /ʃ/ | ш | sh | **ş** |
|
|
213
|
+
| /tʃ/ | ч | ch | **ç** |
|
|
214
|
+
| /ʁ/ | ғ | gʻ | **ğ** |
|
|
215
|
+
| /oʻ/ | ў | oʻ | **ö** |
|
|
216
|
+
| /q/ | қ | q | q |
|
|
217
|
+
| /h/ | ҳ | h | h |
|
|
218
|
+
| /x/ | х | x | x |
|
|
219
|
+
| /dʒ/ | ж | j | j |
|
|
220
|
+
| glottal stop | ъ | ʼ | ʼ |
|
|
221
|
+
|
|
222
|
+
Only the four bolded rows differ between the two Latin alphabets. Everything
|
|
223
|
+
else is shared, which is why `latin_1995` serves as the pivot for every
|
|
224
|
+
conversion.
|
|
225
|
+
|
|
226
|
+
### Context-dependent rules
|
|
227
|
+
|
|
228
|
+
Five readings depend on position rather than on the letter alone:
|
|
229
|
+
|
|
230
|
+
| Rule | Example |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| `е` is `ye` word-initially and after a vowel, `e` otherwise | Ер → Yer, поезд → poyezd, тезда → tezda |
|
|
233
|
+
| `ц` is `ts` after a vowel, `s` otherwise | милиция → militsiya, функция → funksiya, цирк → sirk |
|
|
234
|
+
| `ts` is `т` + `с` before an Uzbek suffix, `т` + `ш` before `h`, `ц` otherwise — a heuristic, see below | ketsa → кетса, hayotsiz → ҳаётсиз, adabiyotshunos → адабиётшунос, operatsiya → операция |
|
|
235
|
+
| `с` before `ҳ` takes a separator in `latin_1995` | Исҳоқ → Isʼhoq (not `Ishoq`) |
|
|
236
|
+
| `ъ` is dropped after a consonant before `е ё ю я` and after `ў`, `ʼ` otherwise | объект → obyekt, мўъжиза → moʻjiza, маъно → maʼno |
|
|
237
|
+
|
|
238
|
+
The separator needs a note. In `latin_1995`, `sh` is a digraph, so a genuine
|
|
239
|
+
s+h sequence has to be separated with the tutuq belgisi or it reads as `ш`. The
|
|
240
|
+
2026 alphabet does not need the separator, because `ş` carries that sound —
|
|
241
|
+
`Isʼhoq` becomes simply `Ishoq`, while `ish` (work) becomes `iş`. The pair
|
|
242
|
+
survives the round trip in both directions.
|
|
243
|
+
|
|
244
|
+
The `ts` rule needs a note of its own. In loanwords `ts` is the letter `ц`;
|
|
245
|
+
where a stem ending in `t` meets a suffix beginning with `s` it is two letters:
|
|
246
|
+
`ketsa` (if he goes) is кетса, not кеца. There is no dictionary, only a short
|
|
247
|
+
list of loanwords, so the reading comes from the shape of the word. The checks
|
|
248
|
+
run in order and the first that applies wins:
|
|
249
|
+
|
|
250
|
+
- the `s` starts `sh` → `т` + `ш`: adabiyotshunos → адабиётшунос;
|
|
251
|
+
- the word starts with `ts` → `ц`: tsement → цемент;
|
|
252
|
+
- `i` and then a vowel or `y` follow → `ц`: operatsiya → операция, ratsion →
|
|
253
|
+
рацион;
|
|
254
|
+
- a loanword whose tail would pass for suffixes → `ц`. The list: names in
|
|
255
|
+
`-mitsin`, `-rubitsin`, `-floksatsin` and `-leytsin`, plus `glitsin`,
|
|
256
|
+
`oksitotsin`, `rifampitsin`, `amikatsin` and `aktsiz`; and words beginning
|
|
257
|
+
with one of the `-ица` stems `tablits`, `teplits`, `stolits`, `ulits`,
|
|
258
|
+
`bolnits`, `granits`, `gostinits` (`TS_LOANWORD_STEMS` in `tables.py`).
|
|
259
|
+
Inflected forms count too: levofloksatsinni → левофлоксацинни, tablitsada →
|
|
260
|
+
таблицада. The list exists to split `ts`; it is not a dictionary and is not
|
|
261
|
+
meant to grow. Uzbek words with the same endings keep `т` + `с` — bitsa,
|
|
262
|
+
bitsin, oʻqitsa, koʻrsatsin, and nouns in `-kt` with `-siz` such as
|
|
263
|
+
kontaktsiz;
|
|
264
|
+
- the rest of the word is Uzbek suffixes → `т` + `с`. The `s` must open one of
|
|
265
|
+
`-sa`, `-sak`, `-sam`, `-sang`, `-sangiz`, `-salar`, `-sin`, `-sinlar`,
|
|
266
|
+
`-siz`, `-sizlik` or `-sevar`, and anything after it must be further suffixes
|
|
267
|
+
or particles (`-lik`, `-lar`, `-ga`, `-da`, `-ni`, `-man`, `-mi`, `-gina`,
|
|
268
|
+
`-yu`, `-ku`, `-yoq`, `-mikan`, …): ketsinmi → кетсинми, hayotsizlik →
|
|
269
|
+
ҳаётсизлик, aytsangizgina → айтсангизгина. Starting with the right letters is
|
|
270
|
+
not enough, so meditsina (`sin` + `a`) and natsizm (`siz` + `m`) keep `ц`;
|
|
271
|
+
- otherwise `ц`: leykotsit → лейкоцит, protsent → процент.
|
|
272
|
+
|
|
273
|
+
The known misses:
|
|
274
|
+
|
|
275
|
+
- A loanword off that list whose tail looks like a suffix gets `т` + `с`:
|
|
276
|
+
karbetotsin → карбетотсин (карбетоцин). oksitotsin, rifampitsin and the
|
|
277
|
+
`-rubitsin` names had this problem in 0.1.1 and are on the list since 0.2.0.
|
|
278
|
+
- A stem on the list also takes a native ending: granitsiz (granite + `-siz`)
|
|
279
|
+
→ границиз, not гранитсиз.
|
|
280
|
+
- An Uzbek word whose `t` + `s` boundary is not in the list keeps `ц`:
|
|
281
|
+
compounds such as oʻtsoʻndirgich → ўцўндиргич (ўтсўндиргич) and
|
|
282
|
+
soatsoz → соацоз (соатсоз), and suffixes such as `-simon` (otsimon → оцимон).
|
|
283
|
+
- pitsa and nitsa are wrong either way: питса and нитса now, пица and ница
|
|
284
|
+
before 0.1.1, пицца and Ницца in fact.
|
|
285
|
+
|
|
286
|
+
### The two modifier marks
|
|
287
|
+
|
|
288
|
+
`ʻ` (U+02BB, turned comma) is part of the letters `oʻ` and `gʻ`. `ʼ` (U+02BC,
|
|
289
|
+
modifier apostrophe) is the tutuq belgisi. Most people type both as a plain
|
|
290
|
+
`'`, so the library recovers the distinction from context: after `o` or `g` the
|
|
291
|
+
mark is a turned comma, elsewhere it is the tutuq belgisi.
|
|
292
|
+
|
|
293
|
+
Input accepts eight characters for either mark: `ʻ` U+02BB, `ʼ` U+02BC, `’`
|
|
294
|
+
U+2019, `‘` U+2018, `´` U+00B4, `ʹ` U+02B9, `'` U+0027 and `` ` `` U+0060.
|
|
295
|
+
Conversion always emits the canonical two, which makes it idempotent and its
|
|
296
|
+
output safe to store; `normalize_apostrophes` does the same without converting,
|
|
297
|
+
or writes `'` for both with `style="ascii"`. `normalize` drops all eight, so the
|
|
298
|
+
mark a word was typed with never changes its key (since 0.2.0 this includes
|
|
299
|
+
`´`, which Unicode normalization used to turn into a space).
|
|
300
|
+
|
|
301
|
+
## Guarantees, and their limits
|
|
302
|
+
|
|
303
|
+
**Lossless.** `latin_1995` ↔ `latin_2026` round-trips exactly for text in
|
|
304
|
+
canonical form: marks written as `ʻ` and `ʼ`, and each word in lower case,
|
|
305
|
+
with an initial capital, or in all caps (`OʻZBEKISTON` included). Other input
|
|
306
|
+
comes back canonicalised — `o'zbek` as `oʻzbek`, `McDonald` as `Mcdonald`.
|
|
307
|
+
Cyrillic → Latin → Cyrillic round-trips exactly over the native Uzbek letter
|
|
308
|
+
inventory, with two gaps: `т` followed by `с` comes back as `ц` unless the `s`
|
|
309
|
+
starts a suffix the `ts` rule knows (`ўтсўндиргич`, `соатсоз`; see
|
|
310
|
+
Context-dependent rules), and `ъ` after `ў` does not come back (`мўъжиза` →
|
|
311
|
+
`moʻjiza` → `мўжиза`). Both are enforced by the test suite.
|
|
312
|
+
|
|
313
|
+
**Known lossy mappings.** Four Cyrillic letters occur only in Russian
|
|
314
|
+
loanwords, and folding them is deliberate; `ъ` is dropped where the 1995
|
|
315
|
+
spelling drops it:
|
|
316
|
+
|
|
317
|
+
| Letter | Behaviour | Reason |
|
|
318
|
+
| --- | --- | --- |
|
|
319
|
+
| щ | merges with ш → `sh` | no distinct sound in Uzbek |
|
|
320
|
+
| ы | merges with и → `i` | no distinct sound in Uzbek |
|
|
321
|
+
| ь | dropped | not represented in either Latin alphabet |
|
|
322
|
+
| ц | positional `ts`/`s`, not reversible | a heuristic; loanword spelling is genuinely inconsistent |
|
|
323
|
+
| ъ | dropped after a consonant before `е ё ю я` and after `ў` (`объект` → `obyekt`, `мўъжиза` → `moʻjiza`), `ʼ` elsewhere; not reversible | the 1995 spelling writes no mark there, so the Latin side has nothing to restore it from |
|
|
324
|
+
|
|
325
|
+
**Script detection is a heuristic.** A `latin_2026` string containing none of
|
|
326
|
+
the four reformed letters is indistinguishable from a `latin_1995` one. That is
|
|
327
|
+
harmless for nearly every word — the two orthographies write them identically —
|
|
328
|
+
but not for a separated s+h: `Ishoq` is Исҳоқ in `latin_2026` and Ишоқ in
|
|
329
|
+
`latin_1995`. Pass `source` explicitly when the orthography is known from
|
|
330
|
+
context.
|
|
331
|
+
|
|
332
|
+
**Out of scope.** `normalize` folds orthography, not romanization choice. It
|
|
333
|
+
does not treat `Xomidov` and `Khomidov`, or `Jasur` and `Zhasur`, as the same
|
|
334
|
+
string — those are different transcription conventions, and collapsing them
|
|
335
|
+
would merge names belonging to different people. Handle them upstream as scored
|
|
336
|
+
candidates. Translation matching and fuzzy or edit-distance matching are
|
|
337
|
+
likewise out of scope.
|
|
338
|
+
|
|
339
|
+
**A note on `ö`.** Law No. O'RQ-1184 writes the letter as `ö`, and that is
|
|
340
|
+
what this library emits. Earlier drafts of the reform used `ō` (o with macron)
|
|
341
|
+
or `ǒ`; both are accepted on input as aliases of `ö`, so text written under a
|
|
342
|
+
draft reads correctly, but they are never output.
|
|
343
|
+
|
|
344
|
+
## Golden vectors
|
|
345
|
+
|
|
346
|
+
`golden/` holds the library's output, sealed:
|
|
347
|
+
|
|
348
|
+
- `inputs.json` — the inputs: every letter of the three alphabets, the context
|
|
349
|
+
rules, every apostrophe form, mixed content, ecosystem traps. Append-only: an
|
|
350
|
+
`id` never changes and is never reused.
|
|
351
|
+
- `golden-<version>.json` — the output of every public function on every input,
|
|
352
|
+
for that release.
|
|
353
|
+
- `SHA256SUMS` — the hashes of those files.
|
|
354
|
+
|
|
355
|
+
Check them with:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
cd golden
|
|
359
|
+
shasum -a 256 -c SHA256SUMS # or: sha256sum -c SHA256SUMS
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The files are the shared reference for code that switches to this library and
|
|
363
|
+
for the ports to other languages (JavaScript, PHP): a port is correct when it
|
|
364
|
+
reproduces `golden-<version>.json` byte for byte. `python tools/golden.py
|
|
365
|
+
--check` regenerates the file from the source and compares;
|
|
366
|
+
`python tools/golden.py --diff OLD NEW` lists every output that changed between
|
|
367
|
+
two releases.
|
|
368
|
+
|
|
369
|
+
## Versioning
|
|
370
|
+
|
|
371
|
+
[Semantic versioning](https://semver.org). The output of `normalize()` never
|
|
372
|
+
changes in a patch release (0.y.Z): search indexes are built on it. Every
|
|
373
|
+
change to any output is listed in [CHANGELOG.md](CHANGELOG.md) with its golden
|
|
374
|
+
diff.
|
|
375
|
+
|
|
376
|
+
## Development
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
python3 -m venv .venv
|
|
380
|
+
.venv/bin/pip install -e ".[test]"
|
|
381
|
+
.venv/bin/python -m pytest
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Or with [uv](https://docs.astral.sh/uv/): `uv run --with pytest -- python -m
|
|
385
|
+
pytest`.
|
|
386
|
+
|
|
387
|
+
The suite covers the alphabet letter by letter in both directions, the context
|
|
388
|
+
rules, the round-trip tiers above, casing (including ALL-CAPS, where `ШОШИМОВА`
|
|
389
|
+
must give `ŞOŞIMOVA` and not `Şoşimova`, and `OʻZBEKISTON` must give
|
|
390
|
+
`ЎЗБЕКИСТОН`), the cross-script normalization contract, sorting, and the
|
|
391
|
+
golden vectors. The examples in this README and in the docstrings run as tests.
|
|
392
|
+
`tests/test_regressions.py` and `tests/test_changes_0_2_0.py` pin every
|
|
393
|
+
behaviour change since 0.1.0; see [CHANGELOG.md](CHANGELOG.md).
|
|
394
|
+
|
|
395
|
+
## Security
|
|
396
|
+
|
|
397
|
+
See [SECURITY.md](SECURITY.md). The library does not use the network and
|
|
398
|
+
stores nothing.
|
|
399
|
+
|
|
400
|
+
## Contributing
|
|
401
|
+
|
|
402
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). This project follows a
|
|
403
|
+
[code of conduct](CODE_OF_CONDUCT.md).
|
|
404
|
+
|
|
405
|
+
## License
|
|
406
|
+
|
|
407
|
+
MIT — see [LICENSE](LICENSE).
|
|
408
|
+
|
|
409
|
+
## Who
|
|
410
|
+
|
|
411
|
+
Built by [AkademTech](https://akademtech.org).
|