diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c0e64bb..97311499 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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`. - `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. + -`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. ### Dictionary Formats diff --git a/app/languages/definitions/Arabic.yml b/app/languages/definitions/Arabic.yml index 7b73563a..48e88fe1 100644 --- a/app/languages/definitions/Arabic.yml +++ b/app/languages/definitions/Arabic.yml @@ -3,6 +3,7 @@ currency: ﷼ dictionaryFile: ar-utf8.csv abcString: أﺏﺕ hasUpperCase: no +numerals: [٠,١,٢,٣,٤,٥,٦,٧,٨,٩] layout: - [SPECIAL] # 0 - [PUNCTUATION_AR] # 1 diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeABC.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeABC.java index d1605ede..b7b15eeb 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeABC.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeABC.java @@ -46,7 +46,7 @@ class ModeABC extends InputMode { autoAcceptTimeout = 0; digitSequence = String.valueOf(number); shouldSelectNextLetter = false; - suggestions.add(language.getKeyNumber(number)); + suggestions.add(language.getKeyNumeral(number)); } else if (repeat > 0) { autoAcceptTimeout = settings.getAbcAutoAcceptTimeout(); shouldSelectNextLetter = true; @@ -56,7 +56,7 @@ class ModeABC extends InputMode { digitSequence = String.valueOf(number); shouldSelectNextLetter = false; 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; @@ -86,7 +86,7 @@ class ModeABC extends InputMode { return false; } - suggestions.add(language.getKeyNumber(digitSequence.charAt(0) - '0')); + suggestions.add(language.getKeyNumeral(digitSequence.charAt(0) - '0')); return true; } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeCheonjiin.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeCheonjiin.java index 69ce43de..2e3c820a 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeCheonjiin.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeCheonjiin.java @@ -143,7 +143,7 @@ class ModeCheonjiin extends InputMode { digitSequence = PUNCTUATION_SEQUENCE; } else { autoAcceptTimeout = 0; - suggestions.add(language.getKeyNumber(number)); + suggestions.add(language.getKeyNumeral(number)); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeWords.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeWords.java index fef729f3..2bafa17d 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeWords.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModeWords.java @@ -94,7 +94,7 @@ class ModeWords extends ModeCheonjiin { @Override protected void onNumberHold(int number) { autoAcceptTimeout = 0; - suggestions.add(language.getKeyNumber(number)); + suggestions.add(language.getKeyNumeral(number)); } diff --git a/app/src/main/java/io/github/sspanak/tt9/languages/Language.java b/app/src/main/java/io/github/sspanak/tt9/languages/Language.java index a0a4b8fa..68b6a7de 100644 --- a/app/src/main/java/io/github/sspanak/tt9/languages/Language.java +++ b/app/src/main/java/io/github/sspanak/tt9/languages/Language.java @@ -55,7 +55,7 @@ abstract public class Language { return getKeyCharacters(key, 0); } - @NonNull public String getKeyNumber(int key) { + @NonNull public String getKeyNumeral(int key) { return String.valueOf(key); } diff --git a/app/src/main/java/io/github/sspanak/tt9/languages/LanguageDefinition.java b/app/src/main/java/io/github/sspanak/tt9/languages/LanguageDefinition.java index 4547f381..b835cf21 100644 --- a/app/src/main/java/io/github/sspanak/tt9/languages/LanguageDefinition.java +++ b/app/src/main/java/io/github/sspanak/tt9/languages/LanguageDefinition.java @@ -8,6 +8,7 @@ import androidx.annotation.Nullable; import java.io.BufferedReader; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import io.github.sspanak.tt9.BuildConfig; import io.github.sspanak.tt9.util.AssetFile; @@ -29,6 +30,7 @@ public class LanguageDefinition { public ArrayList> layout = new ArrayList<>(); public String locale = ""; public String name = ""; + @NonNull public HashMap numerals = new HashMap<>(); private boolean inLayout = false; @@ -125,28 +127,31 @@ public class LanguageDefinition { switch (key) { case "abcString": abcString = value; - break; + return; case "currency": currency = value; - break; + return; case "dictionaryFile": dictionaryFile = value.replaceFirst("\\.\\w+$", "." + BuildConfig.DICTIONARY_EXTENSION); - break; + return; case "hasSpaceBetweenWords": hasSpaceBetweenWords = parseYamlBoolean(value); - break; + return; case "hasUpperCase": hasUpperCase = parseYamlBoolean(value); - break; + return; case "sounds": isSyllabary = true; - break; + return; case "locale": locale = value; - break; + return; case "name": name = value; - break; + return; + case "numerals": + setNumerals(value); + return; } } @@ -159,7 +164,7 @@ public class LanguageDefinition { return inLayout = "layout:".equals(line); } - ArrayList layoutEntry = getLayoutEntryFromYamlLine(line); + ArrayList layoutEntry = parseList(line); if (layoutEntry == null) { inLayout = false; } else { @@ -171,13 +176,25 @@ public class LanguageDefinition { } + private void setNumerals(@NonNull String yamlList) { + ArrayList 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). * If the YAML line is invalid, NULL will be returned. */ @Nullable - private ArrayList getLayoutEntryFromYamlLine(@NonNull String yamlLine) { + private ArrayList parseList(@NonNull String yamlLine) { int start = yamlLine.indexOf('['); int end = yamlLine.indexOf(']'); if (start == -1 || end == -1 || start >= end) { diff --git a/app/src/main/java/io/github/sspanak/tt9/languages/NaturalLanguage.java b/app/src/main/java/io/github/sspanak/tt9/languages/NaturalLanguage.java index 7a888f3e..2c28091a 100644 --- a/app/src/main/java/io/github/sspanak/tt9/languages/NaturalLanguage.java +++ b/app/src/main/java/io/github/sspanak/tt9/languages/NaturalLanguage.java @@ -22,6 +22,7 @@ public class NaturalLanguage extends Language implements Comparable> layout = new ArrayList<>(); private final HashMap characterKeyMap = new HashMap<>(); + @NonNull private HashMap numerals = new HashMap<>(); public static NaturalLanguage fromDefinition(LanguageDefinition definition) throws Exception { @@ -37,6 +38,7 @@ public class NaturalLanguage extends Language implements Comparable= 0 && key < 10 && LanguageKind.isArabic(this) ? Characters.ArabicNumbers.get(key) : super.getKeyNumber(key); + public String getKeyNumeral(int key) { + String digit = numerals.containsKey(key) ? numerals.get(key) : null; + return digit != null ? digit : super.getKeyNumeral(key); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyNumber.java b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyNumber.java index 93f88158..f8af53b8 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyNumber.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyNumber.java @@ -85,8 +85,8 @@ public class SoftKeyNumber extends BaseSoftKeyWithIcons { protected String getLocalizedNumber(int number) { - if (isArabicNumber() && tt9 != null && tt9.getLanguage() != null) { - return tt9.getLanguage().getKeyNumber(number); + if (tt9 != null && !tt9.isInputModeNumeric() && tt9.getLanguage() != null) { + return tt9.getLanguage().getKeyNumeral(number); } else { return String.valueOf(number); } @@ -98,9 +98,4 @@ public class SoftKeyNumber extends BaseSoftKeyWithIcons { float defaultScale = super.getHoldElementScale(); return tt9 != null && LanguageKind.isArabic(tt9.getLanguage()) ? defaultScale * 1.25f : defaultScale; } - - - private boolean isArabicNumber() { - return tt9 != null && !tt9.isInputModeNumeric() && LanguageKind.isArabic(tt9.getLanguage()); - } } diff --git a/app/src/main/java/io/github/sspanak/tt9/util/chars/Punctuation.java b/app/src/main/java/io/github/sspanak/tt9/util/chars/Punctuation.java index c02a697c..623ee1e1 100644 --- a/app/src/main/java/io/github/sspanak/tt9/util/chars/Punctuation.java +++ b/app/src/main/java/io/github/sspanak/tt9/util/chars/Punctuation.java @@ -17,10 +17,6 @@ class Punctuation { public static final String ZWNJ = "\u200C"; public static final String ZWNJ_GRAPHIC = "ZWNJ"; - final public static ArrayList ArabicNumbers = new ArrayList<>(Arrays.asList( - "٠", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩" - )); - final public static ArrayList CombiningPunctuation = new ArrayList<>(Arrays.asList( ',', '-', '\'', ':', ';', '!', '?', '.' )); diff --git a/app/validate-languages.gradle b/app/validate-languages.gradle index 992070a7..b5d94023 100644 --- a/app/validate-languages.gradle +++ b/app/validate-languages.gradle @@ -54,7 +54,7 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir -> boolean hasLayout = false boolean hasSounds = false - boolean isLocaleValid = false + boolean areNumeralsValid = true String localeString = "" String dictionaryFileName = "" @@ -69,6 +69,7 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir -> && !rawLine.startsWith("layout") && !rawLine.startsWith("locale") && !rawLine.startsWith("name") + && !rawLine.startsWith("numerals") && !rawLine.startsWith("sounds") ) { 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" } + if (line.startsWith("numerals")) { + areNumeralsValid = line.matches("^numerals:\\s*\\[(.,\\s*?){9}.\\]") + } + if (line.startsWith("layout")) { hasLayout = true } @@ -100,7 +105,6 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir -> if (line.startsWith("locale")) { localeString = line.replace("locale:", "").trim() - isLocaleValid = localeString.matches("^[a-z]{2,3}(?:-[A-Z]{2})?\$") } 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" } - if (!isLocaleValid) { + if (!localeString.matches("^[a-z]{2,3}(?:-[A-Z]{2})?\$")) { errorCount++ def msg = localeString.isEmpty() ? "Missing 'locale' property." : "Unrecognized locale format: '${localeString}'" 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}") if (dictionaryFileName.isEmpty() || !dictionaryFile.exists()) { errorCount++