• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 20737823009

06 Jan 2026 04:16AM UTC coverage: 71.324% (-2.6%) from 73.964%
20737823009

push

github

tyevco
feat(localization): Add RTL layout support and advanced localization features

- Add TextDirection enum for distinguishing LTR/RTL text flow
- Add IsRightToLeft property and RTL locale constants to Locale struct
- Add MirrorForRtl property to UIRect for automatic layout mirroring
- Update UILayoutSystem to handle RTL-aware anchor and flexbox layouts
- Add text shaping infrastructure with ITextShaper interface
- Implement ArabicTextShaper for contextual letter forms
- Add BidirectionalTextShaper for mixed LTR/RTL text handling
- Add ComplexScriptInfo for Thai, Hindi, Bengali, Tamil shaping info
- Add CsvStringSource for CSV import/export translator workflows
- Add comprehensive unit tests for all new functionality

Closes #635

5865 of 7851 branches covered (74.7%)

Branch coverage included in aggregate %.

26 of 764 new or added lines in 9 files covered. (3.4%)

452 existing lines in 6 files now uncovered.

37040 of 52304 relevant lines covered (70.82%)

1.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/KeenEyes.Localization/TextShaping/ArabicTextShaper.cs
1
using System.Text;
2

3
namespace KeenEyes.Localization.TextShaping;
4

