1
0
Fork 0

added support for custom numerals in the language definitions

This commit is contained in:
sspanak 2025-02-17 15:26:07 +02:00 committed by Dimo Karaivanov
parent afa509cee0
commit c28c9f053e
11 changed files with 56 additions and 34 deletions

View file

@ -60,6 +60,7 @@ To support a new language one needs to:
- `hasSpaceBetweenWords` _(optional)_ set to `no` when the language does not use spaces between words. For example: Thai, Chinese, Japanese, Korean, and so on. The default is `yes`. - `hasSpaceBetweenWords` _(optional)_ set to `no` when the language does not use spaces between words. For example: Thai, Chinese, Japanese, Korean, and so on. The default is `yes`.
- `hasUpperCase` _(optional)_ set to `no` when the language has no upper- and lowercase letters. For example: Arabic, Hebrew, East Asian languages, and so on. The default is `yes`. - `hasUpperCase` _(optional)_ set to `no` when the language has no upper- and lowercase letters. For example: Arabic, Hebrew, East Asian languages, and so on. The default is `yes`.
- `name` _(optional)_ is automatically generated and equals the native name of the language (e.g. "English", "Deutsch", "Українська"). However, sometimes, the automatically selected name may be ambiguous. For example, both Portuguese in Portugal and Brazil will default to "Português", so assigning "Português brasileiro" would make it clear it's the language used in Brazil. - `name` _(optional)_ is automatically generated and equals the native name of the language (e.g. "English", "Deutsch", "Українська"). However, sometimes, the automatically selected name may be ambiguous. For example, both Portuguese in Portugal and Brazil will default to "Português", so assigning "Português brasileiro" would make it clear it's the language used in Brazil.
-`numerals` _(optional)_ can be used to set a custom list of numerals. The list must contain exactly 10 characters equivalent to the digits from 0 to 9. For example, in Arabic you could use: `numerals: [٠,١,٢,٣,٤,٥,٦,٧,٨,٩]`.
- `sounds` _(mandatory for non-alphabetic languages)_ is an array of elements in the format: `[sound,digits]`. It is used for East Asian or other languages where there are thousands of different characters, that can not be described in the layout property. `sounds` contains all possible vowel and consonant sounds and their respective digit combinations. There must be no repeating sounds. If a single Latin letter stands for a sound, the letter must be capital. If more than one letter is necessary to represent the sound, the first letter must be capital, while the rest must be small. For example, "A", "P", "Wo", "Ei", "Dd". The sounds are then used in the dictionary format with phonetic transcriptions. See `Korean.yml` and the respective dictionary file for an example. - `sounds` _(mandatory for non-alphabetic languages)_ is an array of elements in the format: `[sound,digits]`. It is used for East Asian or other languages where there are thousands of different characters, that can not be described in the layout property. `sounds` contains all possible vowel and consonant sounds and their respective digit combinations. There must be no repeating sounds. If a single Latin letter stands for a sound, the letter must be capital. If more than one letter is necessary to represent the sound, the first letter must be capital, while the rest must be small. For example, "A", "P", "Wo", "Ei", "Dd". The sounds are then used in the dictionary format with phonetic transcriptions. See `Korean.yml` and the respective dictionary file for an example.
### Dictionary Formats ### Dictionary Formats

View file

@ -3,6 +3,7 @@ currency: ﷼
dictionaryFile: ar-utf8.csv dictionaryFile: ar-utf8.csv
abcString: أﺏﺕ abcString: أﺏﺕ
hasUpperCase: no hasUpperCase: no
numerals: [٠,١,٢,٣,٤,٥,٦,٧,٨,٩]
layout: layout:
- [SPECIAL] # 0 - [SPECIAL] # 0
- [PUNCTUATION_AR] # 1 - [PUNCTUATION_AR] # 1

View file

