• 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/BidirectionalTextShaper.cs
1
using System.Text;
2

3
namespace KeenEyes.Localization.TextShaping;
4

5
/// <summary>
6
/// Text shaper that handles bidirectional text (mixed LTR and RTL content).
7
/// </summary>
8
/// <remarks>
9
/// <para>
10
/// This shaper implements a simplified version of the Unicode Bidirectional Algorithm (UBA)
11
/// to properly order text that contains both left-to-right and right-to-left characters.
12
/// </para>
13
/// <para>
14
/// Common use cases:
15
/// <list type="bullet">
16
///   <item><description>English text with Arabic words</description></item>
17
///   <item><description>Hebrew text with English technical terms</description></item>
18
///   <item><description>Numbers in RTL text</description></item>
19
/// </list>
20
/// </para>
21
/// </remarks>
22
/// <example>
23
/// <code>
24
/// var shaper = new BidirectionalTextShaper();
25
/// string result = shaper.Shape("Hello مرحبا World", Locale.EnglishUS);
26
/// // Returns: "Hello ابحرم World" (Arabic reversed for LTR base)
27
/// </code>
28
/// </example>
29
public sealed class BidirectionalTextShaper : ITextShaper
30
{
NEW
31
    private readonly ArabicTextShaper arabicShaper = new();
×
32

33
    /// <inheritdoc />
34
    public IEnumerable<ScriptType> SupportedScripts =>
NEW
35
    [
×
NEW
36
        ScriptType.Latin,
×
NEW
37
        ScriptType.Arabic,
×
NEW
38
        ScriptType.Hebrew,
×
NEW
39
        ScriptType.Cyrillic,
×
NEW
40
        ScriptType.Greek
×
NEW
41
    ];
×
42

43
    /// <inheritdoc />
NEW
44
    public bool SupportsScript(ScriptType script) => script switch
×
NEW
45
    {
×
NEW
46
        ScriptType.Latin => true,
×
NEW
47
        ScriptType.Arabic => true,
×
NEW
48
        ScriptType.Hebrew => true,
×
NEW
49
        ScriptType.Cyrillic => true,
×
NEW
50
        ScriptType.Greek => true,
×
NEW
51
        _ => false
×
NEW
52
    };
×
53

54
    /// <inheritdoc />
55
    public string Shape(string text, Locale locale)
NEW
56
    {
×
NEW
57
        if (string.IsNullOrEmpty(text))
×
NEW
58
        {
×
NEW
59
            return text;
×
60
        }
61

62
        // Determine base direction from locale
NEW
63
        var baseDirection = locale.TextDirection;
×
64

65
        // Split into runs of same-direction text
NEW
66
        var runs = IdentifyDirectionalRuns(text);
×
67

68
        // If no RTL content found, return as-is (with Arabic shaping if needed)
NEW
69
        if (!runs.Any(r => r.IsRtl))
×
NEW
70
        {
×
NEW
71
            return text;
×
72
        }
73

74
        // Process and reorder runs
NEW
75
        var result = new StringBuilder(text.Length);
×
76

NEW
77
        if (baseDirection == TextDirection.LeftToRight)
×
NEW
78
        {
×
NEW
79
            ProcessLtrBase(runs, result);
×
NEW
80
        }
×
81
        else
NEW
82
        {
×
NEW
83
            ProcessRtlBase(runs, result);
×
NEW
84
        }
×
85

NEW
86
        return result.ToString();
×
NEW
87
    }
×
88

89
    /// <inheritdoc />
90
    public ShapingResult ShapeWithInfo(string text, Locale locale)
NEW
91
    {
×
NEW
92
        var runs = IdentifyDirectionalRuns(text);
×
NEW
93
        bool hasRtl = runs.Any(r => r.IsRtl);
×
NEW
94
        bool hasLtr = runs.Any(r => !r.IsRtl);
×
95

NEW
96
        string shapedText = Shape(text, locale);
×
97

NEW
98
        var scripts = new List<ScriptType>();
×
NEW
99
        foreach (var run in runs)
×
NEW
100
        {
×
NEW
101
            if (run.IsRtl)
×
NEW
102
            {
×
NEW
103
                scripts.Add(ScriptType.Arabic); // Simplified - could detect Hebrew vs Arabic
×
NEW
104
            }
×
NEW
105
            else if (run.Text.Any(char.IsLetter))
×
NEW
106
            {
×
NEW
107
                scripts.Add(ScriptType.Latin);
×
NEW
108
            }
×
NEW
109
        }
×
110

NEW
111
        return new ShapingResult(
×
NEW
112
            ShapedText: shapedText,
×
NEW
113
            OriginalText: text,
×
NEW
114
            BaseDirection: locale.TextDirection,
×
NEW
115
            ContainsRtl: hasRtl,
×
NEW
116
            IsMixedDirection: hasRtl && hasLtr,
×
NEW
117
            DetectedScripts: scripts.Distinct().ToList());
×
NEW
118
    }
×
119

120
    private void ProcessLtrBase(List<DirectionalRun> runs, StringBuilder result)
NEW
121
    {
×
NEW
122
        foreach (var run in runs)
×
NEW
123
        {
×
NEW
124
            if (run.IsRtl)
×
NEW
125
            {
×
126
                // Shape Arabic text and reverse for display in LTR context
NEW
127
                string shaped = arabicShaper.Shape(run.Text, Locale.Arabic);
×
NEW
128
                result.Append(ReverseString(shaped));
×
NEW
129
            }
×
130
            else
NEW
131
            {
×
NEW
132
                result.Append(run.Text);
×
NEW
133
            }
×
NEW
134
        }
×
NEW
135
    }
×
136

137
    private void ProcessRtlBase(List<DirectionalRun> runs, StringBuilder result)
NEW
138
    {
×
139
        // In RTL base, process runs in reverse order
NEW
140
        for (int i = runs.Count - 1; i >= 0; i--)
×
NEW
141
        {
×
NEW
142
            var run = runs[i];
×
NEW
143
            if (run.IsRtl)
×
NEW
144
            {
×
145
                // Shape Arabic text (already in correct order for RTL display)
NEW
146
                string shaped = arabicShaper.Shape(run.Text, Locale.Arabic);
×
NEW
147
                result.Append(shaped);
×
NEW
148
            }
×
149
            else
NEW
150
            {
×
151
                // LTR text needs to be kept as-is within the RTL flow
NEW
152
                result.Append(run.Text);
×
NEW
153
            }
×
NEW
154
        }
×
NEW
155
    }
×
156

157
    private static List<DirectionalRun> IdentifyDirectionalRuns(string text)
NEW
158
    {
×
NEW
159
        var runs = new List<DirectionalRun>();
×
NEW
160
        if (string.IsNullOrEmpty(text))
×
NEW
161
        {
×
NEW
162
            return runs;
×
163
        }
164

NEW
165
        var currentRun = new StringBuilder();
×
NEW
166
        bool? currentIsRtl = null;
×
167

NEW
168
        foreach (char c in text)
×
NEW
169
        {
×
NEW
170
            bool isRtl = IsRtlCharacter(c);
×
NEW
171
            bool isNeutral = IsNeutralCharacter(c);
×
172

NEW
173
            if (isNeutral)
×
NEW
174
            {
×
175
                // Neutral characters (spaces, punctuation) join the current run
NEW
176
                currentRun.Append(c);
×
NEW
177
            }
×
NEW
178
            else if (currentIsRtl == null || currentIsRtl == isRtl)
×
NEW
179
            {
×
NEW
180
                currentIsRtl = isRtl;
×
NEW
181
                currentRun.Append(c);
×
NEW
182
            }
×
183
            else
NEW
184
            {
×
185
                // Direction change - save current run
NEW
186
                if (currentRun.Length > 0)
×
NEW
187
                {
×
NEW
188
                    runs.Add(new DirectionalRun(currentRun.ToString(), currentIsRtl.Value));
×
NEW
189
                    currentRun.Clear();
×
NEW
190
                }
×
NEW
191
                currentIsRtl = isRtl;
×
NEW
192
                currentRun.Append(c);
×
NEW
193
            }
×
NEW
194
        }
×
195

196
        // Add final run
NEW
197
        if (currentRun.Length > 0)
×
NEW
198
        {
×
NEW
199
            runs.Add(new DirectionalRun(currentRun.ToString(), currentIsRtl ?? false));
×
NEW
200
        }
×
201

NEW
202
        return runs;
×
NEW
203
    }
×
204

205
    private static bool IsRtlCharacter(char c)
NEW
206
    {
×
207
        // Arabic: U+0600-U+06FF, U+FB50-U+FDFF, U+FE70-U+FEFF
208
        // Hebrew: U+0590-U+05FF
NEW
209
        return (c >= '\u0600' && c <= '\u06FF') ||
×
NEW
210
               (c >= '\uFB50' && c <= '\uFDFF') ||
×
NEW
211
               (c >= '\uFE70' && c <= '\uFEFF') ||
×
NEW
212
               (c >= '\u0590' && c <= '\u05FF');
×
NEW
213
    }
×
214

215
    private static bool IsNeutralCharacter(char c)
NEW
216
    {
×
217
        // Spaces, digits, and common punctuation are neutral
NEW
218
        return char.IsWhiteSpace(c) ||
×
NEW
219
               char.IsDigit(c) ||
×
NEW
220
               char.IsPunctuation(c) ||
×
NEW
221
               char.IsSymbol(c);
×
NEW
222
    }
×
223

224
    private static string ReverseString(string s)
NEW
225
    {
×
NEW
226
        char[] arr = s.ToCharArray();
×
NEW
227
        Array.Reverse(arr);
×
NEW
228
        return new string(arr);
×
NEW
229
    }
×
230

NEW
231
    private readonly record struct DirectionalRun(string Text, bool IsRtl);
×
232
}
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