diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4fcb609e..5c1ffdce 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -27,8 +27,8 @@ jobs:
- name: Validate Dictionaries
run: ./gradlew validateLanguages
- name: Build Languages
- run: ./gradlew copyLanguages writeDictionaryProperties
+ run: ./gradlew copyDefinitions copyDictionaries writeDictionaryProperties
- name: Lint
run: ./gradlew lint # this actually runs mergeResources, so it must come after the dictionary tasks
- - name: Build Release APK
+ - name: Build all APK variants
run: ./gradlew build
diff --git a/.gitignore b/.gitignore
index 6c2841e6..d22e7cde 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,8 @@ gen/
.gradle/
assets/
build/
+full/
+lite/
release/
# Local configuration file (sdk path, etc)
diff --git a/README.md b/README.md
index 9b6025c7..716a7585 100644
--- a/README.md
+++ b/README.md
@@ -94,7 +94,7 @@ Thanks to your donations, a brand new testing device is available, a Sonim XP380
## πͺ Privacy Policy and Philosophy
- No ads, no premium or paid features. It's all free.
- No spying, no tracking, no telemetry or reports. No nothing!
-- No network connectivity, except when voice input is active.
+- Network connectivity is only used for voice input and downloading dictionaries from Github. You can also use the Full version that includes all languages and requires no Internet permission.
- It only does its job.
- Open-source, so you can verify all the above yourself.
- Created with help from the entire community.
diff --git a/app/build.gradle b/app/build.gradle
index 6a4adb10..db45114f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -17,22 +17,25 @@ tasks.register('validateLanguages') {
}
}
-tasks.register('copyLanguages', Copy) {
+tasks.register('copyDefinitions', Copy) {
from LANGUAGES_INPUT_DIR
- include '**/*.csv'
- include '**/*.txt'
include '**/*.yml'
into LANGUAGES_OUTPUT_DIR
}
+tasks.register('copyDictionaries', Copy) {
+ from DICTIONARIES_INPUT_DIR
+ include '**/*.csv'
+ include '**/*.txt'
+ into DICTIONARIES_OUTPUT_DIR
+}
+
tasks.register('writeDictionaryProperties') {
inputs.dir fileTree(dir: DICTIONARIES_INPUT_DIR)
- outputs.dir DICTIONARIES_OUTPUT_DIR
+ outputs.dir DICTIONARY_META_OUTPUT_DIR
doLast {
- [getDictionarySizes, getDictionaryHashes].parallelStream().forEach { action ->
- action(DICTIONARIES_INPUT_DIR, DICTIONARIES_OUTPUT_DIR)
- }
+ getDictionaryProperties(DICTIONARIES_INPUT_DIR, DICTIONARY_META_OUTPUT_DIR)
}
}
@@ -44,6 +47,7 @@ tasks.register('updateManifest') {
clean {
delete LANGUAGES_OUTPUT_DIR
+ delete DICTIONARIES_OUTPUT_DIR
}
// using the exported Closures directly causes weird values, hence the extra wrappers here
@@ -87,24 +91,35 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
- applicationVariants.configureEach { variant ->
- tasks.named("generate${variant.name.capitalize()}Assets")?.configure {
- dependsOn(validateLanguages, copyLanguages, writeDictionaryProperties)
- }
+ flavorDimensions = ['app']
+ productFlavors {
+ full { dimension 'app' }
+ lite { dimension 'app' }
+ }
- ["lintAnalyzeDebug", "generateDebugLintReportModel", "lintVitalAnalyzeRelease", "generateReleaseLintVitalReportModel"].each { taskName ->
+ applicationVariants.configureEach { variant ->
+ [
+ "merge${variant.name.capitalize()}Assets",
+ "lintAnalyze${variant.name.capitalize()}",
+ "generate${variant.name.capitalize()}LintReportModel",
+ "lintVitalAnalyze${variant.name.capitalize()}",
+ "generate${variant.name.capitalize()}LintVitalReportModel"
+ ].each { taskName ->
try {
tasks.named(taskName)?.configure {
- dependsOn(validateLanguages, copyLanguages, writeDictionaryProperties)
+ dependsOn(validateLanguages, copyDefinitions, copyDictionaries, writeDictionaryProperties)
}
} catch (UnknownTaskException ignored) {}
}
- assembleDebug.finalizedBy(updateManifest)
- assembleRelease.finalizedBy(updateManifest)
+ assembleLiteDebug.finalizedBy(updateManifest)
+ assembleFullDebug.finalizedBy(updateManifest)
+ assembleLiteRelease.finalizedBy(updateManifest)
+ assembleFullRelease.finalizedBy(updateManifest)
variant.outputs.configureEach {
- outputFileName = "${APP_NAME}-v${getVerName()}.apk"
+ def suffix = variant.flavorName == 'full' ? '-full' : ''
+ outputFileName = "${APP_NAME}-v${getVerName()}${suffix}.apk"
}
}
}
diff --git a/app/constants.gradle b/app/constants.gradle
index 31afbc89..b715549d 100644
--- a/app/constants.gradle
+++ b/app/constants.gradle
@@ -7,15 +7,17 @@ ext.DICTIONARIES_DIR_NAME = 'dictionaries'
ext.DICTIONARY_SIZES_DIR_NAME = 'dictionary-sizes'
def ROOT_DIR = "${project.rootDir}/app"
-def ASSETS_DIR = "${ROOT_DIR}/src/main/assets"
+def MAIN_ASSETS_DIR = "${ROOT_DIR}/src/main/assets"
+def FULL_VERSION_ASSETS_DIR = "${ROOT_DIR}/src/full/assets"
ext.LANGUAGES_INPUT_DIR = "${ROOT_DIR}/${LANGUAGES_DIR_NAME}"
ext.DEFINITIONS_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DEFINITIONS_DIR_NAME}"
ext.DICTIONARIES_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DICTIONARIES_DIR_NAME}"
-ext.LANGUAGES_OUTPUT_DIR = "${ASSETS_DIR}/${LANGUAGES_DIR_NAME}"
+ext.LANGUAGES_OUTPUT_DIR = "${MAIN_ASSETS_DIR}/${LANGUAGES_DIR_NAME}"
ext.DEFINITIONS_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DEFINITIONS_DIR_NAME}"
-ext.DICTIONARIES_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DICTIONARIES_DIR_NAME}"
+ext.DICTIONARY_META_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DICTIONARIES_DIR_NAME}"
+ext.DICTIONARIES_OUTPUT_DIR = "${FULL_VERSION_ASSETS_DIR}/${LANGUAGES_DIR_NAME}/${DICTIONARIES_DIR_NAME}"
ext.LANGUAGE_VALIDATION_DIR = layout.buildDirectory.dir("langValidation")
diff --git a/app/dictionary-tools.gradle b/app/dictionary-tools.gradle
index d3a01f5b..57844f9b 100644
--- a/app/dictionary-tools.gradle
+++ b/app/dictionary-tools.gradle
@@ -1,13 +1,9 @@
-ext.getDictionarySizes = { dictionariesDir, sizesDir ->
- fileTree(dir: dictionariesDir).getFiles().parallelStream().forEach {dictionary ->
- def dictionarySize = dictionary.exists() ? dictionary.text.split("\n").length : 0
- new File(sizesDir, "${dictionary.getName()}.size").text = dictionarySize
- }
-}
-
-ext.getDictionaryHashes = { dictionariesDir, timestampsDir ->
+ext.getDictionaryProperties = { dictionariesDir, sizesDir ->
fileTree(dir: dictionariesDir).getFiles().parallelStream().forEach {dictionary ->
def hash = dictionary.exists() ? dictionary.text.digest("SHA-1") : ""
- new File(timestampsDir, "${dictionary.getName()}.hash").text = hash
+ def revision = dictionary.exists() ? exec("git log --pretty=tformat:%H -n 1 ${dictionary}") : ""
+ def words = dictionary.exists() ? dictionary.text.split("\n").length : 0
+
+ new File(sizesDir, "${dictionary.getName()}.props.yml").text = "hash: ${hash}\nrevision: ${revision}\nwords: ${words}"
}
-}
+}
\ No newline at end of file
diff --git a/app/src/lite/AndroidManifest.xml b/app/src/lite/AndroidManifest.xml
new file mode 100644
index 00000000..e0e81ba4
--- /dev/null
+++ b/app/src/lite/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java b/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java
index 20fcb467..8e4942ce 100644
--- a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java
+++ b/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java
@@ -68,7 +68,7 @@ public class DictionaryLoader {
}
- public boolean load(ArrayList languages) {
+ public boolean load(Context context, ArrayList languages) {
if (isRunning()) {
return false;
}
@@ -91,7 +91,7 @@ public class DictionaryLoader {
if (isInterrupted()) {
break;
}
- importAll(lang);
+ importAll(context, lang);
currentFile++;
}
}
@@ -105,7 +105,7 @@ public class DictionaryLoader {
public static void load(Context context, Language language) {
DictionaryLoadingBar progressBar = DictionaryLoadingBar.getInstance(context);
getInstance(context).setOnStatusChange(status -> progressBar.show(context, status));
- self.load(new ArrayList() {{ add(language); }});
+ self.load(context, new ArrayList() {{ add(language); }});
}
@@ -129,7 +129,7 @@ public class DictionaryLoader {
load(context, language);
}
// or if the database is outdated, compared to the dictionary file, ask for confirmation and load
- else if (!hash.equals(new WordFile(language.getDictionaryFile(), self.assets).getHash())) {
+ else if (!hash.equals(new WordFile(context, language.getDictionaryFile(), self.assets).getHash())) {
new DictionaryUpdateNotification(context, language).show();
}
},
@@ -151,7 +151,7 @@ public class DictionaryLoader {
}
- private void importAll(Language language) {
+ private void importAll(Context context, Language language) {
if (language == null) {
Logger.e(LOG_TAG, "Failed loading a dictionary for NULL language.");
sendError(InvalidLanguageException.class.getSimpleName(), -1);
@@ -178,7 +178,7 @@ public class DictionaryLoader {
sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
logLoadingStep("Letters imported", language, Timer.restart());
- importWordFile(language, lettersCount, progress, 88);
+ importWordFile(context, language, lettersCount, progress, 88);
progress = 88;
sendProgressMessage(language, progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
logLoadingStep("Dictionary file imported", language, Timer.restart());
@@ -252,8 +252,8 @@ public class DictionaryLoader {
}
- private void importWordFile(Language language, int positionShift, float minProgress, float maxProgress) throws Exception {
- WordFile wordFile = new WordFile(language.getDictionaryFile(), assets);
+ private void importWordFile(Context context, Language language, int positionShift, float minProgress, float maxProgress) throws Exception {
+ WordFile wordFile = new WordFile(context, language.getDictionaryFile(), assets);
WordBatch batch = new WordBatch(language, wordFile.getTotalLines());
int currentLine = 1;
float progressRatio = (maxProgress - minProgress) / wordFile.getTotalLines();
diff --git a/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java b/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java
index b2ae39cc..db5a0308 100644
--- a/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java
+++ b/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java
@@ -1,27 +1,39 @@
package io.github.sspanak.tt9.db.entities;
+import android.content.Context;
import android.content.res.AssetManager;
import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStream;
import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
+import io.github.sspanak.tt9.BuildConfig;
+import io.github.sspanak.tt9.R;
+import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.Logger;
public class WordFile {
private static final String LOG_TAG = WordFile.class.getSimpleName();
private final AssetManager assets;
+ private final Context context;
private final String name;
private String hash = null;
+ private String downloadUrl = null;
private int totalLines = -1;
- public WordFile(String name, AssetManager assets) {
+
+ public WordFile(Context context, String name, AssetManager assets) {
this.assets = assets;
+ this.context = context;
this.name = name;
}
+
public static String[] splitLine(String line) {
String[] parts = { line, "" };
@@ -38,6 +50,7 @@ public class WordFile {
return parts;
}
+
public static short getFrequencyFromLineParts(String[] frequencyParts) {
try {
return Short.parseShort(frequencyParts[1]);
@@ -46,40 +59,115 @@ public class WordFile {
}
}
- public BufferedReader getReader() throws IOException {
- return new BufferedReader(new InputStreamReader(assets.open(name), StandardCharsets.UTF_8));
+
+ public boolean exists() {
+ try {
+ assets.open(name).close();
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
}
- public int getTotalLines() {
- if (totalLines < 0) {
- String rawTotalLines = getProperty("size");
- try {
- totalLines = Integer.parseInt(rawTotalLines);
- } catch (Exception e) {
- Logger.w(LOG_TAG, "Invalid 'size' property of: " + name + ". Expecting an integer, got: '" + rawTotalLines + "'.");
- totalLines = 0;
- }
+
+ public InputStream getRemoteStream() throws IOException {
+ URLConnection connection = new URL(getDownloadUrl()).openConnection();
+ connection.setConnectTimeout(SettingsStore.DICTIONARY_DOWNLOAD_CONNECTION_TIMEOUT);
+ connection.setReadTimeout(SettingsStore.DICTIONARY_DOWNLOAD_READ_TIMEOUT);
+ return connection.getInputStream();
+ }
+
+
+ public BufferedReader getReader() throws IOException {
+ InputStream stream = exists() ? assets.open(name) : getRemoteStream();
+ return new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
+ }
+
+
+ private String getDownloadUrl() {
+ if (downloadUrl == null) {
+ loadProperties();
}
- return totalLines;
+ return downloadUrl;
}
+
+ private void setDownloadUrl(String rawProperty, String rawValue) {
+ if (!rawProperty.equals("revision")) {
+ return;
+ }
+
+ String revision = rawValue == null || rawValue.isEmpty() ? "" : rawValue;
+ downloadUrl = revision.isEmpty() ? null : context.getString(R.string.dictionary_url, revision, name);
+
+ if (revision.isEmpty()) {
+ Logger.w(LOG_TAG, "Invalid 'revision' property of: " + name + ". Expecting a string, got: '" + rawValue + "'.");
+ }
+ }
+
+
public String getHash() {
if (hash == null) {
- hash = getProperty("hash");
+ loadProperties();
}
return hash;
}
- private String getProperty(String propertyName) {
- String propertyFilename = name + "." + propertyName;
+
+ private void setHash(String rawProperty, String rawValue) {
+ if (!rawProperty.equals("hash")) {
+ return;
+ }
+
+ hash = rawValue == null || rawValue.isEmpty() ? "" : rawValue;
+
+ if (hash.isEmpty()) {
+ Logger.w(LOG_TAG, "Invalid 'hash' property of: " + name + ". Expecting a string, got: '" + rawValue + "'.");
+ }
+ }
+
+
+ public int getTotalLines() {
+ if (totalLines < 0) {
+ loadProperties();
+ }
+
+ return totalLines;
+ }
+
+
+ private void setTotalLines(String rawProperty, String rawValue) {
+ if (!rawProperty.equals("words")) {
+ return;
+ }
+
+ try {
+ totalLines = Integer.parseInt(rawValue);
+ } catch (Exception e) {
+ Logger.w(LOG_TAG, "Invalid 'words' property of: " + name + ". Expecting an integer, got: '" + rawValue + "'.");
+ totalLines = 0;
+ }
+ }
+
+
+ private void loadProperties() {
+ String propertyFilename = name + ".props.yml";
try (BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(propertyFilename)))) {
- return reader.readLine();
+ for (String line; (line = reader.readLine()) != null; ) {
+ String[] parts = line.split("\\s*:\\s*");
+ if (parts.length < 2) {
+ continue;
+ }
+
+ setDownloadUrl(parts[0], parts[1]);
+ setHash(parts[0], parts[1]);
+ setTotalLines(parts[0], parts[1]);
+ }
} catch (Exception e) {
- Logger.w(LOG_TAG, "Could not read the '" + propertyName + "' property of: " + name + " from: " + propertyFilename + ". " + e.getMessage());
- return "";
+ Logger.w(LOG_TAG, "Could not read the property file: " + propertyFilename + ". " + e.getMessage());
}
}
}
diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java
index 275db768..ef59d892 100644
--- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java
+++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java
@@ -70,7 +70,7 @@ class ItemLoadDictionary extends ItemClickable {
ArrayList languages = LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds());
setLoadingStatus();
- if (!loader.load(languages)) {
+ if (!loader.load(activity, languages)) {
loader.stop();
setReadyStatus();
}
diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java
index 10393beb..a4aeadc8 100644
--- a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java
+++ b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java
@@ -9,6 +9,8 @@ public class SettingsStore extends SettingsUI {
/************* internal settings *************/
public final static int DELETE_WORDS_SEARCH_DELAY = 500; // ms
public final static int DICTIONARY_AUTO_LOAD_COOLDOWN_TIME = 1200000; // 20 minutes in ms
+ public final static int DICTIONARY_DOWNLOAD_CONNECTION_TIMEOUT = 10000; // ms
+ public final static int DICTIONARY_DOWNLOAD_READ_TIMEOUT = 10000; // ms
public final static int DICTIONARY_IMPORT_BATCH_SIZE = 5000; // words
public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms
public final static byte SLOW_QUERY_TIME = 50; // ms
diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryLoadingBar.java b/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryLoadingBar.java
index 75c69921..6d901da3 100644
--- a/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryLoadingBar.java
+++ b/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryLoadingBar.java
@@ -5,6 +5,8 @@ import android.os.Bundle;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.net.SocketException;
+import java.net.UnknownHostException;
import java.util.Locale;
import io.github.sspanak.tt9.R;
@@ -138,6 +140,8 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
message = resources.getString(R.string.add_word_invalid_language);
} else if (errorType.equals(DictionaryImportException.class.getSimpleName()) || errorType.equals(InvalidLanguageCharactersException.class.getSimpleName())) {
message = resources.getString(R.string.dictionary_load_bad_char, word, line, lang.getName());
+ } else if (errorType.equals(UnknownHostException.class.getSimpleName()) || errorType.equals(SocketException.class.getSimpleName())) {
+ message = resources.getString(R.string.dictionary_load_no_internet, lang.getName());
} else if (errorType.equals(IOException.class.getSimpleName()) || errorType.equals(FileNotFoundException.class.getSimpleName())) {
message = resources.getString(R.string.dictionary_not_found, lang.getName());
} else {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 262d7d75..d3f82be9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,6 @@
+ https://raw.githubusercontent.com/sspanak/tt9/%1$s/app/%2$s
https://github.com/sspanak/tt9/blob/master/docs/user-manual.md
Traditional T9
TT9
@@ -80,6 +81,7 @@
Cancel Loading
Loading failed. Invalid word \"%1$s\" on line %2$d of language \"%3$s\".
Failed loading the dictionary for language \"%1$s\" (%2$s).
+ Failed downloading the dictionary for language \"%1$s\". Check your Internet connection.
Dictionary load cancelled.
Dictionary load completed.
Loading dictionary (%1$s)β¦
diff --git a/app/version-tools.gradle b/app/version-tools.gradle
index 374b2f63..2010f643 100644
--- a/app/version-tools.gradle
+++ b/app/version-tools.gradle
@@ -44,6 +44,10 @@ def generateVersionName() {
return "$versionTagsCount.$commitsSinceLastTag$betaString"
}
+ext.exec = { command ->
+ return execThing(command)
+}
+
ext.getVersionName = { ->
return generateVersionName()
}
diff --git a/fastlane/metadata/android/bg-BG/full_description.txt b/fastlane/metadata/android/bg-BG/full_description.txt
index bbee1660..37854f86 100644
--- a/fastlane/metadata/android/bg-BG/full_description.txt
+++ b/fastlane/metadata/android/bg-BG/full_description.txt
@@ -1,11 +1,11 @@
-Traditional T9 Π΅ 12-ΠΊΠ»Π°Π²ΠΈΡΠ½Π° ΠΊΠ»Π°Π²ΠΈΠ°ΡΡΡΠ° Π·Π° ΡΡΡΡΠΎΠΉΡΡΠ²Π° Ρ ΠΊΠΎΠΏΡΠ΅ΡΠ°. ΠΠΎΠ΄Π΄ΡΡΠΆΠ° ΠΏΠΎΠ΄ΡΠΊΠ°Π²Π°Ρ ΡΠ΅ΠΊΡΡΠ° Π½Π° ΠΏΠΎΠ²Π΅ΡΠ΅ ΠΎΡ 25 Π΅Π·ΠΈΠΊΠ° ΠΈ Π±ΡΡΠ·ΠΈ ΠΊΠ»Π°Π²ΠΈΡΠΈ, Π° Π²ΠΈΡΡΡΠ°Π»Π½Π°ΡΠ° ΠΊΠ»Π°Π²ΠΈΠ°ΡΡΡΠ° ΠΏΡΠ΅ΡΡΠ·Π΄Π°Π²Π° Π΄ΠΎΠ±ΡΠΎΡΠΎ ΡΡΠ°ΡΠΎ ΡΡΠ΅ΡΠ°Π½Π΅ Π·Π° ΠΠΎΠΊΠΈΡ Π½Π° ΡΡΠ²ΡΠ΅ΠΌΠ΅Π½Π½ΠΈΡΠ΅ ΡΠ΅Π»Π΅ΡΠΎΠ½ΠΈ ΡΡΡ ΡΠ΅Π½Π·ΠΎΡΠ΅Π½ Π΅ΠΊΡΠ°Π½. Π Π½Π°ΠΉ-Ρ
ΡΠ±Π°Π²ΠΎΡΠΎ Π΅, ΡΠ΅ Π½Π΅ Π²ΠΈ ΡΠΏΠΈΠΎΠ½ΠΈΡΠ°.
+Traditional T9 Π΅ 12-ΠΊΠ»Π°Π²ΠΈΡΠ½Π° ΠΊΠ»Π°Π²ΠΈΠ°ΡΡΡΠ° Π·Π° ΡΡΡΡΠΎΠΉΡΡΠ²Π° Ρ ΠΊΠΎΠΏΡΠ΅ΡΠ°. ΠΠΎΠ΄Π΄ΡΡΠΆΠ° ΠΏΠΎΠ΄ΡΠΊΠ°Π²Π°Ρ ΡΠ΅ΠΊΡΡ Π½Π° ΠΏΠΎΠ²Π΅ΡΠ΅ ΠΎΡ 25 Π΅Π·ΠΈΠΊΠ° ΠΈ Π±ΡΡΠ·ΠΈ ΠΊΠ»Π°Π²ΠΈΡΠΈ, Π° Π²ΠΈΡΡΡΠ°Π»Π½Π°ΡΠ° ΠΊΠ»Π°Π²ΠΈΠ°ΡΡΡΠ° ΠΏΡΠ΅ΡΡΠ·Π΄Π°Π²Π° Π΄ΠΎΠ±ΡΠΎΡΠΎ ΡΡΠ°ΡΠΎ ΡΡΠ΅ΡΠ°Π½Π΅ Π·Π° ΠΠΎΠΊΠΈΡ Π½Π° ΡΡΠ²ΡΠ΅ΠΌΠ΅Π½Π½ΠΈΡΠ΅ ΡΠ΅Π»Π΅ΡΠΎΠ½ΠΈ ΡΡΡ ΡΠ΅Π½Π·ΠΎΡΠ΅Π½ Π΅ΠΊΡΠ°Π½. Π Π½Π°ΠΉ-Ρ
ΡΠ±Π°Π²ΠΎΡΠΎ Π΅, ΡΠ΅ Π½Π΅ Π²ΠΈ ΡΠΏΠΈΠΎΠ½ΠΈΡΠ°.
ΠΠΎΠ΄Π΄ΡΡΠΆΠ°Π½ΠΈ Π΅Π·ΠΈΡΠΈ: Π°ΡΠ°Π±ΡΠΊΠΈ, Π±ΡΠ»Π³Π°ΡΡΠΊΠΈ, Ρ
ΡΡΠ²Π°ΡΡΠΊΠΈ, ΡΠ΅ΡΠΊΠΈ, Π΄Π°ΡΡΠΊΠΈ, Ρ
ΠΎΠ»Π°Π½Π΄ΡΠΊΠΈ, Π°Π½Π³Π»ΠΈΠΉΡΠΊΠΈ, ΡΠΈΠ½Π»Π°Π½Π΄ΡΠΊΠΈ, ΡΡΠ΅Π½ΡΠΊΠΈ, Π½Π΅ΠΌΡΠΊΠΈ, Π³ΡΡΡΠΊΠΈ, ΠΈΠ²ΡΠΈΡ, ΡΠ½Π³Π°ΡΡΠΊΠΈ, ΠΈΠ½Π΄ΠΎΠ½Π΅Π·ΠΈΠΉΡΠΊΠΈ, ΠΈΡΠ°Π»ΠΈΠ°Π½ΡΠΊΠΈ, ΠΊΠΈΡΡΠ°Ρ
ΠΈΠ»ΠΈ, Π½ΠΎΡΠ²Π΅ΠΆΠΊΠΈ, ΠΏΠΎΠ»ΡΠΊΠΈ, ΠΏΠΎΡΡΡΠ³Π°Π»ΡΠΊΠΈ (Π΅Π²ΡΠΎΠΏΠ΅ΠΉΡΠΊΠΈ ΠΈ Π±ΡΠ°Π·ΠΈΠ»ΡΠΊΠΈ), ΡΡΠΌΡΠ½ΡΠΊΠΈ, ΡΡΡΠΊΠΈ, ΠΈΡΠΏΠ°Π½ΡΠΊΠΈ, ΡΠ²Π΅Π΄ΡΠΊΠΈ, ΡΡΡΡΠΊΠΈ, ΡΠΊΡΠ°ΠΈΠ½ΡΠΊΠΈ, ΠΈΠ΄ΠΈΡ.
Π€ΠΈΠ»ΠΎΡΠΎΡΠΈΡ ΠΈ Π·Π°ΡΠΈΡΠ° Π½Π΅ Π»ΠΈΡΠ½ΠΈΡΠ΅ Π΄Π°Π½Π½ΠΈ:
- ΠΠ΅Π· ΡΠ΅ΠΊΠ»Π°ΠΌΠΈ, ΡΠΏΠ΅ΡΠΈΠ°Π»Π½ΠΈ ΠΈΠ»ΠΈ ΠΏΠ»Π°ΡΠ΅Π½ΠΈ ΡΡΠ½ΠΊΡΠΈΠΈ. ΠΡΠΈΡΠΊΠΎ Π΅ Π½Π°ΠΏΡΠ»Π½ΠΎ Π±Π΅Π·ΠΏΠ»Π°ΡΠ½ΠΎ.
- ΠΠ΅Π· ΡΠΏΠΈΠΎΠ½ΠΈΡΠ°Π½Π΅, ΡΠ»Π΅Π΄Π΅Π½Π΅, ΡΠ΅Π»Π΅ΠΌΠ΅ΡΡΠΈΡ ΠΈ ΠΎΡΡΠ΅ΡΠΈ. ΠΠ΅Π· Π³Π»ΡΠΏΠΎΡΡΠΈ!
-- ΠΠ΅Π· Π²ΡΡΠ·ΠΊΠ° ΠΊΡΠΌ ΠΈΠ½ΡΠ΅ΡΠ½Π΅Ρ, ΠΎΡΠ²Π΅Π½ ΠΊΠΎΠ³Π°ΡΠΎ Π΅ Π°ΠΊΡΠΈΠ²Π½ΠΎ Π³Π»Π°ΡΠΎΠ²ΠΎΡΠΎ Π²ΡΠ²Π΅ΠΆΠ΄Π°Π½Π΅.
+- ΠΠ·ΠΏΠΎΠ»Π·Π²Π° ΠΈΠ½ΡΠ΅ΡΠ½Π΅Ρ ΡΠ°ΠΌΠΎ ΠΏΡΠΈ Π°ΠΊΡΠΈΠ²Π½ΠΎ Π³Π»Π°ΡΠΎΠ²ΠΎ Π²ΡΠ²Π΅ΠΆΠ΄Π°Π½Π΅ ΠΈ Π·Π° ΠΈΠ·ΡΠ΅Π³Π»ΡΠ½Π΅ Π½Π° ΡΠ΅ΡΠ½ΠΈΡΠΈ ΠΎΡ Github. ΠΠΎΠΆΠ΅ΡΠ΅ Π΄Π° ΠΈΠ·Π±Π΅ΡΠ΅ΡΠ΅ ΠΈ ΠΏΡΠ»Π½Π°ΡΠ° Π²Π΅ΡΡΠΈΡ, ΠΊΠΎΡΡΠΎ ΡΡΠ΄ΡΡΠΆΠ° Π²ΡΠΈΡΠΊΠΈ Π΅Π·ΠΈΡΠΈ ΠΈ Π½Π΅ ΠΈΠ·ΠΈΡΠΊΠ²Π° ΡΠ°Π·ΡΠ΅ΡΠ΅Π½ΠΈΠ΅ΡΠΎ Π·Π° ΠΈΠ½ΡΠ΅ΡΠ½Π΅Ρ.
- ΠΠ΄ΠΈΠ½ΡΡΠ²Π΅Π½ΠΎ ΡΠΈ Π²ΡΡΡΠΈ ΡΠ°Π±ΠΎΡΠ°ΡΠ°.
- Π‘ ΠΎΡΠ²ΠΎΡΠ΅Π½ ΠΊΠΎΠ΄, ΡΠ°ΠΊΠ° ΡΠ΅ ΠΌΠΎΠΆΠ΅ Π΄Π° ΠΏΡΠΎΠ²Π΅ΡΠΈΡΠ΅ Π³ΠΎΡΠ½ΠΎΡΠΎ ΠΈ ΡΠ°ΠΌΠΈ.
- Π‘ΡΠ·Π΄Π°Π΄Π΅Π½Π° Ρ ΠΏΠΎΠΌΠΎΡΡΠ° Π½Π° ΡΡΠ»Π°ΡΠ° ΠΎΠ±ΡΠ½ΠΎΡΡ.
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 04047369..7865f843 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -5,7 +5,7 @@ Supported languages: Arabic, Bulgarian, Croatian, Czech, Danish, Dutch, English,
Privacy Policy and Philosophy:
- No ads, no premium or paid features. It's all free.
- No spying, no tracking, no telemetry or reports. No nothing!
-- No network connectivity, except when voice input is active.
+- Network connectivity is only used for voice input and downloading dictionaries from Github. You can also use the Full version that includes all languages and requires no Internet permission.
- It only does its job.
- Open-source, so you can verify all the above yourself.
-- Created with help from the entire community.
\ No newline at end of file
+- Created with help from the entire community.