Skip to content

Commit c8ef721

Browse files
issue #4 add support for Macedonian and Bulgarian accented vowels (#57)
1 parent cc00c69 commit c8ef721

18 files changed

Lines changed: 985 additions & 514 deletions

File tree

README.md

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,68 @@ CyrTranslit can be used both programatically and via command line interface.
160160
"Під лежачий камінь вода не тече"
161161
```
162162

163+
### Accented Characters (Macedonian & Bulgarian)
164+
165+
CyrTranslit supports Cyrillic characters with grave accents used in Macedonian and Bulgarian for homograph disambiguation and stress marking. By default, accents are stripped during transliteration for cleaner output. Use the `preserve_accents` parameter to preserve them.
166+
167+
#### Supported Accented Characters
168+
169+
**Macedonian:**
170+
- **Ѐ/ѐ** (U+0400/U+0450) - Cyrillic IE with grave
171+
- **Purpose:** Distinguishes homographs (e.g., нѐ "us" vs не "no", сѐ "everything" vs се "reflexive pronoun")
172+
- **Standard:** ISO 9:1968/1995, adopted by Macedonian Academy of Arts and Sciences (1970)
173+
174+
- **Ѝ/ѝ** (U+040D/U+045D) - Cyrillic I with grave
175+
- **Purpose:** Distinguishes homographs (e.g., ѝ "her" vs и "and")
176+
- **Standard:** ISO 9:1968/1995
177+
178+
**Bulgarian:**
179+
- **Ѝ/ѝ** (U+040D/U+045D) - Cyrillic I with grave
180+
- **Purpose:** Stress marking and homograph disambiguation (e.g., ѝ "her" vs и "and")
181+
- **Standard:** ISO 9:1995
182+
183+
**Sources:**
184+
- ISO 9:1995 - Information and documentation — Transliteration of Cyrillic characters into Latin characters
185+
- [Wikipedia: I with grave (Cyrillic)](https://en.wikipedia.org/wiki/I_with_grave_(Cyrillic))
186+
- [Wikipedia: Ye with grave](https://en.wikipedia.org/wiki/Ye_with_grave)
187+
188+
#### Usage Examples
189+
190+
**Default behavior (accents stripped):**
191+
```python
192+
>>> import cyrtranslit
193+
>>> cyrtranslit.to_latin("ѝ је", "mk")
194+
"i je"
195+
>>> cyrtranslit.to_latin("нѐ сме", "mk")
196+
"ne sme"
197+
>>> cyrtranslit.to_cyrillic("i je", "mk")
198+
"и је"
199+
```
200+
201+
**With accents preserved:**
202+
```python
203+
>>> import cyrtranslit
204+
>>> cyrtranslit.to_latin("ѝ је", "mk", preserve_accents=True)
205+
"ì je"
206+
>>> cyrtranslit.to_latin("нѐ сме", "mk", preserve_accents=True)
207+
"nè sme"
208+
>>> cyrtranslit.to_cyrillic("ì je", "mk", preserve_accents=True)
209+
"ѝ је"
210+
>>> cyrtranslit.to_cyrillic("nè sme", "mk", preserve_accents=True)
211+
"нѐ сме"
212+
```
213+
214+
**Command-line usage:**
215+
```bash
216+
# Default (accents stripped)
217+
$ echo "ѝ је" | cyrtranslit -l mk
218+
i je
219+
220+
# Preserve accents
221+
$ echo "ѝ је" | cyrtranslit -l mk --preserve-accents
222+
ì je
223+
```
224+
163225
## Command Line Interface
164226
Sample command line call to transliterate a Russian text file:
165227
```bash
@@ -202,8 +264,8 @@ Try CyrTranslit by running it directly on the Python command line interface, e.g
202264
## How can I contribute?
203265
Include support for other Cyrillic script alphabets. Follow these steps in order to do so:
204266

