Skip to content

Commit 77c843f

Browse files
committed
feat: fix Vietnamese Telex input transformation
- Change iteration from backwards to forward (longest-match-first) - Add case-insensitive matching for Telex rules - Add case preservation (Aw→Ă, aw→ă, Ow→Ơ, ow→ơ) - Add Vietnamese chars (ă, ư, ơ) to keyboard popups - Fix test helpers to use case-insensitive matching - Add missing 'ww→ư' rule to test suite - Add 4 new tests for case preservation verification
1 parent 86e80ba commit 77c843f

3 files changed

Lines changed: 75 additions & 16 deletions

File tree

app/src/main/kotlin/org/fossify/keyboard/services/SimpleKeyboardIME.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -383,14 +383,19 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
383383
} else 0
384384
if (lastIndexEmpty >= 0) {
385385
val word = fullText.subSequence(lastIndexEmpty, fullText.length).trim().toString()
386-
val wordChars = word.toCharArray()
387-
val predictWord = StringBuilder()
388-
for (char in wordChars.size - 1 downTo 0) {
389-
predictWord.append(wordChars[char])
390-
val shouldChangeText = predictWord.reverse().toString()
391-
if (cachedVNTelexData.containsKey(shouldChangeText)) {
392-
inputConnection.setComposingRegion(fullText.length - shouldChangeText.length, fullText.length)
393-
inputConnection.setComposingText(cachedVNTelexData[shouldChangeText], fullText.length)
386+
for (i in word.indices) {
387+
val partialWord = word.substring(i, word.length)
388+
val partialWordLower = partialWord.lowercase()
389+
if (cachedVNTelexData.containsKey(partialWordLower)) {
390+
val replacement = cachedVNTelexData[partialWordLower]!!
391+
// Preserve case: if first char is uppercase, capitalize replacement
392+
val finalReplacement = if (partialWord.firstOrNull()?.isUpperCase() == true && replacement.isNotEmpty()) {
393+
replacement.replaceFirstChar { it.uppercase() }
394+
} else {
395+
replacement
396+
}
397+
inputConnection.setComposingRegion(fullText.length - partialWordLower.length, fullText.length)
398+
inputConnection.setComposingText(finalReplacement, fullText.length)
394399
inputConnection.setComposingRegion(fullText.length, fullText.length)
395400
return
396401
}

app/src/main/res/xml/keys_letters_english_qwerty.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
app:topSmallNumber="1" />
4444
<Key
4545
app:keyLabel="w"
46-
app:popupCharacters="2"
46+
app:popupCharacters=""
4747
app:popupKeyboard="@xml/keyboard_popup_template"
4848
app:topSmallNumber="2" />
4949
<Key
@@ -78,7 +78,7 @@
7878
app:topSmallNumber="8" />
7979
<Key
8080
app:keyLabel="o"
81-
app:popupCharacters="őöøóôòõ9ō"
81+
app:popupCharacters="őöøóôòõ9ōơ"
8282
app:popupKeyboard="@xml/keyboard_popup_template"
8383
app:topSmallNumber="9" />
8484
<Key
@@ -93,7 +93,7 @@
9393
app:horizontalGap="5%"
9494
app:keyEdgeFlags="left"
9595
app:keyLabel="a"
96-
app:popupCharacters="áàâãäåāæą"
96+
app:popupCharacters="áàâãäåāæąă"
9797
app:popupKeyboard="@xml/keyboard_popup_template" />
9898
<Key
9999
app:keyLabel="s"

app/src/test/kotlin/org/fossify/keyboard/services/VietnameseTelexTest.kt

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class VietnameseTelexTest {
1212
fun setup() {
1313
telexRules = hashMapOf(
1414
"w" to "ư",
15+
"ww" to "ư",
1516
"a" to "ă",
1617
"aw" to "ă",
1718
"aa" to "â",
@@ -63,7 +64,7 @@ class VietnameseTelexTest {
6364
fun testDoubleWTransformation() {
6465
val input = "ww"
6566
val shortestFirstResult = applyRulesShortestFirst(input)
66-
assertEquals("", shortestFirstResult)
67+
assertEquals("", shortestFirstResult)
6768

6869
val longestFirstResult = applyRulesLongestFirst(input)
6970
assertEquals("ư", longestFirstResult)
@@ -122,6 +123,34 @@ class VietnameseTelexTest {
122123
assertEquals("O ư", result)
123124
}
124125

126+
@Test
127+
fun testUppercaseCasePreservation_Aw() {
128+
val input = "Aw"
129+
val result = applyRulesWithCasePreservation(input)
130+
assertEquals("Ă", result)
131+
}
132+
133+
@Test
134+
fun testLowercaseCasePreservation_aw() {
135+
val input = "aw"
136+
val result = applyRulesWithCasePreservation(input)
137+
assertEquals("ă", result)
138+
}
139+
140+
@Test
141+
fun testUppercaseCasePreservation_Ow() {
142+
val input = "Ow"
143+
val result = applyRulesWithCasePreservation(input)
144+
assertEquals("Ơ", result)
145+
}
146+
147+
@Test
148+
fun testLowercaseCasePreservation_ow() {
149+
val input = "ow"
150+
val result = applyRulesWithCasePreservation(input)
151+
assertEquals("ơ", result)
152+
}
153+
125154
/**
126155
* Helper function that applies transformation rules checking shortest patterns first.
127156
* This demonstrates incorrect behavior when shorter patterns match before longer ones.
@@ -133,10 +162,11 @@ class VietnameseTelexTest {
133162
for (char in wordChars.size - 1 downTo 0) {
134163
predictWord.append(wordChars[char])
135164
val shouldChangeText = predictWord.reverse().toString()
165+
val shouldChangeTextLower = shouldChangeText.lowercase()
136166

137-
if (telexRules.containsKey(shouldChangeText)) {
167+
if (telexRules.containsKey(shouldChangeTextLower)) {
138168
val prefix = word.substring(0, word.length - shouldChangeText.length)
139-
return prefix + telexRules[shouldChangeText]
169+
return prefix + telexRules[shouldChangeTextLower]
140170
}
141171

142172
predictWord.reverse()
@@ -152,15 +182,39 @@ class VietnameseTelexTest {
152182
private fun applyRulesLongestFirst(word: String): String {
153183
for (length in word.length downTo 1) {
154184
val suffix = word.substring(word.length - length)
155-
if (telexRules.containsKey(suffix)) {
185+
val suffixLower = suffix.lowercase()
186+
if (telexRules.containsKey(suffixLower)) {
156187
val prefix = word.substring(0, word.length - length)
157-
return prefix + telexRules[suffix]!!
188+
return prefix + telexRules[suffixLower]!!
158189
}
159190
}
160191

161192
return word
162193
}
163194

195+
/**
196+
* Helper function that applies transformation rules with case preservation.
197+
* Matches case-insensitively but preserves the original case in output.
198+
*/
199+
private fun applyRulesWithCasePreservation(word: String): String {
200+
for (length in word.length downTo 1) {
201+
val suffix = word.substring(word.length - length)
202+
val suffixLower = suffix.lowercase()
203+
if (telexRules.containsKey(suffixLower)) {
204+
val prefix = word.substring(0, word.length - length)
205+
val replacement = telexRules[suffixLower]!!
206+
// Preserve case: if first char is uppercase, capitalize replacement
207+
val finalReplacement = if (suffix.firstOrNull()?.isUpperCase() == true && replacement.isNotEmpty()) {
208+
replacement.replaceFirstChar { it.uppercase() }
209+
} else {
210+
replacement
211+
}
212+
return prefix + finalReplacement
213+
}
214+
}
215+
return word
216+
}
217+
164218
/**
165219
* Helper function that applies rules with case-sensitive matching.
166220
* Mixed-case input won't match lowercase rules.

0 commit comments

Comments
 (0)