@ -46,7 +46,7 @@ class ModeABC extends InputMode {
autoAcceptTimeout = 0; autoAcceptTimeout = 0;
digitSequence = String.valueOf(number); digitSequence = String.valueOf(number);
shouldSelectNextLetter = false; shouldSelectNextLetter = false;
suggestions.add(language.getKeyNumber(number)); suggestions.add(language.getKeyNumeral(number));
} else if (repeat > 0) { } else if (repeat > 0) {
autoAcceptTimeout = settings.getAbcAutoAcceptTimeout(); autoAcceptTimeout = settings.getAbcAutoAcceptTimeout();
shouldSelectNextLetter = true; shouldSelectNextLetter = true;
@ -56,7 +56,7 @@ class ModeABC extends InputMode {
digitSequence = String.valueOf(number); digitSequence = String.valueOf(number);
shouldSelectNextLetter = false; shouldSelectNextLetter = false;
suggestions.addAll(KEY_CHARACTERS.size() > number ? KEY_CHARACTERS.get(number) : settings.getOrderedKeyChars(language, number)); suggestions.addAll(KEY_CHARACTERS.size() > number ? KEY_CHARACTERS.get(number) : settings.getOrderedKeyChars(language, number));
suggestions.add(language.getKeyNumber(number)); suggestions.add(language.getKeyNumeral(number));
} }
return true; return true;
@ -86,7 +86,7 @@ class ModeABC extends InputMode {
return false; return false;
} }
suggestions.add(language.getKeyNumber(digitSequence.charAt(0) - '0')); suggestions.add(language.getKeyNumeral(digitSequence.charAt(0) - '0'));
return true; return true;
} }

View file

@ -143,7 +143,7 @@ class ModeCheonjiin extends InputMode {
digitSequence = PUNCTUATION_SEQUENCE; digitSequence = PUNCTUATION_SEQUENCE;
} else { } else {
autoAcceptTimeout = 0; autoAcceptTimeout = 0;
suggestions.add(language.getKeyNumber(number)); suggestions.add(language.getKeyNumeral(number));
} }
} }

View file

@ -94,7 +94,7 @@ class ModeWords extends ModeCheonjiin {
@Override @Override
protected void onNumberHold(int number) { protected void onNumberHold(int number) {
autoAcceptTimeout = 0; autoAcceptTimeout = 0;
suggestions.add(language.getKeyNumber(number)); suggestions.add(language.getKeyNumeral(number));
} }

View file

@ -55,7 +55,7 @@ abstract public class Language {
return getKeyCharacters(key, 0); return getKeyCharacters(key, 0);
} }
@NonNull public String getKeyNumber(int key) { @NonNull public String getKeyNumeral(int key) {
return String.valueOf(key); return String.valueOf(key);
} }

View file