205-
1. Create a new transliteration dictionary in the **[mapping.py](https://github.com/opendatakosovo/cyrillic-transliteration/blob/master/cyrtranslit/mapping.py)** file and reference to it in the _**[TRANSLIT\_DICT](https://github.com/opendatakosovo/cyrillic-transliteration/blob/ab88bb466d12b9a9ad8d3eb6dc86d0bab871175d/cyrtranslit/mapping.py#L326-L360)**_ dictionary.
206-
2. Watch out for cases where two consecutive Latin alphabet letters are meant to transliterate into a single Cyrillic script letter. These cases need to be explicitly checked for [inside the **to_cyrillic()** function in **\_\_init\_\_.py**](https://github.com/opendatakosovo/cyrillic-transliteration/blob/ab88bb466d12b9a9ad8d3eb6dc86d0bab871175d/cyrtranslit/__init__.py#L62-L191).
267+
1. Create a new transliteration mapping file in the **[mapping/](https://github.com/opendatakosovo/cyrillic-transliteration/blob/master/cyrtranslit/mapping/)** directory (using the language code as the filename, e.g., `xx.py`) and reference to it in the _**[TRANSLIT\_DICT](https://github.com/opendatakosovo/cyrillic-transliteration/blob/master/cyrtranslit/mapping/__init__.py)**_ dictionary in **mapping/\_\_init\_\_.py**. If the language uses accented characters (like Macedonian and Bulgarian), create separate accented dictionaries (e.g., `XX_CYR_TO_LAT_ACCENTED_DICT`) following the pattern in **[mk.py](https://github.com/opendatakosovo/cyrillic-transliteration/blob/master/cyrtranslit/mapping/mk.py)** or **[bg.py](https://github.com/opendatakosovo/cyrillic-transliteration/blob/master/cyrtranslit/mapping/bg.py)**.
268+
2. Watch out for cases where two consecutive Latin alphabet letters are meant to transliterate into a single Cyrillic script letter. These cases need to be explicitly checked for inside the **to_cyrillic()** function in **[\_\_init\_\_.py](https://github.com/opendatakosovo/cyrillic-transliteration/blob/master/cyrtranslit/__init__.py)**.
207269
3. Add test cases inside of **[tests.py](https://github.com/opendatakosovo/cyrillic-transliteration/blob/master/tests.py)**.
208270
4. Add test CLI input files in the **[tests](https://github.com/opendatakosovo/cyrillic-transliteration/tree/master/tests)** directory.
209271
5. Update the documentation in the **[README.md](https://github.com/opendatakosovo/cyrillic-transliteration/blob/master/README.md)**.

cyrtranslit/__init__.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ def __decode_utf8(_string):
1414
else:
1515
return _string
1616

17-
def to_latin(string_to_transliterate, lang_code='sr'):
17+
def to_latin(string_to_transliterate, lang_code='sr', preserve_accents=False):
1818
''' Transliterate cyrillic string of characters to latin string of characters.
1919
:param string_to_transliterate: The cyrillic string to transliterate into latin characters.
2020
:param lang_code: Indicates the cyrillic language code we are translating from. Defaults to Serbian (sr).
21+
:param preserve_accents: If False (default), uses standard mappings (accented Cyrillic → unaccented Latin, e.g., Ѐ→E, ѝ→i).
22+
If True, merges accented mappings (accented Cyrillic → accented Latin, e.g., Ѐ→È, ѝ→ì).
2123
:return: A string of latin characters transliterated from the given cyrillic string.
2224
'''
2325

@@ -34,7 +36,11 @@ def to_latin(string_to_transliterate, lang_code='sr'):
3436
else:
3537

3638
# Get the character per character transliteration dictionary
37-
transliteration_dict = TRANSLIT_DICT[lang_code.lower()]['tolatin']
39+
transliteration_dict = TRANSLIT_DICT[lang_code.lower()]['tolatin'].copy()
40+
41+
# If preserve_accents=True and accented mappings exist, merge them (accented overrides standard)
42+
if preserve_accents and 'tolatin_accented' in TRANSLIT_DICT[lang_code.lower()]:
43+
transliteration_dict.update(TRANSLIT_DICT[lang_code.lower()]['tolatin_accented'])
3844

3945
# Initialize the output latin string variable
4046
latinized_str = ''
@@ -59,10 +65,12 @@ def to_latin(string_to_transliterate, lang_code='sr'):
5965
return __encode_utf8(latinized_str)
6066

6167

62-
def to_cyrillic(string_to_transliterate, lang_code='sr'):
68+
def to_cyrillic(string_to_transliterate, lang_code='sr', preserve_accents=False):
6369
''' Transliterate latin string of characters to cyrillic string of characters.
6470
:param string_to_transliterate: The latin string to transliterate into cyrillic characters.
6571
:param lang_code: Indicates the cyrillic language code we are translating to. Defaults to Serbian (sr).
72+
:param preserve_accents: If False (default), uses standard mappings (accented Latin → unaccented Cyrillic, e.g., È→Е, ì→и).
73+
If True, merges accented mappings (accented Latin → accented Cyrillic, e.g., È→Ѐ, ì→ѝ).
6674
:return: A string of cyrillic characters transliterated from the given latin string.
6775
'''
6876

@@ -77,7 +85,11 @@ def to_cyrillic(string_to_transliterate, lang_code='sr'):
7785

7886
else:
7987
# Get the character per character transliteration dictionary
80-
transliteration_dict = TRANSLIT_DICT[lang_code.lower()]['tocyrillic']
88+
transliteration_dict = TRANSLIT_DICT[lang_code.lower()]['tocyrillic'].copy()
89+
90+
# If preserve_accents=True and accented mappings exist, merge them (accented overrides standard)
91+
if preserve_accents and 'tocyrillic_accented' in TRANSLIT_DICT[lang_code.lower()]:
92+
transliteration_dict.update(TRANSLIT_DICT[lang_code.lower()]['tocyrillic_accented'])
8193

8294
# Initialize the output cyrillic string variable
8395
cyrillic_str = ''

cyrtranslit/cyrtranslit.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ def main():
4040
parser.add_argument("-c", dest="to_cyrillic", action='store_true',
4141
help="Parse latin characters to cyrillic (reverse of transliteration)")
4242

43+
# Flag to preserve accent marks in transliteration
44+
parser.add_argument("-p", "--preserve-accents", dest="preserve_accents", action='store_true',
45+
help="Preserve accent marks (e.g., Macedonian/Bulgarian Ѐ→È, ѝ→ì instead of Ѐ→E, ѝ→i)")
46+
4347
# Input file encoding.
4448
# Not required. Defaults to utf-8 with fallback to common Cyrillic encodings.
4549
parser.add_argument("-e", "--encoding", dest="encoding", required=False,
@@ -52,6 +56,7 @@ def main():
5256
# Fetch arguments.
5357
lang_code = args.language_code
5458
to_cyrillic = args.to_cyrillic
59+
preserve_accents = args.preserve_accents
5560
encoding = args.encoding
5661

5762
# Open input file with proper encoding handling
@@ -107,9 +112,9 @@ def try_encoding(filepath, enc):
107112
try:
108113
for line in file_input:
109114
if to_cyrillic is True:
110-
file_output.write(cyrtranslit.to_cyrillic(line, lang_code=lang_code))
115+
file_output.write(cyrtranslit.to_cyrillic(line, lang_code=lang_code, preserve_accents=preserve_accents))
111116
else:
112-
file_output.write(cyrtranslit.to_latin(line, lang_code=lang_code))
117+
file_output.write(cyrtranslit.to_latin(line, lang_code=lang_code, preserve_accents=preserve_accents))
113118
finally:
114119
# Close streams if they're not stdin/stdout
115120
if args.input_file and file_input:

0 commit comments

Comments
 (0)