5
/// <summary>
6
/// Text shaper for Arabic script that handles contextual letter forms.
7
/// </summary>
8
/// <remarks>
9
/// <para>
10
/// Arabic letters have different forms depending on their position in a word:
11
/// </para>
12
/// <list type="bullet">
13
///   <item><description><b>Isolated</b> - letter appears alone</description></item>
14
///   <item><description><b>Initial</b> - letter at the start of a word</description></item>
15
///   <item><description><b>Medial</b> - letter in the middle of a word</description></item>
16
///   <item><description><b>Final</b> - letter at the end of a word</description></item>
17
/// </list>
18
/// <para>
19
/// Some letters (like ا alif, د dal, ذ dhal, ر ra, ز zay, و waw) do not connect
20
/// to the following letter, affecting the form of subsequent letters.
21
/// </para>
22
/// </remarks>
23
/// <example>
24
/// <code>
25
/// var shaper = new ArabicTextShaper();
26
/// string shaped = shaper.Shape("مرحبا", Locale.Arabic);
27
/// // Returns Arabic text with proper contextual letter forms
28
/// </code>
29
/// </example>
30
public sealed class ArabicTextShaper : ITextShaper
31
{
32
    // Arabic letter form mappings: [isolated, initial, medial, final]
33
    // Only includes letters that have contextual forms
NEW
34
    private static readonly Dictionary<char, char[]> arabicForms = new()
×
NEW
35
    {
×
NEW
36
        // Alif - does not connect to following letter
×
NEW
37
        ['ا'] = ['ا', 'ا', 'ﺎ', 'ﺎ'],
×
NEW
38
        // Ba
×
NEW
39
        ['ب'] = ['ب', 'ﺑ', 'ﺒ', 'ﺐ'],
×
NEW
40
        // Ta
×
NEW
41
        ['ت'] = ['ت', 'ﺗ', 'ﺘ', 'ﺖ'],
×
NEW
42
        // Tha
×
NEW
43
        ['ث'] = ['ث', 'ﺛ', 'ﺜ', 'ﺚ'],
×
NEW
44
        // Jim
×
NEW
45
        ['ج'] = ['ج', 'ﺟ', 'ﺠ', 'ﺞ'],
×
NEW
46
        // Ha
×
NEW
47
        ['ح'] = ['ح', 'ﺣ', 'ﺤ', 'ﺢ'],
×
NEW
48
        // Kha
×
NEW
49
        ['خ'] = ['خ', 'ﺧ', 'ﺨ', 'ﺦ'],
×
NEW
50
        // Dal - does not connect to following letter
×
NEW
51
        ['د'] = ['د', 'د', 'ﺪ', 'ﺪ'],
×
NEW
52
        // Dhal - does not connect to following letter
×
NEW
53
        ['ذ'] = ['ذ', 'ذ', 'ﺬ', 'ﺬ'],
×
NEW
54
        // Ra - does not connect to following letter
×
NEW
55
        ['ر'] = ['ر', 'ر', 'ﺮ', 'ﺮ'],
×
NEW
56
        // Zay - does not connect to following letter
×
NEW
57
        ['ز'] = ['ز', 'ز', 'ﺰ', 'ﺰ'],
×
NEW
58
        // Sin
×
NEW
59
        ['س'] = ['س', 'ﺳ', 'ﺴ', 'ﺲ'],
×
NEW
60
        // Shin
×
NEW
61
        ['ش'] = ['ش', 'ﺷ', 'ﺸ', 'ﺶ'],
×
NEW
62
        // Sad
×
NEW
63
        ['ص'] = ['ص', 'ﺻ', 'ﺼ', 'ﺺ'],
×
NEW
64
        // Dad
×
NEW
65
        ['ض'] = ['ض', 'ﺿ', 'ﻀ', 'ﺾ'],
×
NEW
66
        // Tah
×
NEW
67
        ['ط'] = ['ط', 'ﻃ', 'ﻄ', 'ﻂ'],
×
NEW
68
        // Dhah
×
NEW
69
        ['ظ'] = ['ظ', 'ﻇ', 'ﻈ', 'ﻆ'],
×
NEW
70
        // Ain
×
NEW
71
        ['ع'] = ['ع', 'ﻋ', 'ﻌ', 'ﻊ'],
×
NEW
72
        // Ghain
×
NEW
73
        ['غ'] = ['غ', 'ﻏ', 'ﻐ', 'ﻎ'],
×
NEW
74
        // Fa
×
NEW
75
        ['ف'] = ['ف', 'ﻓ', 'ﻔ', 'ﻒ'],
×
NEW
76
        // Qaf
×
NEW
77
        ['ق'] = ['ق', 'ﻗ', 'ﻘ', 'ﻖ'],
×
NEW
78
        // Kaf
×
NEW
79
        ['ك'] = ['ك', 'ﻛ', 'ﻜ', 'ﻚ'],
×
NEW
80
        // Lam
×
NEW
81
        ['ل'] = ['ل', 'ﻟ', 'ﻠ', 'ﻞ'],
×
NEW
82
        // Mim
×
NEW
83
        ['م'] = ['م', 'ﻣ', 'ﻤ', 'ﻢ'],
×
NEW
84
        // Nun
×
NEW
85
        ['ن'] = ['ن', 'ﻧ', 'ﻨ', 'ﻦ'],
×
NEW
86
        // Ha
×
NEW
87
        ['ه'] = ['ه', 'ﻫ', 'ﻬ', 'ﻪ'],
×
NEW
88
        // Waw - does not connect to following letter
×
NEW
89
        ['و'] = ['و', 'و', 'ﻮ', 'ﻮ'],
×
NEW
90
        // Ya
×
NEW
91
        ['ي'] = ['ي', 'ﻳ', 'ﻴ', 'ﻲ'],
×
NEW
92
        // Alif with hamza above
×
NEW
93
        ['أ'] = ['أ', 'أ', 'ﺄ', 'ﺄ'],
×
NEW
94
        // Alif with hamza below
×
NEW
95
        ['إ'] = ['إ', 'إ', 'ﺈ', 'ﺈ'],
×
NEW
96
        // Alif madda
×
NEW
97
        ['آ'] = ['آ', 'آ', 'ﺂ', 'ﺂ'],
×
NEW
98
        // Hamza on waw
×
NEW
99
        ['ؤ'] = ['ؤ', 'ؤ', 'ﺆ', 'ﺆ'],
×
NEW
100
        // Hamza on ya
×
NEW
101
        ['ئ'] = ['ئ', 'ﺋ', 'ﺌ', 'ﺊ'],
×
NEW
102
        // Ta marbuta - does not connect to following letter
×
NEW
103
        ['ة'] = ['ة', 'ة', 'ﺔ', 'ﺔ'],
×
NEW
104
        // Alif maksura - does not connect to following letter
×
NEW
105
        ['ى'] = ['ى', 'ى', 'ﻰ', 'ﻰ'],
×
NEW
106
    };
×
107

108
    // Letters that do not connect to the following letter (right-side non-joiners)
NEW
109
    private static readonly HashSet<char> nonJoiningRight =
×
NEW
110
    [
×
NEW
111
        'ا', 'أ', 'إ', 'آ', 'د', 'ذ', 'ر', 'ز', 'و', 'ؤ', 'ة', 'ى'
×
NEW
112
    ];
×
113

114
    /// <inheritdoc />
NEW
115
    public IEnumerable<ScriptType> SupportedScripts => [ScriptType.Arabic];
×
116

117
    /// <inheritdoc />
NEW
118
    public bool SupportsScript(ScriptType script) => script == ScriptType.Arabic;
×
119

120
    /// <inheritdoc />
121
    public string Shape(string text, Locale locale)
NEW
122
    {
×
NEW
123
        if (string.IsNullOrEmpty(text))
×
NEW
124
        {
×
NEW
125
            return text;
×
126
        }
127

NEW
128
        var result = new StringBuilder(text.Length);
×
NEW
129
        int i = 0;
×
130

NEW
131
        while (i < text.Length)
×
NEW
132
        {
×
NEW
133
            char current = text[i];
×
134

135
            // Check if this is an Arabic letter
NEW
136
            if (!IsArabicLetter(current))
×
NEW
137
            {
×
NEW
138
                result.Append(current);
×
NEW
139
                i++;
×
NEW
140
                continue;
×
141
            }
142

143
            // Determine the form based on context
NEW
144
            bool prevConnects = i > 0 && IsArabicLetter(text[i - 1]) && !nonJoiningRight.Contains(text[i - 1]);
×
NEW
145
            bool nextConnects = i < text.Length - 1 && IsArabicLetter(text[i + 1]);
×
146

NEW
147
            char shapedChar = GetContextualForm(current, prevConnects, nextConnects);
×
NEW
148
            result.Append(shapedChar);
×
NEW
149
            i++;
×
NEW
150
        }
×
151

NEW
152
        return result.ToString();
×
NEW
153
    }
×
154

155
    /// <inheritdoc />
156
    public ShapingResult ShapeWithInfo(string text, Locale locale)
NEW
157
    {
×
NEW
158
        string shapedText = Shape(text, locale);
×
159

NEW
160
        return new ShapingResult(
×
NEW
161
            ShapedText: shapedText,
×
NEW
162
            OriginalText: text,
×
NEW
163
            BaseDirection: TextDirection.RightToLeft,
×
NEW
164
            ContainsRtl: ContainsArabic(text),
×
NEW
165
            IsMixedDirection: ContainsMixedDirections(text),
×
NEW
166
            DetectedScripts: [ScriptType.Arabic]);
×
NEW
167
    }
×
168

169
    /// <summary>
170
    /// Determines whether a character is an Arabic letter.
171
    /// </summary>
172
    /// <param name="c">The character to check.</param>
173
    /// <returns><c>true</c> if the character is Arabic; otherwise, <c>false</c>.</returns>
174
    public static bool IsArabicLetter(char c)
NEW
175
    {
×
176
        // Arabic Unicode range: U+0600-U+06FF (Arabic)
177
        // Also check U+FB50-U+FDFF (Arabic Presentation Forms-A)
178
        // And U+FE70-U+FEFF (Arabic Presentation Forms-B)
NEW
179
        return (c >= '\u0600' && c <= '\u06FF') ||
×
NEW
180
               (c >= '\uFB50' && c <= '\uFDFF') ||
×
NEW
181
               (c >= '\uFE70' && c <= '\uFEFF');
×
NEW
182
    }
×
183

184
    /// <summary>
185
    /// Gets the contextual form of an Arabic letter.
186
    /// </summary>
187
    /// <param name="c">The base letter.</param>
188
    /// <param name="prevConnects">Whether the previous letter connects.</param>
189
    /// <param name="nextConnects">Whether the next letter connects.</param>
190
    /// <returns>The appropriate contextual form.</returns>
191
    private static char GetContextualForm(char c, bool prevConnects, bool nextConnects)
NEW
192
    {
×
NEW
193
        if (!arabicForms.TryGetValue(c, out var forms))
×
NEW
194
        {
×
NEW
195
            return c; // No contextual forms defined
×
196
        }
197

198
        // Determine form index: 0=isolated, 1=initial, 2=medial, 3=final
199
        int formIndex;
NEW
200
        if (!prevConnects && !nextConnects)
×
NEW
201
        {
×
NEW
202
            formIndex = 0; // Isolated
×
NEW
203
        }
×
NEW
204
        else if (!prevConnects && nextConnects)
×
NEW
205
        {
×
NEW
206
            formIndex = 1; // Initial
×
NEW
207
        }
×
NEW
208
        else if (prevConnects && nextConnects)
×
NEW
209
        {
×
NEW
210
            formIndex = 2; // Medial
×
NEW
211
        }
×
212
        else // prevConnects && !nextConnects
NEW
213
        {
×
NEW
214
            formIndex = 3; // Final
×
NEW
215
        }
×
216

NEW
217
        return forms[formIndex];
×
NEW
218
    }
×
219

220
    private static bool ContainsArabic(string text)
NEW
221
    {
×
NEW
222
        foreach (char c in text)
×
NEW
223
        {
×
NEW
224
            if (IsArabicLetter(c))
×
NEW
225
            {
×
NEW
226
                return true;
×
227
            }
NEW
228
        }
×
NEW
229
        return false;
×
NEW
230
    }
×
231

232
    private static bool ContainsMixedDirections(string text)
NEW
233
    {
×
NEW
234
        bool hasLtr = false;
×
NEW
235
        bool hasRtl = false;
×
236

NEW
237
        foreach (char c in text)
×
NEW
238
        {
×
NEW
239
            if (IsArabicLetter(c) || (c >= '\u0590' && c <= '\u05FF')) // Hebrew range
×
NEW
240
            {
×
NEW
241
                hasRtl = true;
×
NEW
242
            }
×
NEW
243
            else if (char.IsLetter(c))
×
NEW
244
            {
×
NEW
245
                hasLtr = true;
×
NEW
246
            }
×
247

NEW
248
            if (hasLtr && hasRtl)
×
NEW
249
            {
×
NEW
250
                return true;
×
251
            }
NEW
252
        }
×
253

NEW
254
        return false;
×
NEW
255
    }
×
256
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc