added support for custom numerals in the language definitions
This commit is contained in:
parent
afa509cee0
commit
c28c9f053e
11 changed files with 56 additions and 34 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
',', '-', '\'', ':', ';', '!', '?', '.'
|
',', '-', '\'', ':', ';', '!', '?', '.'
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -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++
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue