Skip to content

Commit 38d3442

Browse files
committed
feat: Add support for multiple Arabic romanization systems (ALA-LC, DIN 31635, Buckwalter).
1 parent 3c7d96a commit 38d3442

4 files changed

Lines changed: 276 additions & 106 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Japanese Kanji support added using [`kuromoji`](https://pub.dev/packages/kuromoji).
44
- Hebrew romanization support added based on ISO 259 / DIN 31636.
5+
- Added support for multiple Arabic romanization systems (ALA-LC, DIN 31635, Buckwalter).
56

67
## 0.0.1
78

lib/src/romanizers/arabic.dart

Lines changed: 180 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,175 @@
11
import 'package:romanize/romanize.dart';
22

3+
/// Specifies the romanization system to use for Arabic.
4+
enum ArabicSystem {
5+
/// The ALA-LC (American Library Association – Library of Congress) romanization.
6+
///
7+
/// This is a common academic standard that uses digraphs (e.g., 'sh', 'th', 'kh')
8+
/// and is generally easier for English speakers to read.
9+
///
10+
/// Example: 'شمس' -> 'shams'
11+
alaLc,
12+
13+
/// The DIN 31635 standard (Deutsches Institut für Normung).
14+
///
15+
/// This system provides a strict 1-to-1 mapping using diacritics (e.g., 'š', 'ṯ', 'ḫ').
16+
/// It is widely used in Arabic studies and Germany.
17+
///
18+
/// Example: 'شمس' -> 'šams'
19+
din31635,
20+
21+
/// The Buckwalter Transliteration.
22+
///
23+
/// A strict 1-to-1 mapping using only ASCII characters. It is heavily used
24+
/// in Natural Language Processing (NLP) and data storage.
25+
///
26+
/// Example: 'شمس' -> '$ms'
27+
buckwalter,
28+
}
29+
330
/// A romanizer for Arabic script.
431
///
532
/// Supports transliteration of Arabic characters into Latin script using
6-
/// a standard transliteration table based on ISO 233 and DIN 31635.
33+
/// various standards (ALA-LC, DIN 31635, Buckwalter).
734
class ArabicRomanizer extends Romanizer {
8-
const ArabicRomanizer() : super(language: 'arabic');
9-
10-
/// Transliteration map for Arabic to Latin characters.
35+
/// Creates an Arabic romanizer with the specified [system].
1136
///
12-
/// Based on ISO 233 and DIN 31635 standards, covering most common Arabic
13-
/// characters and their romanized equivalents.
14-
static const Map<String, String> _transliterationMap = {
15-
// Basic Arabic alphabet
16-
'ا': 'a', // Alef
17-
'أ': 'a', // Alef with Hamza above
18-
'إ': 'i', // Alef with Hamza below
19-
'آ': 'ā', // Alef Madda
20-
'ٱ': 'a', // Alef Wasla
37+
/// Defaults to [ArabicSystem.alaLc].
38+
const ArabicRomanizer({this.system = ArabicSystem.alaLc})
39+
: super(language: 'arabic');
40+
41+
final ArabicSystem system;
42+
43+
// --- Mappings ---
44+
45+
// ALA-LC (Base/Current)
46+
static const Map<String, String> _alaLcMap = {
47+
// Note: 'ا' (Alef) is handled dynamically in the romanize method
48+
// to distinguish between initial 'a' and medial/final 'ā'.
49+
// We map it to 'ā' here as the default for lookup if logic falls through.
50+
'ا': 'ā',
51+
'أ': 'a', 'إ': 'i', 'آ': 'ā', 'ٱ': 'a',
52+
'ب': 'b', 'ت': 't', 'ث': 'th', 'ج': 'j', 'ح': 'ḥ', 'خ': 'kh',
53+
'د': 'd', 'ذ': 'dh', 'ر': 'r', 'ز': 'z', 'س': 's', 'ش': 'sh',
54+
'ص': 'ṣ', 'ض': 'ḍ', 'ط': 'ṭ', 'ظ': 'ẓ', 'ع': 'ʿ', 'غ': 'gh',
55+
'ف': 'f', 'ق': 'q', 'ك': 'k', 'ل': 'l', 'م': 'm', 'ن': 'n',
56+
'ه': 'h', 'ة': 'h', 'و': 'w', 'ؤ': 'u', 'ي': 'y', 'ى': 'ā',
57+
'ئ': 'i', 'ء': 'ʾ',
58+
'َ': 'a', 'ُ': 'u', 'ِ': 'i',
59+
'ً': 'an', 'ٌ': 'un', 'ٍ': 'in',
60+
'ّ': '', 'ْ': '', 'ٰ': 'ā',
61+
'٠': '0', '١': '1', '٢': '2', '٣': '3', '٤': '4',
62+
'٥': '5', '٦': '6', '٧': '7', '٨': '8', '٩': '9',
63+
};
64+
65+
// DIN 31635 (Uses diacritics for digraphs)
66+
static const Map<String, String> _din31635Map = {
67+
'ا': 'ā',
68+
'أ': 'ʾ',
69+
'إ': 'ʾ',
70+
'آ': 'ʾā',
71+
'ٱ': 'hw',
2172
'ب': 'b',
2273
'ت': 't',
23-
'ث': 'th',
24-
'ج': 'j',
74+
'ث': '',
75+
'ج': 'ǧ',
2576
'ح': 'ḥ',
26-
'خ': 'kh',
77+
'خ': '',
2778
'د': 'd',
28-
'ذ': 'dh',
79+
'ذ': '',
2980
'ر': 'r',
3081
'ز': 'z',
3182
'س': 's',
32-
'ش': 'sh',
83+
'ش': 'š',
3384
'ص': 'ṣ',
3485
'ض': 'ḍ',
3586
'ط': 'ṭ',
3687
'ظ': 'ẓ',
3788
'ع': 'ʿ',
38-
'غ': 'gh',
89+
'غ': 'ġ',
3990
'ف': 'f',
4091
'ق': 'q',
4192
'ك': 'k',
4293
'ل': 'l',
4394
'م': 'm',
4495
'ن': 'n',
4596
'ه': 'h',
46-
'ة': 'h', // Teh Marbuta
97+
'ة': 'h',
4798
'و': 'w',
48-
'ؤ': 'u', // Waw with Hamza
99+
'ؤ': 'u',
49100
'ي': 'y',
50-
'ى': 'ā', // Alef Maksura
51-
'ئ': 'i', // Yeh with Hamza
52-
'ء': 'ʾ', // Hamza
53-
// Arabic diacritics (usually omitted in romanization but included for completeness)
54-
'َ': 'a', // Fatha
55-
'ُ': 'u', // Damma
56-
'ِ': 'i', // Kasra
57-
'ً': 'an', // Tanwin Fath
58-
'ٌ': 'un', // Tanwin Damm
59-
'ٍ': 'in', // Tanwin Kasr
60-
'ّ': '', // Shadda (gemination, usually handled contextually)
61-
'ْ': '', // Sukun (no vowel, usually omitted)
62-
'ٰ': 'ā', // Superscript Alef
63-
// Arabic numbers
101+
'ى': 'ā',
102+
'ئ': 'ʾ',
103+
'ء': 'ʾ',
104+
'َ': 'a',
105+
'ُ': 'u',
106+
'ِ': 'i',
107+
'ً': 'an',
108+
'ٌ': 'un',
109+
'ٍ': 'in',
110+
'ّ': '',
111+
'ْ': '',
112+
'ٰ': 'ā',
113+
'٠': '0',
114+
'١': '1',
115+
'٢': '2',
116+
'٣': '3',
117+
'٤': '4',
118+
'٥': '5',
119+
'٦': '6',
120+
'٧': '7',
121+
'٨': '8',
122+
'٩': '9',
123+
};
124+
125+
// Buckwalter (ASCII only)
126+
static const Map<String, String> _buckwalterMap = {
127+
'ا': 'A',
128+
'أ': '>',
129+
'إ': '<',
130+
'آ': '|',
131+
'ٱ': '{',
132+
'ب': 'b',
133+
'ت': 't',
134+
'ث': 'v',
135+
'ج': 'j',
136+
'ح': 'H',
137+
'خ': 'x',
138+
'د': 'd',
139+
'ذ': '*',
140+
'ر': 'r',
141+
'ز': 'z',
142+
'س': 's',
143+
'ش': '\$',
144+
'ص': 'S',
145+
'ض': 'D',
146+
'ط': 'T',
147+
'ظ': 'Z',
148+
'ع': 'E',
149+
'غ': 'g',
150+
'ف': 'f',
151+
'ق': 'q',
152+
'ك': 'k',
153+
'ل': 'l',
154+
'م': 'm',
155+
'ن': 'n',
156+
'ه': 'h',
157+
'ة': 'p',
158+
'و': 'w',
159+
'ؤ': '&',
160+
'ي': 'y',
161+
'ى': 'Y',
162+
'ئ': '}',
163+
'ء': '\'',
164+
'َ': 'a',
165+
'ُ': 'u',
166+
'ِ': 'i',
167+
'ً': 'F',
168+
'ٌ': 'N',
169+
'ٍ': 'K',
170+
'ّ': '~',
171+
'ْ': 'o',
172+
'ٰ': '`',
64173
'٠': '0',
65174
'١': '1',
66175
'٢': '2',
@@ -77,50 +186,52 @@ class ArabicRomanizer extends Romanizer {
77186
r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0870-\u089F\uFB50-\uFDFF\uFE70-\uFEFF]',
78187
);
79188

80-
/// Converts a given Arabic string to its Romanized form.
81-
///
82-
/// This method transliterates Arabic characters into their Latin
83-
/// equivalents using a standard transliteration table. Non-Arabic
84-
/// characters (such as Latin letters, numbers, and punctuation) are
85-
/// preserved as-is.
86-
///
87-
/// Example:
88-
/// ```dart
89-
/// final romanizer = ArabicRomanizer();
90-
/// final result = romanizer.romanize('أنا العربي');
91-
/// print(result); // ana al'arabi
92-
/// ```
93189
@override
94190
String romanize(String input) {
95191
final buffer = StringBuffer();
96-
for (final char in input.runes) {
192+
final map = switch (system) {
193+
ArabicSystem.alaLc => _alaLcMap,
194+
ArabicSystem.din31635 => _din31635Map,
195+
ArabicSystem.buckwalter => _buckwalterMap,
196+
};
197+
198+
final runes = input.runes.toList();
199+
200+
for (int i = 0; i < runes.length; i++) {
201+
final char = runes[i];
97202
final charString = String.fromCharCode(char);
98-
buffer.write(_transliterationMap[charString] ?? charString);
203+
204+
// Special handling for ALA-LC Alef 'ا'
205+
if (system == ArabicSystem.alaLc && charString == 'ا') {
206+
final isStartOfWord = i == 0 || _isSeparator(runes[i - 1]);
207+
if (isStartOfWord) {
208+
buffer.write('a');
209+
} else {
210+
buffer.write('ā');
211+
}
212+
continue;
213+
}
214+
215+
buffer.write(map[charString] ?? charString);
99216
}
100217
return buffer.toString();
101218
}
102219

103-
/// Validates if the input string contains Arabic characters.
104-
///
105-
/// Returns `true` if the input contains any Arabic characters from
106-
/// the Unicode Arabic blocks.
107-
///
108-
/// Example:
109-
/// ```dart
110-
/// final romanizer = ArabicRomanizer();
111-
/// print(romanizer.isValid('أنا')); // true
112-
/// print(romanizer.isValid('Hello')); // false
113-
/// print(romanizer.isValid('أنا Hello')); // true (mixed content)
114-
/// ```
220+
/// Check for whitespace or common punctuation
221+
bool _isSeparator(int charCode) {
222+
return charCode == 32 || // Space
223+
charCode == 10 || // Newline
224+
charCode == 9 || // Tab
225+
charCode == 13 || // CR
226+
charCode == 46 || // .
227+
charCode == 44 || // ,
228+
charCode == 1548 || // ، (Arabic comma)
229+
charCode == 63 || // ?
230+
charCode == 1567; // ؟ (Arabic Question mark)
231+
}
232+
115233
@override
116234
bool isValid(String input) {
117-
// Check for Arabic characters in Unicode ranges:
118-
// - Arabic: U+0600–U+06FF
119-
// - Arabic Supplement: U+0750–U+077F
120-
// - Arabic Extended-A: U+08A0–U+08FF
121-
// - Arabic Extended-B: U+0870–U+089F
122-
// - Arabic Presentation Forms-A: U+FB50–U+FDFF
123-
// - Arabic Presentation Forms-B: U+FE70–U+FEFF
124235
return _arabicPattern.hasMatch(input);
125236
}
126237
}

0 commit comments

Comments
 (0)