@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import io.github.sspanak.tt9.BuildConfig; import io.github.sspanak.tt9.BuildConfig;
import io.github.sspanak.tt9.util.AssetFile; import io.github.sspanak.tt9.util.AssetFile;
@ -29,6 +30,7 @@ public class LanguageDefinition {
public ArrayList<ArrayList<String>> layout = new ArrayList<>(); public ArrayList<ArrayList<String>> layout = new ArrayList<>();
public String locale = ""; public String locale = "";
public String name = ""; public String name = "";
@NonNull public HashMap<Integer, String> numerals = new HashMap<>();
private boolean inLayout = false; private boolean inLayout = false;
@ -125,28 +127,31 @@ public class LanguageDefinition {
switch (key) { switch (key) {
case "abcString": case "abcString":
abcString = value; abcString = value;
break; return;
case "currency": case "currency":
currency = value; currency = value;
break; return;
case "dictionaryFile": case "dictionaryFile":
dictionaryFile = value.replaceFirst("\\.\\w+$", "." + BuildConfig.DICTIONARY_EXTENSION); dictionaryFile = value.replaceFirst("\\.\\w+$", "." + BuildConfig.DICTIONARY_EXTENSION);
break; return;
case "hasSpaceBetweenWords": case "hasSpaceBetweenWords":
hasSpaceBetweenWords = parseYamlBoolean(value); hasSpaceBetweenWords = parseYamlBoolean(value);
break; return;
case "hasUpperCase": case "hasUpperCase":
hasUpperCase = parseYamlBoolean(value); hasUpperCase = parseYamlBoolean(value);
break; return;
case "sounds": case "sounds":
isSyllabary = true; isSyllabary = true;
break; return;
case "locale": case "locale":
locale = value; locale = value;
break; return;
case "name": case "name":
name = value; name = value;
break; return;
case "numerals":
setNumerals(value);
return;
} }
} }
@ -159,7 +164,7 @@ public class LanguageDefinition {
return inLayout = "layout:".equals(line); return inLayout = "layout:".equals(line);
} }
ArrayList<String> layoutEntry = getLayoutEntryFromYamlLine(line); ArrayList<String> layoutEntry = parseList(line);
if (layoutEntry == null) { if (layoutEntry == null) {
inLayout = false; inLayout = false;
} else { } else {
@ -171,13 +176,25 @@ public class LanguageDefinition {
} }
private void setNumerals(@NonNull String yamlList) {
ArrayList<String> numberList = parseList(yamlList);
if (numberList == null || numberList.size() != 10) {
return;
}
for (int i = 0; i < 10; i++) {
numerals.put(i, numberList.get(i));
}
}
/** /**
* getLayoutEntryFromYamlLine
* Validates a YAML line as an array and returns the character list to be assigned to a given key (a layout entry). * Validates a YAML line as an array and returns the character list to be assigned to a given key (a layout entry).
* If the YAML line is invalid, NULL will be returned. * If the YAML line is invalid, NULL will be returned.
*/ */
@Nullable @Nullable
private ArrayList<String> getLayoutEntryFromYamlLine(@NonNull String yamlLine) { private ArrayList<String> parseList(@NonNull String yamlLine) {
int start = yamlLine.indexOf('['); int start = yamlLine.indexOf('[');
int end = yamlLine.indexOf(']'); int end = yamlLine.indexOf(']');
if (start == -1 || end == -1 || start >= end) { if (start == -1 || end == -1 || start >= end) {

View file

@ -22,6 +22,7 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
protected final ArrayList<ArrayList<String>> layout = new ArrayList<>(); protected final ArrayList<ArrayList<String>> layout = new ArrayList<>();
private final HashMap<Character, String> characterKeyMap = new HashMap<>(); private final HashMap<Character, String> characterKeyMap = new HashMap<>();
@NonNull private HashMap<Integer, String> numerals = new HashMap<>();
public static NaturalLanguage fromDefinition(LanguageDefinition definition) throws Exception { public static NaturalLanguage fromDefinition(LanguageDefinition definition) throws Exception {
@ -37,6 +38,7 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
lang.hasUpperCase = definition.hasUpperCase; lang.hasUpperCase = definition.hasUpperCase;
lang.isSyllabary = definition.isSyllabary; lang.isSyllabary = definition.isSyllabary;
lang.name = definition.name.isEmpty() ? lang.name : definition.name; lang.name = definition.name.isEmpty() ? lang.name : definition.name;
lang.numerals = definition.numerals;
lang.setLocale(definition); lang.setLocale(definition);
lang.setLayout(definition); lang.setLayout(definition);
@ -195,7 +197,7 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
private void generateCharacterKeyMap() { private void generateCharacterKeyMap() {
characterKeyMap.clear(); characterKeyMap.clear();
for (int digit = 0; digit <= 9; digit++) { for (int digit = 0; digit <= 9; digit++) {
characterKeyMap.put(getKeyNumber(digit).charAt(0), String.valueOf(digit)); characterKeyMap.put(getKeyNumeral(digit).charAt(0), String.valueOf(digit));
for (String keyChar : getKeyCharacters(digit)) { for (String keyChar : getKeyCharacters(digit)) {
characterKeyMap.put(keyChar.charAt(0), String.valueOf(digit)); characterKeyMap.put(keyChar.charAt(0), String.valueOf(digit));
} }
@ -224,8 +226,9 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
@NonNull @NonNull
public String getKeyNumber(int key) { public String getKeyNumeral(int key) {
return key >= 0 && key < 10 && LanguageKind.isArabic(this) ? Characters.ArabicNumbers.get(key) : super.getKeyNumber(key); String digit = numerals.containsKey(key) ? numerals.get(key) : null;
return digit != null ? digit : super.getKeyNumeral(key);
} }

View file

@ -85,8 +85,8 @@ public class SoftKeyNumber extends BaseSoftKeyWithIcons {
protected String getLocalizedNumber(int number) { protected String getLocalizedNumber(int number) {
if (isArabicNumber() && tt9 != null && tt9.getLanguage() != null) { if (tt9 != null && !tt9.isInputModeNumeric() && tt9.getLanguage() != null) {
return tt9.getLanguage().getKeyNumber(number); return tt9.getLanguage().getKeyNumeral(number);
} else { } else {
return String.valueOf(number); return String.valueOf(number);
} }
@ -98,9 +98,4 @@ public class SoftKeyNumber extends BaseSoftKeyWithIcons {
float defaultScale = super.getHoldElementScale(); float defaultScale = super.getHoldElementScale();
return tt9 != null && LanguageKind.isArabic(tt9.getLanguage()) ? defaultScale * 1.25f : defaultScale; return tt9 != null && LanguageKind.isArabic(tt9.getLanguage()) ? defaultScale * 1.25f : defaultScale;
} }
private boolean isArabicNumber() {
return tt9 != null && !tt9.isInputModeNumeric() && LanguageKind.isArabic(tt9.getLanguage());
}
} }

View file

@ -17,10 +17,6 @@ class Punctuation {
public static final String ZWNJ = "\u200C"; public static final String ZWNJ = "\u200C";
public static final String ZWNJ_GRAPHIC = "ZWNJ"; public static final String ZWNJ_GRAPHIC = "ZWNJ";
final public static ArrayList<String> ArabicNumbers = new ArrayList<>(Arrays.asList(
"٠", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"
));
final public static ArrayList<Character> CombiningPunctuation = new ArrayList<>(Arrays.asList( final public static ArrayList<Character> CombiningPunctuation = new ArrayList<>(Arrays.asList(
',', '-', '\'', ':', ';', '!', '?', '.' ',', '-', '\'', ':', ';', '!', '?', '.'
)); ));

View file

@ -54,7 +54,7 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir ->
boolean hasLayout = false boolean hasLayout = false
boolean hasSounds = false boolean hasSounds = false
boolean isLocaleValid = false boolean areNumeralsValid = true
String localeString = "" String localeString = ""
String dictionaryFileName = "" String dictionaryFileName = ""
@ -69,6 +69,7 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir ->
&& !rawLine.startsWith("layout") && !rawLine.startsWith("layout")
&& !rawLine.startsWith("locale") && !rawLine.startsWith("locale")
&& !rawLine.startsWith("name") && !rawLine.startsWith("name")
&& !rawLine.startsWith("numerals")
&& !rawLine.startsWith("sounds") && !rawLine.startsWith("sounds")
) { ) {
def parts = rawLine.split(":") def parts = rawLine.split(":")
@ -90,6 +91,10 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir ->
errorMsg += "Language '${languageFile.name}' is invalid. Unrecognized '${property}' value: '${invalidVal}'. Only 'yes' and 'no' are allowed.\n" errorMsg += "Language '${languageFile.name}' is invalid. Unrecognized '${property}' value: '${invalidVal}'. Only 'yes' and 'no' are allowed.\n"
} }
if (line.startsWith("numerals")) {
areNumeralsValid = line.matches("^numerals:\\s*\\[(.,\\s*?){9}.\\]")
}
if (line.startsWith("layout")) { if (line.startsWith("layout")) {
hasLayout = true hasLayout = true
} }
@ -100,7 +105,6 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir ->
if (line.startsWith("locale")) { if (line.startsWith("locale")) {
localeString = line.replace("locale:", "").trim() localeString = line.replace("locale:", "").trim()
isLocaleValid = localeString.matches("^[a-z]{2,3}(?:-[A-Z]{2})?\$")
} }
if (line.startsWith("dictionaryFile")) { if (line.startsWith("dictionaryFile")) {
@ -146,12 +150,17 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir ->
errorMsg += "Language '${languageFile.name}' is invalid. 'sounds' property must contain series of phonetic transcriptions per digit sequence in the format: ' - [Yae,1221]' and so on.\n" errorMsg += "Language '${languageFile.name}' is invalid. 'sounds' property must contain series of phonetic transcriptions per digit sequence in the format: ' - [Yae,1221]' and so on.\n"
} }
if (!isLocaleValid) { if (!localeString.matches("^[a-z]{2,3}(?:-[A-Z]{2})?\$")) {
errorCount++ errorCount++
def msg = localeString.isEmpty() ? "Missing 'locale' property." : "Unrecognized locale format: '${localeString}'" def msg = localeString.isEmpty() ? "Missing 'locale' property." : "Unrecognized locale format: '${localeString}'"
errorMsg += "Language '${languageFile.name}' is invalid. ${msg}\n" errorMsg += "Language '${languageFile.name}' is invalid. ${msg}\n"
} }
if (!areNumeralsValid) {
errorCount++
errorMsg += "Language '${languageFile.name}' is invalid. 'numerals' property must contain a comma-separated list of 10 characters representing the digits from 0 to 9.\n"
}
dictionaryFile = new File("$dictionariesDir/${dictionaryFileName}") dictionaryFile = new File("$dictionariesDir/${dictionaryFileName}")
if (dictionaryFileName.isEmpty() || !dictionaryFile.exists()) { if (dictionaryFileName.isEmpty() || !dictionaryFile.exists()) {
errorCount++ errorCount++