1
0
Fork 0

New dictionary format (#662)

* new dictionary format that supports syllabaries

* optimized the dictionary build cache significantly to truly build only the changed language files

* code style fixes
This commit is contained in:
Dimo Karaivanov 2024-11-06 10:43:16 +02:00 committed by GitHub
parent 56b355631a
commit da5b4f17b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 871 additions and 397 deletions

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
android:versionCode="774"
android:versionName="40.0"
android:versionCode="775"
android:versionName="40.1"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- allows displaying notifications on Android >= 13 -->

View file

@ -70,7 +70,7 @@ public class CustomWordFile {
return null;
}
String[] parts = WordFile.splitLine(line);
String[] parts = WordFile.getLineData(line);
if (parts == null || parts.length < 2) {
return null;
}
@ -79,7 +79,7 @@ public class CustomWordFile {
}
@NonNull public static String getWord(String line) {
String[] parts = WordFile.splitLine(line);
String[] parts = WordFile.getLineData(line);
return parts != null && parts.length > 0 ? parts[0] : "";
}
}

View file

@ -4,16 +4,14 @@ import androidx.annotation.NonNull;
import java.util.ArrayList;
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
public class WordBatch {
@NonNull private final Language language;
@NonNull private final ArrayList<Word> words;
@NonNull private final ArrayList<WordPosition> positions;
private WordPosition lastWordPosition;
public WordBatch(@NonNull Language language, int size) {
this.language = language;
words = size > 0 ? new ArrayList<>(size) : new ArrayList<>();
@ -24,31 +22,25 @@ public class WordBatch {
this(language, 0);
}
public boolean add(@NonNull String word, int frequency, int position) throws InvalidLanguageCharactersException {
public void add(String word, int frequency, int position) throws InvalidLanguageCharactersException {
words.add(Word.create(word, frequency, position));
positions.add(WordPosition.create(language.getDigitSequenceForWord(word), position, position));
}
public void add(@NonNull ArrayList<String> words, @NonNull String digitSequence, int position) {
if (words.isEmpty() || digitSequence.isEmpty()) {
return;
}
for (int i = 0, size = words.size(); i < size; i++) {
this.words.add(Word.create(words.get(i), size - i, position + i));
}
if (position == 0) {
return true;
return;
}
String sequence = language.getDigitSequenceForWord(word);
if (position == 1 || lastWordPosition == null) {
lastWordPosition = WordPosition.create(sequence, position);
} else {
lastWordPosition.end = position;
}
if (!sequence.equals(lastWordPosition.sequence)) {
lastWordPosition.end--;
positions.add(lastWordPosition);
lastWordPosition = WordPosition.create(sequence, position);
return true;
}
return false;
positions.add(WordPosition.create(digitSequence, position, position + words.size() - 1));
}
public void clear() {

View file

@ -4,13 +4,17 @@ import android.content.Context;
import android.content.res.AssetManager;
import java.io.BufferedReader;
import java.io.File;
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 java.util.ArrayList;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
@ -21,26 +25,34 @@ public class WordFile {
private final AssetManager assets;
private final Context context;
private final String name;
private final String path;
private int lastCharCode;
private BufferedReader reader;
private String hash = null;
private String downloadUrl = null;
private int totalLines = -1;
private int words = -1;
private long size = -1;
private int sequences = -1;
public WordFile(Context context, String name, AssetManager assets) {
public WordFile(Context context, String path, AssetManager assets) {
this.assets = assets;
this.context = context;
this.name = name;
this.path = path;
lastCharCode = 0;
reader = null;
}
public static String[] splitLine(String line) {
String[] parts = { line, "" };
public static String[] getLineData(String line) {
String[] parts = {line, ""};
// This is faster than String.split() by around 10%, so it's worth having it.
// It runs very often, so any other optimizations are welcome.
for (int i = 0 ; i < line.length(); i++) {
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) == ' ') { // the delimiter is TAB
parts[0] = line.substring(0, i);
parts[1] = i < line.length() - 1 ? line.substring(i + 1) : "";
@ -52,18 +64,9 @@ public class WordFile {
}
public static short getFrequencyFromLineParts(String[] frequencyParts) {
try {
return Short.parseShort(frequencyParts[1]);
} catch (Exception e) {
return 0;
}
}
public boolean exists() {
try {
assets.open(name).close();
assets.open(path).close();
return true;
} catch (IOException e) {
return false;
@ -80,8 +83,17 @@ public class WordFile {
public BufferedReader getReader() throws IOException {
InputStream stream = exists() ? assets.open(name) : getRemoteStream();
return new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
if (reader != null) {
return reader;
}
InputStream stream = exists() ? assets.open(path) : getRemoteStream();
ZipInputStream zipStream = new ZipInputStream(stream);
ZipEntry entry = zipStream.getNextEntry();
if (entry == null) {
throw new IOException("Dictionary ZIP file: " + path + " is empty.");
}
return reader = new BufferedReader(new InputStreamReader(zipStream, StandardCharsets.UTF_8));
}
@ -99,12 +111,20 @@ public class WordFile {
return;
}
String revision = rawValue == null || rawValue.isEmpty() ? "" : rawValue;
downloadUrl = revision.isEmpty() ? null : context.getString(R.string.dictionary_url, revision, name);
downloadUrl = null;
String revision = rawValue == null || rawValue.isEmpty() ? "" : rawValue;
if (revision.isEmpty()) {
Logger.w(LOG_TAG, "Invalid 'revision' property of: " + name + ". Expecting a string, got: '" + rawValue + "'.");
Logger.w(LOG_TAG, "Invalid 'revision' property of: " + path + ". Expecting a string, got: '" + rawValue + "'.");
return;
}
if (path == null || path.isEmpty()) {
Logger.w(LOG_TAG, "Cannot generate a download URL for an empty path.");
return;
}
downloadUrl = context.getString(R.string.dictionary_url, revision, new File(path).getName());
}
@ -125,39 +145,62 @@ public class WordFile {
hash = rawValue == null || rawValue.isEmpty() ? "" : rawValue;
if (hash.isEmpty()) {
Logger.w(LOG_TAG, "Invalid 'hash' property of: " + name + ". Expecting a string, got: '" + rawValue + "'.");
Logger.w(LOG_TAG, "Invalid 'hash' property of: " + path + ". Expecting a string, got: '" + rawValue + "'.");
}
}
public int getTotalLines() {
if (totalLines < 0) {
public int getSequences() {
if (sequences < 0) {
loadProperties();
}
return totalLines;
return sequences;
}
public String getFormattedTotalLines(String suffix) {
if (getTotalLines() > 1000000) {
return String.format(Locale.ROOT, "%1.2fM %s", getTotalLines() / 1000000.0, suffix);
} else {
return getTotalLines() / 1000 + "k " + suffix;
private void setSequences(String rawProperty, String rawValue) {
if (!rawProperty.equals("sequences")) {
return;
}
try {
sequences = Integer.parseInt(rawValue);
} catch (Exception e) {
Logger.w(LOG_TAG, "Invalid 'sequences' property of: " + path + ". Expecting an integer, got: '" + rawValue + "'.");
sequences = 0;
}
}
private void setTotalLines(String rawProperty, String rawValue) {
public int getWords() {
if (words < 0) {
loadProperties();
}
return words;
}
public String getFormattedWords(String suffix) {
if (getWords() > 1000000) {
return String.format(Locale.ROOT, "%1.2fM %s", getWords() / 1000000.0, suffix);
} else {
return getWords() / 1000 + "k " + suffix;
}
}
private void setWords(String rawProperty, String rawValue) {
if (!rawProperty.equals("words")) {
return;
}
try {
totalLines = Integer.parseInt(rawValue);
words = Integer.parseInt(rawValue);
} catch (Exception e) {
Logger.w(LOG_TAG, "Invalid 'words' property of: " + name + ". Expecting an integer, got: '" + rawValue + "'.");
totalLines = 0;
Logger.w(LOG_TAG, "Invalid 'words' property of: " + path + ". Expecting an integer, got: '" + rawValue + "'.");
words = 0;
}
}
@ -184,14 +227,14 @@ public class WordFile {
try {
size = Long.parseLong(rawValue);
} catch (Exception e) {
Logger.w(LOG_TAG, "Invalid 'size' property of: " + name + ". Expecting an integer, got: '" + rawValue + "'.");
Logger.w(LOG_TAG, "Invalid 'size' property of: " + path + ". Expecting an integer, got: '" + rawValue + "'.");
size = 0;
}
}
private void loadProperties() {
String propertyFilename = name + ".props.yml";
String propertyFilename = path.replaceFirst("\\.\\w+$", "") + ".props.yml";
try (BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(propertyFilename)))) {
for (String line; (line = reader.readLine()) != null; ) {
@ -202,11 +245,102 @@ public class WordFile {
setDownloadUrl(parts[0], parts[1]);
setHash(parts[0], parts[1]);
setTotalLines(parts[0], parts[1]);
setWords(parts[0], parts[1]);
setSequences(parts[0], parts[1]);
setSize(parts[0], parts[1]);
}
} catch (Exception e) {
Logger.w(LOG_TAG, "Could not read the property file: " + propertyFilename + ". " + e.getMessage());
}
}
public String getNextSequence() throws IOException {
if (reader == null || !notEOF()) {
return "";
}
StringBuilder sequence = new StringBuilder();
// use the last char from getNextWords() if it's a digit
if (Character.isDigit(lastCharCode)) {
sequence.append((char) lastCharCode);
}
while ((lastCharCode = reader.read()) != -1) {
if (Character.isDigit(lastCharCode)) {
sequence.append((char) lastCharCode);
} else {
break;
}
}
if (sequence.length() == 0) {
throw new IOException("Could not find next sequence. Unexpected end of file.");
}
return sequence.toString();
}
public ArrayList<String> getNextWords(String digitSequence) throws IOException {
ArrayList<String> words = new ArrayList<>();
if (reader == null || !notEOF()) {
return words;
}
boolean areWordsSeparated = false;
StringBuilder word = new StringBuilder();
// If the word string starts with a space, it means there are words longer than the sequence.
// We must make sure to extract them correctly.
if (lastCharCode == ' ') {
areWordsSeparated = true;
}
// use the last char from getNextSequence() if it's a letter
else if (!Character.isDigit(lastCharCode)) {
word.append((char) lastCharCode);
}
int sequenceLength = digitSequence.length();
// start extracting the words
int wordLength = word.length();
while ((lastCharCode = reader.read()) != -1) {
if (Character.isDigit(lastCharCode)) {
break;
}
if (lastCharCode == ' ') {
areWordsSeparated = true;
} else {
word.append((char) lastCharCode);
wordLength++;
}
if ((areWordsSeparated && lastCharCode == ' ' && wordLength > 0) || (!areWordsSeparated && wordLength == sequenceLength)) {
words.add(word.toString());
wordLength = 0;
word.setLength(wordLength);
}
}
if ((areWordsSeparated && wordLength > 0) || (!areWordsSeparated && wordLength == sequenceLength)) {
words.add(word.toString());
} else if (wordLength > 0) {
throw new IOException("Unexpected end of file. Word: '" + word + "' length (" + wordLength + ") differs from the length of sequence: " + digitSequence);
}
if (words.isEmpty()) {
throw new IOException("Could not find any words for sequence: " + digitSequence);
}
return words;
}
public boolean notEOF() {
return lastCharCode != -1;
}
}

View file

@ -7,10 +7,11 @@ public class WordPosition {
public int start;
public int end;
public static WordPosition create(@NonNull String sequence, int start) {
public static WordPosition create(@NonNull String sequence, int start, int end) {
WordPosition position = new WordPosition();
position.sequence = sequence;
position.start = start;
position.end = end;
return position;
}

View file

@ -1,12 +1,10 @@
package io.github.sspanak.tt9.db.exceptions;
public class DictionaryImportException extends Exception {
public final String word;
public final long line;
public DictionaryImportException(String word, long line) {
super("Dictionary import failed");
this.word = word;
public DictionaryImportException(String message, long line) {
super(message);
this.line = line;
}
}

View file

@ -7,6 +7,7 @@ import android.os.Bundle;
import android.os.Handler;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
@ -79,32 +80,32 @@ public class DictionaryLoader {
return true;
}
loadThread = new Thread() {
@Override
public void run() {
currentFile = 0;
Timer.start(IMPORT_TIMER);
sendStartMessage(languages.size());
// SQLite does not support parallel queries, so let's import them one by one
for (Language lang : languages) {
if (isInterrupted()) {
break;
}
importAll(context, lang);
currentFile++;
}
Timer.stop(IMPORT_TIMER);
}
};
loadThread = new Thread(() -> loadSync(context, languages));
loadThread.start();
return true;
}
private void loadSync(Context context, ArrayList<Language> languages) {
currentFile = 0;
Timer.start(IMPORT_TIMER);
sendStartMessage(languages.size());
// SQLite does not support parallel queries, so let's import them one by one
for (Language lang : languages) {
if (loadThread.isInterrupted()) {
break;
}
importAll(context, lang);
currentFile++;
}
Timer.stop(IMPORT_TIMER);
}
public static void load(Context context, Language language) {
DictionaryLoadingBar progressBar = DictionaryLoadingBar.getInstance(context);
getInstance(context).setOnStatusChange(status -> progressBar.show(context, status));
@ -210,13 +211,11 @@ public class DictionaryLoader {
} catch (DictionaryImportException e) {
stop();
sqlite.failTransaction();
sendImportError(DictionaryImportException.class.getSimpleName(), language.getId(), e.line, e.word);
sendImportError(DictionaryImportException.class.getSimpleName(), language.getId(), e.line);
Logger.e(
LOG_TAG,
" Invalid word: '" + e.word
+ "' in dictionary: '" + language.getDictionaryFile() + "'"
+ " on line " + e.line
" Invalid word in dictionary: '" + language.getDictionaryFile() + "'"
+ " of language '" + language.getName() + "'. "
+ e.getMessage()
);
@ -256,34 +255,32 @@ public class DictionaryLoader {
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();
WordBatch batch = new WordBatch(language, SettingsStore.DICTIONARY_IMPORT_BATCH_SIZE + 1);
float progressRatio = (maxProgress - minProgress) / wordFile.getWords();
int wordCount = 0;
try (BufferedReader br = wordFile.getReader()) {
for (String line; (line = br.readLine()) != null; currentLine++) {
try (BufferedReader ignored = wordFile.getReader()) {
while (wordFile.notEOF()) {
if (loadThread.isInterrupted()) {
sendProgressMessage(language, 0, 0);
throw new DictionaryImportAbortedException();
}
String[] parts = WordFile.splitLine(line);
String word = parts[0];
short frequency = WordFile.getFrequencyFromLineParts(parts);
try {
boolean isFinalized = batch.add(word, frequency, currentLine + positionShift);
if (isFinalized && batch.getWords().size() > SettingsStore.DICTIONARY_IMPORT_BATCH_SIZE) {
String digitSequence = wordFile.getNextSequence();
ArrayList<String> words = wordFile.getNextWords(digitSequence);
batch.add(words, digitSequence, wordCount + positionShift);
wordCount += words.size();
if (batch.getWords().size() > SettingsStore.DICTIONARY_IMPORT_BATCH_SIZE) {
saveWordBatch(batch);
batch.clear();
}
} catch (InvalidLanguageCharactersException e) {
throw new DictionaryImportException(word, currentLine);
} catch (IOException e) {
throw new DictionaryImportException(e.getMessage(), wordCount);
}
if (wordFile.getTotalLines() > 0) {
sendProgressMessage(language, minProgress + progressRatio * currentLine, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
}
sendProgressMessage(language, minProgress + progressRatio * wordCount, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
}
}
@ -353,7 +350,7 @@ public class DictionaryLoader {
}
private void sendImportError(String message, int langId, long fileLine, String word) {
private void sendImportError(String message, int langId, long fileLine) {
if (onStatusChange == null) {
Logger.w(LOG_TAG, "Cannot send an import error without a status Handler. Ignoring message.");
return;
@ -363,14 +360,13 @@ public class DictionaryLoader {
errorMsg.putString("error", message);
errorMsg.putLong("fileLine", fileLine + 1);
errorMsg.putInt("languageId", langId);
errorMsg.putString("word", word);
asyncHandler.post(() -> onStatusChange.accept(errorMsg));
}
private void logLoadingStep(String message, Language language, long time) {
if (Logger.isDebugLevel()) {
Logger.d(LOG_TAG, message + " for language '" + language.getName() + "' in: " + time + " ms");
Logger.d(LOG_TAG, message + " for language '" + language.getName() + "' (" + language.getId() + ") in: " + time + " ms");
}
}
}

View file

@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import io.github.sspanak.tt9.BuildConfig;
import io.github.sspanak.tt9.util.Logger;
public class LanguageDefinition {
@ -93,6 +94,9 @@ public class LanguageDefinition {
definition.locale = getPropertyFromYaml(yaml, "locale", definition.locale);
definition.name = getPropertyFromYaml(yaml, "name", definition.name);
if (definition.dictionaryFile != null) {
definition.dictionaryFile = definition.dictionaryFile.replaceFirst("\\.\\w+$", "." + BuildConfig.DICTIONARY_EXTENSION);
}
return definition;
}

View file

@ -55,7 +55,7 @@ public class PreferenceSwitchLanguage extends SwitchPreferenceCompat {
summary
.append(", ")
.append(
wordFile.getFormattedTotalLines(activity.getString(R.string.language_selection_words))
wordFile.getFormattedWords(activity.getString(R.string.language_selection_words))
);
// download size

View file

@ -75,8 +75,7 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
context,
error,
data.getInt("languageId", -1),
data.getLong("fileLine", -1),
data.getString("word", "")
data.getLong("fileLine", -1)
);
} else if (progress >= 0) {
hasFailed = false;
@ -133,13 +132,13 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
}
private void showError(Context context, String errorType, int langId, long line, String word) {
private void showError(Context context, String errorType, int langId, long line) {
Language lang = LanguageCollection.getLanguage(context, langId);
if (lang == null || errorType.equals(InvalidLanguageException.class.getSimpleName())) {
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());
message = resources.getString(R.string.dictionary_load_bad_char, 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())) {

View file

@ -31,7 +31,7 @@
<string name="pref_status_icon">Икона за състояние</string>
<string name="pref_status_icon_summary">Показвай икона, когато въвеждането с клавиатура е активно.</string>
<string name="dictionary_cancel_load">Отмени зареждането</string>
<string name="dictionary_load_bad_char">Неуспешно зареждане. Невалидна дума „%1$s“ на ред %2$d за език „%3$s“.</string>
<string name="dictionary_load_bad_char">Неуспешно зареждане. Невалидна дума на ред %1$d за език „%2$s“.</string>
<string name="dictionary_load_error">Несупешно зареждане на речник за език „%1$s“ (%2$s).</string>
<string name="dictionary_loaded">Зареждането на речник приключи.</string>
<string name="dictionary_loading">Зареждане на речник (%1$s)…</string>

View file

@ -47,7 +47,7 @@
<string name="pref_upside_down_keys">Die Reihenfolge der Tasten umkehren</string>
<string name="pref_upside_down_keys_summary">Aktivieren Sie, wenn die Tastatur in der ersten Zeile 789 anstelle von 123 hat.</string>
<string name="dictionary_cancel_load">Laden abbrechen</string>
<string name="dictionary_load_bad_char">Laden fehlgeschlagen. Ungültiges Wort „%1$s“ in Zeile %2$d der Sprache „%3$s“.</string>
<string name="dictionary_load_bad_char">Laden fehlgeschlagen. Ungültiges Wort in Zeile %1$d der Sprache „%2$s“.</string>
<string name="dictionary_load_error">Fehler beim Laden des Wörterbuchs für die Sprache „%1$s“ (%2$s).</string>
<string name="dictionary_load_no_internet">Fehler beim Herunterladen des Wörterbuchs für die Sprache „%1$s“. Überprüfen Sie die Internetverbindung.</string>
<string name="dictionary_load_cancelled">Laden abgebrochen.</string>

View file

@ -68,7 +68,7 @@
<string name="pref_auto_space">Espacio automático</string>
<string name="pref_auto_space_summary">Insertar un espacio automático después de palabras y signos de puntuación.</string>
<string name="pref_double_zero_char">Carácter cuando se presiona \"0\" dos veces</string>
<string name="dictionary_load_bad_char">Error al cargar. Palabra inválida \"%1$s\" en la línea %2$d del idioma \"%3$s\".</string>
<string name="dictionary_load_bad_char">Error al cargar. Palabra inválida en la línea %1$d del idioma \"%2$s\".</string>
<string name="dictionary_load_error">Error al cargar el diccionario para el idioma \"%1$s\" (%2$s).</string>
<string name="dictionary_load_no_internet">Error al descargar el diccionario para el idioma \"%1$s\". Verifique la conexión a Internet.</string>
<string name="dictionary_load_cancelled">Carga del diccionario cancelada.</string>

View file

@ -44,7 +44,7 @@
<string name="pref_category_delete_words">Supprimer des mots ajoutés</string>
<string name="pref_category_hacks">Compatibilité</string>
<string name="pref_category_appearance">Apparance</string>
<string name="dictionary_load_bad_char">Echec du chargement. Mot inadmissible «%1$s» à la ligne %2$d de langue «%3$s».</string>
<string name="dictionary_load_bad_char">Echec du chargement. Mot inadmissible à la ligne %1$d de langue «%2$s».</string>
<string name="dictionary_truncated">Le dictionaire est supprimé avec succès.</string>
<string name="pref_hack_fb_messenger">Envoyer avec «OK» dans Facebook Messenger</string>
<string name="pref_hack_always_on_top">Toujours au premier plan</string>

View file

@ -61,7 +61,7 @@
<string name="pref_category_predictive_mode">Scrittura facilitata</string>
<string name="pref_category_function_keys">Scorciatoie da tastiera</string>
<string name="dictionary_loading_indeterminate">Caricamento del dizionario</string>
<string name="dictionary_load_bad_char">Caricamento non riuscito. Parola non valida \"%1$s\" alla riga %2$d della lingua \"%3$s\".</string>
<string name="dictionary_load_bad_char">Caricamento non riuscito. Parola non valida alla riga %1$d della lingua \"%2$s\".</string>
<string name="dictionary_load_error">Caricamento del dizionario per la lingua \"%1$s\" non riuscito (%2$s).</string>
<string name="dictionary_load_no_internet">"Download del dizionario per la lingua \"%1$s\" non riuscito. Controlla la connessione Internet. "</string>
<string name="dictionary_load_cancelled">Caricamento annullato.</string>

View file

@ -60,7 +60,7 @@
<string name="pref_upside_down_keys">להפוך את סדר המקשים</string>
<string name="pref_upside_down_keys_summary">הפעל את ההגדרה אם המקלדת כוללת את המספרים 7-8-9 בשורה הראשונה, במקום 1-2-3.</string>
<string name="dictionary_cancel_load">ביטול טעינה</string>
<string name="dictionary_load_bad_char">הטעינה נכשלה. מילה לא חוקית \"%1$s\" בשורה %2$d עבור \"%3$s\".</string>
<string name="dictionary_load_bad_char">הטעינה נכשלה. מילה לא חוקית בשורה %1$d עבור \"%2$s\".</string>
<string name="dictionary_load_error">נכשלה טעינת המילון עבור \"%1$s\" (%2$s).</string>
<string name="dictionary_load_no_internet">נכשל בהורדת המילון עבור השפה \"%1$s\". בדוק את חיבור האינטרנט.</string>
<string name="dictionary_load_cancelled">טעינת המילון בוטלה</string>

View file

@ -64,7 +64,7 @@
<string name="pref_status_icon">Būsenos piktograma</string>
<string name="pref_status_icon_summary">Rodyti piktogramą, kai aktyvus klaviatūros įvedimas</string>
<string name="dictionary_cancel_load">Atšaukti įkėlimą</string>
<string name="dictionary_load_bad_char">Įkelti \"%3$s\" kalbos nepavyko. Klaida %2$d eilutėje, netinkamas žodis - \"%1$s\".</string>
<string name="dictionary_load_bad_char">Įkelti \"%2$s\" kalbos nepavyko. Klaida %1$d eilutėje, netinkamas žodis.</string>
<string name="dictionary_load_error">Klaida įkeliant \"%1$s\" kalbos žodyną (%2$s).</string>
<string name="dictionary_load_no_internet">Nepavyko atsisiųsti žodyno kalbai „%1$s“. Patikrinkite interneto ryšį.</string>
<string name="dictionary_load_cancelled">Žodyno įkėlimas atšauktas.</string>

View file

@ -46,7 +46,7 @@
<string name="pref_upside_down_keys">De volgorde van de toetsen omkeren</string>
<string name="pref_upside_down_keys_summary">Activeer als het toetsenbord 789 op de eerste rij heeft, in plaats van 123.</string>
<string name="dictionary_cancel_load">Laden annuleren</string>
<string name="dictionary_load_bad_char">Laden mislukt. Ongeldig woord \"%1$s\" op regel %2$d van taal \"%3$s\".</string>
<string name="dictionary_load_bad_char">Laden mislukt. Ongeldig woord op regel %1$d van taal \"%2$s\".</string>
<string name="dictionary_load_error">Het laden van het woordenboek voor de taal \"%1$s\" is mislukt (%2$s).</string>
<string name="dictionary_load_no_internet">Het downloaden van het woordenboek voor de taal \"%1$s\" is mislukt. Controleer de internetverbinding.</string>
<string name="dictionary_load_cancelled">Laden geannuleerd.</string>

View file

@ -59,7 +59,7 @@
<string name="pref_status_icon">Ícone de status</string>
<string name="pref_status_icon_summary">Mostrar um ícone quando a digitação estiver ativa.</string>
<string name="dictionary_cancel_load">Cancelar Carregamento</string>
<string name="dictionary_load_bad_char">Falha no carregamento. \"%1$s\" na linha %2$d do idioma \"%3$s\".</string>
<string name="dictionary_load_bad_char">Falha no carregamento. Palavra inválida na linha %1$d do idioma \"%2$s\".</string>
<string name="dictionary_load_error">Falha no carregamento do dicionário para o idioma \"%1$s\" (%2$s).</string>
<string name="dictionary_load_no_internet">Falha ao baixar o dicionário para o idioma \"%1$s\". Verifique a conexão com a Internet.</string>
<string name="dictionary_load_cancelled">Carregamento de dicionário cancelado.</string>

View file

@ -67,7 +67,7 @@
<string name="pref_auto_text_case_summary">Автоматически начинать предложение с заглавной буквы.</string>
<string name="pref_double_zero_char">Символ при двойном нажатии клавиши 0</string>
<string name="pref_hack_fb_messenger">Отправка с «ОК» в Messenger</string>
<string name="dictionary_load_bad_char">Не удалось загрузить словарь. Проблема в слове «%1$s» в строке %2$d для языка «%3$s».</string>
<string name="dictionary_load_bad_char">Не удалось загрузить словарь. Проблема в слове в строке %1$d для языка «%2$s».</string>
<string name="function_backspace">Стереть</string>
<string name="dictionary_no_notifications">Уведомления словаря</string>
<string name="dictionary_no_notifications_summary">Получать уведомления о обновлениях словаря и о процессе загрузки.</string>

View file

@ -46,7 +46,7 @@
<string name="pref_status_icon">Durum</string>
<string name="pref_status_icon_summary">Klavye girişi etkin olduğunda bir simge göster.</string>
<string name="dictionary_cancel_load">Yüklemeyi İptal Et</string>
<string name="dictionary_load_bad_char">Yükleme başarısız. \"%1$s\" sözcüğü \"%3$s\" dilinin %2$d satırında geçersiz.</string>
<string name="dictionary_load_bad_char">Yükleme başarısız. \"%2$s\" dilinde %1$d. satırda geçersiz kelime.</string>
<string name="dictionary_load_error">“%1$s” dili için sözlük yüklenemedi (%2$s).</string>
<string name="dictionary_load_no_internet">“%1$s” dili için sözlük indirilemedi. İnternet bağlantısını kontrol edin.</string>
<string name="dictionary_load_cancelled">Yükleme iptal edildi.</string>

View file

@ -70,7 +70,7 @@
<string name="pref_status_icon">Іконка статусу</string>
<string name="pref_status_icon_summary">Показати іконку, коли активне введення з клавіатури.</string>
<string name="dictionary_cancel_load">Скасувати завантаження</string>
<string name="dictionary_load_bad_char">Завантаження не вдалося. Невірне слово \"%1$s\" у рядку %2$d мови \"%3$s\".</string>
<string name="dictionary_load_bad_char">Завантаження не вдалося. Невірне слово у рядку %1$d мови \"%2$s\".</string>
<string name="dictionary_load_error">Не вдалося завантажити словник для мови \"%1$s\" (%2$s).</string>
<string name="dictionary_load_no_internet">Не вдалося завантажити словник для мови \"%1$s\". Перевірте підключення до Інтернету.</string>
<string name="dictionary_load_cancelled">Завантаження словника скасовано.</string>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string translatable="false" name="dictionary_url">https://raw.githubusercontent.com/sspanak/tt9/%1$s/app/%2$s</string>
<string translatable="false" name="dictionary_url">https://raw.githubusercontent.com/sspanak/tt9/%1$s/downloads/%2$s</string>
<string name="app_name" translatable="false">Traditional T9</string>
<string name="app_name_short" translatable="false">TT9</string>
<string name="app_settings">TT9 Settings</string>
@ -90,7 +90,7 @@
<string name="pref_upside_down_keys_summary">Enable if the keypad has 789 on the first row, instead of 123.</string>
<string name="dictionary_cancel_load">Cancel Loading</string>
<string name="dictionary_load_bad_char">Loading failed. Invalid word \"%1$s\" on line %2$d of language \"%3$s\".</string>
<string name="dictionary_load_bad_char">Loading failed. Invalid word on line %1$d of language \"%2$s\".</string>
<string name="dictionary_load_error">Failed loading the dictionary for language \"%1$s\" (%2$s).</string>
<string name="dictionary_load_no_internet">Failed downloading the dictionary for language \"%1$s\". Check the Internet connection.</string>
<string name="dictionary_load_cancelled">Dictionary load cancelled.</string>