diff --git a/screen-translator.pro b/screen-translator.pro
index d38cc72..cee762b 100644
--- a/screen-translator.pro
+++ b/screen-translator.pro
@@ -8,7 +8,7 @@ DEPS_DIR=$$(ST_DEPS_DIR)
isEmpty(DEPS_DIR):DEPS_DIR=$$PWD/../deps
INCLUDEPATH += $$DEPS_DIR/include
LIBS += -L$$DEPS_DIR/lib
-LIBS += -ltesseract -lleptonica
+LIBS += -ltesseract -lleptonica -lhunspell
win32{
LIBS += -lUser32
@@ -39,6 +39,8 @@ HEADERS += \
src/capture/capturer.h \
src/commonmodels.h \
src/correct/corrector.h \
+ src/correct/correctorworker.h \
+ src/correct/hunspellcorrector.h \
src/languagecodes.h \
src/manager.h \
src/ocr/recognizer.h \
@@ -72,6 +74,8 @@ SOURCES += \
src/capture/capturer.cpp \
src/commonmodels.cpp \
src/correct/corrector.cpp \
+ src/correct/correctorworker.cpp \
+ src/correct/hunspellcorrector.cpp \
src/languagecodes.cpp \
src/main.cpp \
src/manager.cpp \
diff --git a/share/ci/get_hunspell.py b/share/ci/get_hunspell.py
new file mode 100644
index 0000000..b18b436
--- /dev/null
+++ b/share/ci/get_hunspell.py
@@ -0,0 +1,118 @@
+import common as c
+from config import bitness, msvc_version, build_dir, dependencies_dir
+import os
+import platform
+
+c.print('>> Installing hunspell')
+
+install_dir = dependencies_dir
+url = 'https://github.com/hunspell/hunspell/files/2573619/hunspell-1.7.0.tar.gz'
+required_version = '1.7.0'
+
+
+def check_existing():
+ if platform.system() == "Windows":
+ dll = install_dir + '/bin/hunspell.dll'
+ lib = install_dir + '/lib/hunspell.lib'
+ if not os.path.exists(dll) or not os.path.exists(lib):
+ return False
+ elif platform.system() == "Darwin":
+ lib = install_dir + '/lib/libhunspell.1.7.0.dylib'
+ if not os.path.exists(lib):
+ return False
+ c.symlink(lib, install_dir + '/lib/libhunspell.dylib')
+ else:
+ lib = install_dir + '/lib/libhunspell-1.7.so'
+ if not os.path.exists(lib):
+ return False
+ c.symlink(lib, install_dir + '/lib/libhunspell.so')
+
+ includes_path = install_dir + '/include/hunspell'
+ if len(c.get_folder_files(includes_path)) == 0:
+ return False
+
+ version_file = install_dir + '/lib/pkgconfig/hunspell.pc'
+ if not os.path.exists(version_file):
+ return False
+
+ with open(version_file, 'rt') as f:
+ lines = f.readlines()
+ for l in lines:
+ if not l.startswith('Version'):
+ continue
+ existing_version = l[9:14] # Version: 1.7.0
+ if existing_version != required_version:
+ return False
+ break
+ return True
+
+
+if check_existing():
+ c.print('>> Using cached')
+ exit(0)
+
+archive = os.path.basename(url)
+c.download(url, archive)
+
+src_dir = os.path.abspath('hunspell_src')
+c.extract(archive, '.')
+c.symlink(c.get_archive_top_dir(archive), src_dir)
+
+c.ensure_got_path(install_dir)
+
+c.recreate_dir(build_dir)
+os.chdir(build_dir)
+
+c.set_make_threaded()
+
+if platform.system() != "Windows":
+ c.run('autoreconf -i {}'.format(src_dir))
+ c.run('{}/configure --prefix={}'.format(src_dir, install_dir))
+ c.run('make')
+ c.run('make install')
+else:
+ lib_src = os.path.join(src_dir, 'src', 'hunspell')
+ sources = []
+ with os.scandir(lib_src) as it:
+ for f in it:
+ if not f.is_file() or not f.name.endswith('.cxx'):
+ continue
+ sources.append('${SRC_DIR}/' + f.name)
+
+ headers = ['${SRC_DIR}/atypes.hxx', '${SRC_DIR}/hunspell.h', '${SRC_DIR}/hunspell.hxx',
+ '${SRC_DIR}/hunvisapi.h', '${SRC_DIR}/w_char.hxx']
+
+ cmake_file = os.path.join(build_dir, 'CMakeLists.txt')
+ with open(cmake_file, 'w') as f:
+ f.write('project(hunspell)\n')
+ f.write('cmake_minimum_required(VERSION 3.11)\n')
+ f.write('set(SRC_DIR "{}")\n'.format(lib_src).replace('\\', '/'))
+ f.write('\n')
+ f.write('add_library(hunspell SHARED {})\n'.format(' '.join(sources)))
+ f.write('\n')
+ f.write('add_compile_definitions(HAVE_CONFIG_H _WIN32 BUILDING_LIBHUNSPELL)\n')
+ f.write('\n')
+ f.write('install(FILES {} \
+DESTINATION include/hunspell)\n'.format(' '.join(headers)))
+ f.write('\n')
+ f.write('install(TARGETS hunspell \
+RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib)\n')
+ f.write('\n')
+ f.write('set(prefix "${CMAKE_INSTALL_PREFIX}")\n')
+ f.write('set(VERSION "{}")\n'.format(required_version))
+ f.write('configure_file({}/hunspell.pc.in \
+${{CMAKE_CURRENT_BINARY_DIR}}/hunspell.pc @ONLY)\n'.format(src_dir.replace('\\', '/')))
+ f.write('install(FILES ${CMAKE_CURRENT_BINARY_DIR}/hunspell.pc \
+DESTINATION lib/pkgconfig)\n')
+
+ env_cmd = c.get_msvc_env_cmd(bitness=bitness, msvc_version=msvc_version)
+ c.apply_cmd_env(env_cmd)
+ cmake_args = '"{}" -DCMAKE_INSTALL_PREFIX="{}" {}'.format(
+ build_dir, install_dir, c.get_cmake_arch_args(bitness=bitness))
+ c.run('cmake {}'.format(cmake_args))
+ c.run('cmake --build . --config Release --verbose')
+ c.run('cmake --build . --target install --config Release')
+
+if not check_existing(): # create links
+ c.print('>> Build failed')
+ exit(1)
diff --git a/share/translations/screentranslator_ru.ts b/share/translations/screentranslator_ru.ts
index 73871f6..200e201 100644
--- a/share/translations/screentranslator_ru.ts
+++ b/share/translations/screentranslator_ru.ts
@@ -14,12 +14,17 @@
Сохранить (можно будет захватывать горячими клавишами)
-
+
+ Use auto corrections
+ Использовать автокоррекцию
+
+
+
Recognize:
Распознать:
-
+
⇵
⇵
@@ -50,6 +55,14 @@ Ctrl - продолжить выделять
Отменить
+
+ CorrectorWorker
+
+
+ Failed to init hunspell engine: %1
+ Ошибка инициализации hunspell: %1
+
+
QObject
@@ -589,38 +602,43 @@ Ctrl - продолжить выделять
+ hunspell
+ hunspell
+
+
+
translators
перевод
-
+
Screen translator started
Экранный переводчик запущен
-
+
Update completed
Обновление завершено
-
+
Updates available
Доступны обновления
-
+
Current version might be outdated.
Check for updates to silence this warning
Текущая версия может быть устаревшей.
Проверьте обновления, чтобы отключить это сообщение
-
+
Failed to set log file: %1
Ошибка установки лог-файла: %1
-
+
Started logging to file: %1
Начата запись в лог-файл: %1
@@ -630,7 +648,7 @@ Check for updates to silence this warning
<p>Version: %1</p>
<p>Author: Gres (<a href="mailto:%2">%2</a>)</p>
<p>Issues: <a href="%3">%3</a></p>
- <p>Инструмент оптического распознавания текста (OCR) и перевода</p>
+ <p>Инструмент оптического распознавания текста (OCR) и перевода</p>
<p>Версия: %1</p>
<p>Автор: Gres (<a href="mailto:%2">%2</a>)</p>
<p>Поддержка: <a href="%3">%3</a></p>
@@ -650,6 +668,20 @@ Check for updates to silence this warning
Failed to recognize text or no text selected
Ошибка распознавания текста или нет текста в выделенной зоне
+
+
+ Hunspell path not exists
+%1
+ Путь к словарям hunspell не существует
+%1
+
+
+
+ No .aff or .dic files for hunspell
+in %1
+ Нет .aff или .dic файлов для hunspell
+в %1
+
RecognizeWorker
@@ -790,17 +822,37 @@ Check for updates to silence this warning
сохранять пароль (небезопасно)
-
+
+ User substitutions
+ Пользовательская коррекция
+
+
+
+ Use auto corrections (hunspell)
+ Использовать автокоррекцию (hunspell)
+
+
+
+ Use user substitutions
+ Использовать пользовательскую коррекцию
+
+
+
+ Hunspell dictionaries path:
+ Путь к словарям Hunspell:
+
+
+
Language:
Язык:
-
+
secs
сек
-
+
Ignore SSL errors
Игнорировать ошибки SSL
@@ -840,102 +892,92 @@ Check for updates to silence this warning
Путь к языкам (tessdata):
-
- Substitutions
- Замены
-
-
-
- Substitute recognized text
- Заменять распознанный текст
-
-
-
+
Translators path:
Путь к переводчикам:
-
+
Translators
Переводчики
-
+
Result window
Окно результата
-
+
Font:
Шрифт:
-
+
Font size:
Размер шрифта:
-
+
Font color:
Цвет шрифта:
-
+
Background:
Фон:
-
+
Show image
Показывать изображение
-
+
Show recognized
Показывать распознанное
-
+
Update check interval (days):
Интервал проверки обновления (дней):
-
+
0 - disabled
- отключено
-
+
Apply updates
Применить изменения
-
+
Translate text
Переводить текст
-
+
Single translator timeout:
Переходить к следующему переводчику после:
-
+
Result type
Тип результата
-
+
Tray
Трей
-
+
Window
Окно
-
+
Check now
Проверить сейчас
@@ -1000,7 +1042,7 @@ Check for updates to silence this warning
Текст для проверки
-
+
Portable changed. Apply settings first
Portable режиме изменени. Сначала применить настройки
diff --git a/share/updates/hunspell.py b/share/updates/hunspell.py
new file mode 100644
index 0000000..5bae2bf
--- /dev/null
+++ b/share/updates/hunspell.py
@@ -0,0 +1,101 @@
+import sys
+import os
+import subprocess
+import re
+
+
+def parse_language_names():
+ root = os.path.abspath(os.path.basename(__file__) + '/../../..')
+ lines = []
+ with open(root + '/src/languagecodes.cpp', 'r') as d:
+ lines = d.readlines()
+ result = {}
+ for line in lines:
+ if line.startswith('//'):
+ continue
+ all = re.findall(r'"(.*?)"', line)
+ if len(all) != 6:
+ continue
+ result[all[2]] = all[5]
+ return result
+
+
+if len(sys.argv) < 2:
+ print("Usage:", sys.argv[0], " []")
+ exit(1)
+
+dict_dir = sys.argv[1]
+
+download_url = "https://cgit.freedesktop.org/libreoffice/dictionaries/plain"
+if len(sys.argv) > 2:
+ download_url = sys.argv[2]
+
+language_names = parse_language_names()
+
+preferred = ['sr.aff', 'sv_FI.aff',
+ 'en_US.aff', 'de_DE_frami.aff', 'nb_NO.aff']
+
+files = {}
+it = os.scandir(dict_dir)
+for d in it:
+ if not d.is_dir():
+ continue
+
+ lang = d.name
+ if '_' in lang:
+ lang = lang[0:lang.index('_')]
+
+ affs = []
+ fit = os.scandir(os.path.join(dict_dir, d.name))
+ for f in fit:
+ if not f.is_file or not f.name.endswith('.aff'):
+ continue
+ affs.append(f.name)
+
+ aff = ''
+ if len(affs) == 0:
+ continue
+ if len(affs) == 1:
+ aff = affs[0]
+ else:
+ for p in preferred:
+ if p in affs:
+ aff = p
+ break
+
+ if len(aff) == 0:
+ print('no aff for', lang, affs)
+ continue
+
+ aff = os.path.join(d.name, aff)
+ dic = aff[:aff.rindex('.')] + '.dic'
+ if not os.path.exists(os.path.join(dict_dir, dic)):
+ print('no dic exists', dic)
+
+ files[lang] = [aff, dic]
+
+
+print(',"hunspell": {')
+comma = ''
+unknown_names = []
+for lang, file_names in files.items():
+ if not lang in language_names:
+ unknown_names.append(lang)
+ continue
+ lang_name = language_names[lang]
+ print(' {}"{}":{{"files":['.format(comma, lang_name))
+ comma = ', '
+ lang_comma = ''
+ for file_name in file_names:
+ git_cmd = ['git', 'log', '-1', '--pretty=format:%cI', file_name]
+ date = subprocess.run(git_cmd, cwd=dict_dir, universal_newlines=True,
+ stdout=subprocess.PIPE, check=True).stdout
+ size = os.path.getsize(os.path.join(dict_dir, file_name))
+ installed = lang + file_name[file_name.index('/'):]
+ print(' {}{{"url":"{}/{}", "path":"$hunspell$/{}", "date":"{}", "size":{}}}'.format(
+ lang_comma, download_url, file_name, installed, date, size))
+ lang_comma = ','
+ print(' ]}')
+print('}')
+
+print('unknown names', unknown_names)
diff --git a/src/capture/capturearea.cpp b/src/capture/capturearea.cpp
index 4cc158b..01a7fa3 100644
--- a/src/capture/capturearea.cpp
+++ b/src/capture/capturearea.cpp
@@ -5,6 +5,7 @@
CaptureArea::CaptureArea(const QRect &rect, const Settings &settings)
: rect_(rect)
, doTranslation_(settings.doTranslation)
+ , useHunspell_(settings.useHunspell)
, sourceLanguage_(settings.sourceLanguage)
, targetLanguage_(settings.targetLanguage)
, translators_(settings.translators)
@@ -18,6 +19,7 @@ TaskPtr CaptureArea::task(const QPixmap &pixmap) const
auto task = std::make_shared();
task->generation = generation_;
+ task->useHunspell = useHunspell_;
task->captured = pixmap.copy(rect_);
task->capturePoint = rect_.topLeft();
task->sourceLanguage = sourceLanguage_;
diff --git a/src/capture/capturearea.h b/src/capture/capturearea.h
index d303520..a1b5a21 100644
--- a/src/capture/capturearea.h
+++ b/src/capture/capturearea.h
@@ -28,6 +28,7 @@ private:
QRect rect_;
bool doTranslation_;
bool isLocked_{false};
+ bool useHunspell_{false};
LanguageId sourceLanguage_;
LanguageId targetLanguage_;
QStringList translators_;
diff --git a/src/capture/captureareaeditor.cpp b/src/capture/captureareaeditor.cpp
index 52f9cb9..b7af0fc 100644
--- a/src/capture/captureareaeditor.cpp
+++ b/src/capture/captureareaeditor.cpp
@@ -15,6 +15,7 @@ CaptureAreaEditor::CaptureAreaEditor(const CommonModels &models,
: QWidget(parent)
, doTranslation_(new QCheckBox(tr("Translate:"), this))
, isLocked_(new QCheckBox(tr("Save (can capture via hotkey)"), this))
+ , useHunspell_(new QCheckBox(tr("Use auto corrections"), this))
, sourceLanguage_(new QComboBox(this))
, targetLanguage_(new QComboBox(this))
{
@@ -31,6 +32,9 @@ CaptureAreaEditor::CaptureAreaEditor(const CommonModels &models,
layout->addWidget(doTranslation_, row, 0);
layout->addWidget(targetLanguage_, row, 1);
+ ++row;
+ layout->addWidget(useHunspell_, row, 0, 1, 2);
+
++row;
layout->addWidget(isLocked_, row, 0, 1, 2);
@@ -64,6 +68,7 @@ void CaptureAreaEditor::swapLanguages()
void CaptureAreaEditor::set(const CaptureArea &area)
{
isLocked_->setChecked(area.isLocked());
+ useHunspell_->setChecked(area.useHunspell_);
doTranslation_->setChecked(area.doTranslation_);
sourceLanguage_->setCurrentText(LanguageCodes::name(area.sourceLanguage_));
targetLanguage_->setCurrentText(LanguageCodes::name(area.targetLanguage_));
@@ -72,6 +77,7 @@ void CaptureAreaEditor::set(const CaptureArea &area)
void CaptureAreaEditor::apply(CaptureArea &area) const
{
area.isLocked_ = isLocked_->isChecked();
+ area.useHunspell_ = useHunspell_->isChecked();
area.doTranslation_ = doTranslation_->isChecked();
area.sourceLanguage_ =
LanguageCodes::idForName(sourceLanguage_->currentText());
diff --git a/src/capture/captureareaeditor.h b/src/capture/captureareaeditor.h
index 0159c8c..9127ac6 100644
--- a/src/capture/captureareaeditor.h
+++ b/src/capture/captureareaeditor.h
@@ -23,6 +23,7 @@ private:
QCheckBox* doTranslation_;
QCheckBox* isLocked_;
+ QCheckBox* useHunspell_;
QComboBox* sourceLanguage_;
QComboBox* targetLanguage_;
};
diff --git a/src/correct/corrector.cpp b/src/correct/corrector.cpp
index a8e853c..393071d 100644
--- a/src/correct/corrector.cpp
+++ b/src/correct/corrector.cpp
@@ -1,13 +1,37 @@
#include "corrector.h"
+#include "correctorworker.h"
#include "debug.h"
#include "manager.h"
#include "settings.h"
#include "task.h"
+#include
+
Corrector::Corrector(Manager &manager, const Settings &settings)
: manager_(manager)
, settings_(settings)
+ , workerThread_(new QThread(this))
{
+ auto worker = new CorrectorWorker;
+ connect(this, &Corrector::resetAuto, //
+ worker, &CorrectorWorker::reset);
+ connect(this, &Corrector::correctAuto, //
+ worker, &CorrectorWorker::handle);
+ connect(worker, &CorrectorWorker::finished, //
+ this, &Corrector::finishCorrection);
+ connect(workerThread_, &QThread::finished, //
+ worker, &QObject::deleteLater);
+
+ workerThread_->start();
+ worker->moveToThread(workerThread_);
+}
+
+Corrector::~Corrector()
+{
+ workerThread_->quit();
+ const auto timeoutMs = 2000;
+ if (!workerThread_->wait(timeoutMs))
+ workerThread_->terminate();
}
void Corrector::correct(const TaskPtr &task)
@@ -20,11 +44,26 @@ void Corrector::correct(const TaskPtr &task)
return;
}
+ task->corrected = task->recognized;
+
if (!settings_.userSubstitutions.empty())
task->corrected = substituteUser(task->recognized, task->sourceLanguage);
- else
- task->corrected = task->recognized;
+ if (task->useHunspell) {
+ emit correctAuto(task);
+ return;
+ }
+
+ finishCorrection(task);
+}
+
+void Corrector::updateSettings()
+{
+ emit resetAuto(settings_.hunspellDir);
+}
+
+void Corrector::finishCorrection(const TaskPtr &task)
+{
manager_.corrected(task);
}
diff --git a/src/correct/corrector.h b/src/correct/corrector.h
index 9f0c029..3b61e30 100644
--- a/src/correct/corrector.h
+++ b/src/correct/corrector.h
@@ -2,17 +2,28 @@
#include "stfwd.h"
-class Corrector
+#include
+
+class Corrector : public QObject
{
+ Q_OBJECT
public:
Corrector(Manager &manager, const Settings &settings);
+ ~Corrector();
void correct(const TaskPtr &task);
+ void updateSettings();
+
+signals:
+ void correctAuto(const TaskPtr &task);
+ void resetAuto(const QString &tessdataPath);
private:
+ void finishCorrection(const TaskPtr &task);
QString substituteUser(const QString &source,
const LanguageId &language) const;
Manager &manager_;
const Settings &settings_;
+ QThread *workerThread_;
};
diff --git a/src/correct/correctorworker.cpp b/src/correct/correctorworker.cpp
new file mode 100644
index 0000000..c76d74b
--- /dev/null
+++ b/src/correct/correctorworker.cpp
@@ -0,0 +1,66 @@
+#include "correctorworker.h"
+#include "debug.h"
+#include "hunspellcorrector.h"
+#include "task.h"
+
+CorrectorWorker::CorrectorWorker() = default;
+
+CorrectorWorker::~CorrectorWorker() = default;
+
+void CorrectorWorker::handle(const TaskPtr &task)
+{
+ SOFT_ASSERT(task, return );
+ SOFT_ASSERT(task->isValid(), return );
+ SOFT_ASSERT(!hunspellDir_.isEmpty(), return );
+
+ auto result = task;
+
+ if (!bundles_.count(task->sourceLanguage)) {
+ auto engine =
+ std::make_unique(task->sourceLanguage, hunspellDir_);
+
+ if (!engine->isValid()) {
+ LWARNING()
+ << tr("Failed to init hunspell engine: %1").arg(engine->error());
+ emit finished(result);
+ return;
+ }
+
+ bundles_.emplace(task->sourceLanguage, Bundle{std::move(engine), 0});
+ }
+
+ auto &bundle = bundles_[task->sourceLanguage];
+ SOFT_ASSERT(bundle.hunspell->isValid(), return );
+
+ result->corrected = bundle.hunspell->correct(task->corrected);
+
+ const auto keepGenerations = 10;
+ bundle.usesLeft = keepGenerations;
+ removeUnused(task->generation);
+ lastGeneration_ = task->generation;
+
+ emit finished(result);
+}
+
+void CorrectorWorker::reset(const QString &hunspellDir)
+{
+ if (hunspellDir_ == hunspellDir)
+ return;
+
+ hunspellDir_ = hunspellDir;
+ bundles_.clear();
+}
+
+void CorrectorWorker::removeUnused(Generation current)
+{
+ if (lastGeneration_ == current)
+ return;
+
+ for (auto it = bundles_.begin(), end = bundles_.end(); it != end;) {
+ if (it->second.usesLeft >= 0) {
+ ++it;
+ } else {
+ it = bundles_.erase(it);
+ }
+ }
+}
diff --git a/src/correct/correctorworker.h b/src/correct/correctorworker.h
new file mode 100644
index 0000000..27a5871
--- /dev/null
+++ b/src/correct/correctorworker.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "stfwd.h"
+
+#include
+
+class HunspellCorrector;
+
+class CorrectorWorker : public QObject
+{
+ Q_OBJECT
+public:
+ CorrectorWorker();
+ ~CorrectorWorker();
+
+ void handle(const TaskPtr &task);
+ void reset(const QString &hunspellDir);
+
+signals:
+ void finished(const TaskPtr &task);
+
+private:
+ struct Bundle {
+ std::unique_ptr hunspell;
+ int usesLeft;
+ };
+ void removeUnused(Generation current);
+
+ std::map bundles_;
+ Generation lastGeneration_{};
+ QString hunspellDir_;
+};
diff --git a/src/correct/hunspellcorrector.cpp b/src/correct/hunspellcorrector.cpp
new file mode 100644
index 0000000..9626b5a
--- /dev/null
+++ b/src/correct/hunspellcorrector.cpp
@@ -0,0 +1,169 @@
+#include "hunspellcorrector.h"
+#include "debug.h"
+#include "languagecodes.h"
+#include "settings.h"
+
+#include
+
+#include
+#include
+#include
+
+static int levenshteinDistance(const QString &source, const QString &target)
+{
+ if (source == target)
+ return 0;
+
+ const auto sourceCount = source.size();
+ const auto targetCount = target.size();
+
+ if (sourceCount == 0)
+ return targetCount;
+
+ if (targetCount == 0)
+ return sourceCount;
+
+ if (sourceCount > targetCount)
+ return levenshteinDistance(target, source);
+
+ QVector previousColumn;
+ previousColumn.reserve(targetCount + 1);
+ for (auto i = 0; i < targetCount + 1; ++i) previousColumn.append(i);
+
+ QVector column(targetCount + 1, 0);
+ for (auto i = 0; i < sourceCount; ++i) {
+ column[0] = i + 1;
+ for (auto j = 0; j < targetCount; ++j) {
+ column[j + 1] = std::min(
+ {1 + column.at(j), 1 + previousColumn.at(1 + j),
+ previousColumn.at(j) + ((source.at(i) == target.at(j)) ? 0 : 1)});
+ }
+ column.swap(previousColumn);
+ }
+
+ return previousColumn.at(targetCount);
+}
+
+HunspellCorrector::HunspellCorrector(const LanguageId &language,
+ const QString &dictPath)
+{
+ const auto name = LanguageCodes::iso639_1(language);
+ init(dictPath + QLatin1Char('/') + name);
+}
+
+HunspellCorrector::~HunspellCorrector() = default;
+
+const QString &HunspellCorrector::error() const
+{
+ return error_;
+}
+
+bool HunspellCorrector::isValid() const
+{
+ return engine_.get();
+}
+
+void HunspellCorrector::init(const QString &path)
+{
+ SOFT_ASSERT(!engine_, return );
+
+ QDir dir(path);
+ if (!dir.exists()) {
+ error_ = QObject::tr("Hunspell path not exists\n%1").arg(path);
+ return;
+ }
+
+ QString aff;
+ QStringList dics;
+ for (const auto &file : dir.entryList(QDir::Filter::Files)) {
+ if (file.endsWith(".aff")) {
+ aff = dir.absoluteFilePath(file);
+ continue;
+ }
+ if (file.endsWith(".dic")) {
+ dics.append(dir.absoluteFilePath(file));
+ continue;
+ }
+ }
+
+ if (aff.isEmpty() || dics.isEmpty()) {
+ error_ = QObject::tr("No .aff or .dic files for hunspell\nin %1").arg(path);
+ return;
+ }
+
+ engine_ =
+ std::make_unique(qPrintable(aff), qPrintable(dics.first()));
+
+ dics.pop_front();
+ if (!dics.isEmpty()) {
+ for (const auto &dic : dics) engine_->add_dic(qPrintable(dic));
+ }
+}
+
+QString HunspellCorrector::correct(const QString &original)
+{
+ SOFT_ASSERT(engine_, return original);
+
+ const auto codec =
+ QTextCodec::codecForName(engine_->get_dict_encoding().c_str());
+ SOFT_ASSERT(codec, return original);
+
+ QString result;
+
+ QString word;
+ QString separator;
+ for (auto i = 0, end = original.size(); i < end; ++i) {
+ const auto ch = original[i];
+ if (ch.isPunct() || ch.isSpace()) {
+ if (!word.isEmpty()) {
+ correctWord(word, *codec);
+ result += word;
+ word.clear();
+ }
+ separator += ch;
+ continue;
+ }
+
+ if (!ch.isLetter() && word.isEmpty()) {
+ separator += ch;
+ continue;
+ }
+
+ if (!separator.isEmpty()) {
+ result += separator;
+ separator.clear();
+ }
+ word += ch;
+ }
+
+ if (!word.isEmpty()) {
+ correctWord(word, *codec);
+ result += word;
+ }
+ result += separator;
+
+ return result;
+}
+
+void HunspellCorrector::correctWord(QString &word, QTextCodec &codec) const
+{
+ if (word.isEmpty())
+ return;
+
+ const auto stdWord = codec.fromUnicode(word).toStdString();
+ if (engine_->spell(stdWord))
+ return;
+
+ const auto suggestions = engine_->suggest(stdWord);
+ if (suggestions.empty())
+ return;
+
+ const auto suggestion = QByteArray::fromStdString(suggestions.front());
+ const auto distance = levenshteinDistance(word, suggestion);
+ const auto maxDistance = std::max(int(word.size() * 0.2), 1);
+ LTRACE() << "hunspell" << word << suggestion << "distances" << distance
+ << maxDistance;
+
+ if (distance <= maxDistance)
+ word = suggestion;
+}
diff --git a/src/correct/hunspellcorrector.h b/src/correct/hunspellcorrector.h
new file mode 100644
index 0000000..58c728c
--- /dev/null
+++ b/src/correct/hunspellcorrector.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "stfwd.h"
+
+#include
+
+class Hunspell;
+
+class HunspellCorrector
+{
+public:
+ explicit HunspellCorrector(const LanguageId& language,
+ const QString& dictPath);
+ ~HunspellCorrector();
+
+ const QString& error() const;
+ bool isValid() const;
+ QString correct(const QString& original);
+
+private:
+ void init(const QString& path);
+ void correctWord(QString& word, QTextCodec& codec) const;
+
+ std::unique_ptr engine_;
+ QString error_;
+};
diff --git a/src/manager.cpp b/src/manager.cpp
index d0e7f17..4fcd0cb 100644
--- a/src/manager.cpp
+++ b/src/manager.cpp
@@ -41,6 +41,7 @@ Manager::Manager()
// updater components
(void)QT_TRANSLATE_NOOP("QObject", "app");
(void)QT_TRANSLATE_NOOP("QObject", "recognizers");
+ (void)QT_TRANSLATE_NOOP("QObject", "hunspell");
(void)QT_TRANSLATE_NOOP("QObject", "translators");
tray_ = std::make_unique(*this, *settings_);
@@ -111,6 +112,7 @@ void Manager::updateSettings()
tray_->updateSettings();
capturer_->updateSettings();
recognizer_->updateSettings();
+ corrector_->updateSettings();
translator_->updateSettings();
representer_->updateSettings();
@@ -147,6 +149,7 @@ void Manager::setupUpdates(const Settings &settings)
updater_->model()->setExpansions({
{"$translators$", settings.translatorsDir},
{"$tessdata$", settings.tessdataPath},
+ {"$hunspell$", settings.hunspellDir},
});
SOFT_ASSERT(updateAutoChecker_, return );
diff --git a/src/settings.cpp b/src/settings.cpp
index e5fd0e8..1669e53 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -34,6 +34,7 @@ const QString qs_ocrLanguage = "language";
const QString qs_correctionGroup = "Correction";
const QString qs_userSubstitutions = "userSubstitutions";
const QString qs_useUserSubstitutions = "useUserSubstitutions";
+const QString qs_useHunspell = "useHunspell";
const QString qs_translationGroup = "Translation";
const QString qs_doTranslation = "doTranslation";
@@ -173,6 +174,7 @@ void Settings::save() const
settings.endGroup();
settings.beginGroup(qs_correctionGroup);
+ settings.setValue(qs_useHunspell, useHunspell);
settings.setValue(qs_useUserSubstitutions, useUserSubstitutions);
settings.setValue(qs_userSubstitutions, packSubstitutions(userSubstitutions));
settings.endGroup();
@@ -258,6 +260,7 @@ void Settings::load()
settings.endGroup();
settings.beginGroup(qs_correctionGroup);
+ useHunspell = settings.value(qs_useHunspell, useHunspell).toBool();
useUserSubstitutions =
settings.value(qs_useUserSubstitutions, useUserSubstitutions).toBool();
userSubstitutions =
@@ -328,4 +331,5 @@ void Settings::setPortable(bool isPortable)
: QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
tessdataPath = baseDataPath + "/tessdata";
translatorsDir = baseDataPath + "/translators";
+ hunspellDir = baseDataPath + "/hunspell";
}
diff --git a/src/settings.h b/src/settings.h
index df4ef91..4a65906 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -48,6 +48,8 @@ public:
int autoUpdateIntervalDays{0};
QDateTime lastUpdateCheck;
+ bool useHunspell{false};
+ QString hunspellDir;
Substitutions userSubstitutions;
bool useUserSubstitutions{true};
diff --git a/src/settingseditor.cpp b/src/settingseditor.cpp
index ce52f6f..e4d5246 100644
--- a/src/settingseditor.cpp
+++ b/src/settingseditor.cpp
@@ -145,6 +145,7 @@ Settings SettingsEditor::settings() const
settings.sourceLanguage =
LanguageCodes::idForName(ui->tesseractLangCombo->currentText());
+ settings.useHunspell = ui->useHunspell->isChecked();
settings.useUserSubstitutions = ui->useUserSubstitutions->isChecked();
settings.userSubstitutions = ui->userSubstitutionsTable->substitutions();
@@ -207,6 +208,8 @@ void SettingsEditor::setSettings(const Settings &settings)
ui->tesseractLangCombo->setCurrentText(
LanguageCodes::name(settings.sourceLanguage));
+ ui->useHunspell->setChecked(settings.useHunspell);
+ ui->hunspellDir->setText(settings.hunspellDir);
ui->useUserSubstitutions->setChecked(settings.useUserSubstitutions);
ui->userSubstitutionsTable->setSubstitutions(settings.userSubstitutions);
@@ -308,6 +311,7 @@ void SettingsEditor::handlePortableChanged()
settings.setPortable(ui->portable->isChecked());
ui->tessdataPath->setText(settings.tessdataPath);
ui->translatorsPath->setText(settings.translatorsDir);
+ ui->hunspellDir->setText(settings.hunspellDir);
updateModels(settings.tessdataPath);
updateTranslators();
diff --git a/src/settingseditor.ui b/src/settingseditor.ui
index ac34488..f1f1679 100644
--- a/src/settingseditor.ui
+++ b/src/settingseditor.ui
@@ -286,24 +286,17 @@
- -
+
-
- Substitutions
+ User substitutions
Qt::AlignCenter
- -
-
-
- Substitute recognized text
-
-
-
- -
+
-
QAbstractItemView::SelectRows
@@ -313,6 +306,34 @@
+ -
+
+
+ Use auto corrections (hunspell)
+
+
+
+ -
+
+
+ Use user substitutions
+
+
+
+ -
+
+
+ Hunspell dictionaries path:
+
+
+
+ -
+
+
+
+
+
+
diff --git a/src/task.h b/src/task.h
index 341201b..aeafeaf 100644
--- a/src/task.h
+++ b/src/task.h
@@ -19,6 +19,8 @@ public:
QString corrected;
QString translated;
+ bool useHunspell{false};
+
LanguageId sourceLanguage;
LanguageId targetLanguage;
diff --git a/updates.json b/updates.json
index 9c5db6f..c00f167 100644
--- a/updates.json
+++ b/updates.json
@@ -382,6 +382,196 @@
]}
}
+
+,"hunspell": {
+ "Thai":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/th_TH/th_TH.aff", "path":"$hunspell$/th/th_TH.aff", "date":"2019-04-30T09:35:45+02:00", "size":156}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/th_TH/th_TH.dic", "path":"$hunspell$/th/th_TH.dic", "date":"2019-06-04T14:18:16+02:00", "size":1251425}
+ ]}
+ , "Gujarati":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/gu_IN/gu_IN.aff", "path":"$hunspell$/gu/gu_IN.aff", "date":"2012-10-16T11:09:27-05:00", "size":174}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/gu_IN/gu_IN.dic", "path":"$hunspell$/gu/gu_IN.dic", "date":"2012-10-16T11:09:27-05:00", "size":3792870}
+ ]}
+ , "Afrikaans":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/af_ZA/af_ZA.aff", "path":"$hunspell$/af/af_ZA.aff", "date":"2020-02-16T20:22:16+01:00", "size":5027}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/af_ZA/af_ZA.dic", "path":"$hunspell$/af/af_ZA.dic", "date":"2020-02-16T20:22:16+01:00", "size":1262203}
+ ]}
+ , "Norwegian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/no/nb_NO.aff", "path":"$hunspell$/no/nb_NO.aff", "date":"2013-05-23T11:54:36+01:00", "size":17259}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/no/nb_NO.dic", "path":"$hunspell$/no/nb_NO.dic", "date":"2018-09-05T10:30:32+02:00", "size":5274030}
+ ]}
+ , "Albanian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sq_AL/sq_AL.aff", "path":"$hunspell$/sq/sq_AL.aff", "date":"2017-10-23T23:57:24+02:00", "size":7555}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sq_AL/sq_AL.dic", "path":"$hunspell$/sq/sq_AL.dic", "date":"2017-10-23T23:57:24+02:00", "size":2605147}
+ ]}
+ , "Italian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/it_IT/it_IT.aff", "path":"$hunspell$/it/it_IT.aff", "date":"2012-10-16T11:09:27-05:00", "size":80216}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/it_IT/it_IT.dic", "path":"$hunspell$/it/it_IT.dic", "date":"2012-10-16T11:09:27-05:00", "size":1290681}
+ ]}
+ , "Nepali":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/ne_NP/ne_NP.aff", "path":"$hunspell$/ne/ne_NP.aff", "date":"2012-10-16T11:09:27-05:00", "size":14162}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/ne_NP/ne_NP.dic", "path":"$hunspell$/ne/ne_NP.dic", "date":"2012-10-16T11:09:27-05:00", "size":874372}
+ ]}
+ , "Danish":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/da_DK/da_DK.aff", "path":"$hunspell$/da/da_DK.aff", "date":"2019-07-14T00:13:36+02:00", "size":55779}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/da_DK/da_DK.dic", "path":"$hunspell$/da/da_DK.dic", "date":"2019-07-14T00:13:36+02:00", "size":2915525}
+ ]}
+ , "Ukrainian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/uk_UA/uk_UA.aff", "path":"$hunspell$/uk/uk_UA.aff", "date":"2012-10-16T11:09:27-05:00", "size":159599}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/uk_UA/uk_UA.dic", "path":"$hunspell$/uk/uk_UA.dic", "date":"2012-10-16T11:09:27-05:00", "size":2584267}
+ ]}
+ , "Gaelic":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/gd_GB/gd_GB.aff", "path":"$hunspell$/gd/gd_GB.aff", "date":"2017-06-22T00:27:25+02:00", "size":8228}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/gd_GB/gd_GB.dic", "path":"$hunspell$/gd/gd_GB.dic", "date":"2017-06-22T00:27:25+02:00", "size":4806682}
+ ]}
+ , "Estonian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/et_EE/et_EE.aff", "path":"$hunspell$/et/et_EE.aff", "date":"2012-10-16T11:09:27-05:00", "size":236336}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/et_EE/et_EE.dic", "path":"$hunspell$/et/et_EE.dic", "date":"2012-10-16T11:09:27-05:00", "size":4383841}
+ ]}
+ , "Greek":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/el_GR/el_GR.aff", "path":"$hunspell$/el/el_GR.aff", "date":"2015-09-21T17:56:43+02:00", "size":15647}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/el_GR/el_GR.dic", "path":"$hunspell$/el/el_GR.dic", "date":"2015-09-21T17:56:43+02:00", "size":10125390}
+ ]}
+ , "Bulgarian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/bg_BG/bg_BG.aff", "path":"$hunspell$/bg/bg_BG.aff", "date":"2018-06-29T12:25:29+02:00", "size":58189}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/bg_BG/bg_BG.dic", "path":"$hunspell$/bg/bg_BG.dic", "date":"2018-06-29T12:25:29+02:00", "size":1566331}
+ ]}
+ , "Croatian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/hr_HR/hr_HR.aff", "path":"$hunspell$/hr/hr_HR.aff", "date":"2018-05-29T22:11:06+02:00", "size":95802}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/hr_HR/hr_HR.dic", "path":"$hunspell$/hr/hr_HR.dic", "date":"2018-05-29T22:11:06+02:00", "size":731819}
+ ]}
+ , "Polish":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/pl_PL/pl_PL.aff", "path":"$hunspell$/pl/pl_PL.aff", "date":"2017-05-05T15:26:38+02:00", "size":246842}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/pl_PL/pl_PL.dic", "path":"$hunspell$/pl/pl_PL.dic", "date":"2017-05-21T10:58:59+02:00", "size":4539105}
+ ]}
+ , "Galician":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/gl/gl_ES.aff", "path":"$hunspell$/gl/gl_ES.aff", "date":"2018-12-28T04:42:44+01:00", "size":1159910}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/gl/gl_ES.dic", "path":"$hunspell$/gl/gl_ES.dic", "date":"2018-12-28T04:42:44+01:00", "size":8636406}
+ ]}
+ , "Bengali":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/bn_BD/bn_BD.aff", "path":"$hunspell$/bn/bn_BD.aff", "date":"2012-10-16T11:09:27-05:00", "size":195}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/bn_BD/bn_BD.dic", "path":"$hunspell$/bn/bn_BD.dic", "date":"2012-10-16T11:09:27-05:00", "size":2596038}
+ ]}
+ , "Dutch":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/nl_NL/nl_NL.aff", "path":"$hunspell$/nl/nl_NL.aff", "date":"2013-07-22T17:41:01+00:00", "size":27835}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/nl_NL/nl_NL.dic", "path":"$hunspell$/nl/nl_NL.dic", "date":"2013-07-22T17:41:01+00:00", "size":1881063}
+ ]}
+ , "Bosnian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/bs_BA/bs_BA.aff", "path":"$hunspell$/bs/bs_BA.aff", "date":"2013-01-22T17:32:09+01:00", "size":17468}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/bs_BA/bs_BA.dic", "path":"$hunspell$/bs/bs_BA.dic", "date":"2013-01-22T17:32:09+01:00", "size":339863}
+ ]}
+ , "Swahili":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sw_TZ/sw_TZ.aff", "path":"$hunspell$/sw/sw_TZ.aff", "date":"2012-10-16T11:09:27-05:00", "size":974}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sw_TZ/sw_TZ.dic", "path":"$hunspell$/sw/sw_TZ.dic", "date":"2012-10-16T11:09:27-05:00", "size":630844}
+ ]}
+ , "Slovenian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sl_SI/sl_SI.aff", "path":"$hunspell$/sl/sl_SI.aff", "date":"2012-10-16T11:09:27-05:00", "size":14730}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sl_SI/sl_SI.dic", "path":"$hunspell$/sl/sl_SI.dic", "date":"2012-10-16T11:09:27-05:00", "size":2967766}
+ ]}
+ , "French":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/fr_FR/fr.aff", "path":"$hunspell$/fr/fr.aff", "date":"2018-08-24T15:00:59+02:00", "size":256857}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/fr_FR/fr.dic", "path":"$hunspell$/fr/fr.dic", "date":"2018-08-24T15:00:59+02:00", "size":1100397}
+ ]}
+ , "Latvian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/lv_LV/lv_LV.aff", "path":"$hunspell$/lv/lv_LV.aff", "date":"2012-10-16T11:09:27-05:00", "size":69597}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/lv_LV/lv_LV.dic", "path":"$hunspell$/lv/lv_LV.dic", "date":"2012-10-16T11:09:27-05:00", "size":2226834}
+ ]}
+ , "German":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/de/de_DE_frami.aff", "path":"$hunspell$/de/de_DE_frami.aff", "date":"2017-01-22T19:03:05+00:00", "size":18991}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/de/de_DE_frami.dic", "path":"$hunspell$/de/de_DE_frami.dic", "date":"2017-01-22T19:03:05+00:00", "size":4356858}
+ ]}
+ , "Tibetan":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/bo/bo.aff", "path":"$hunspell$/bo/bo.aff", "date":"2016-11-22T22:23:34+00:00", "size":1706}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/bo/bo.dic", "path":"$hunspell$/bo/bo.dic", "date":"2017-10-23T18:37:13+02:00", "size":4637}
+ ]}
+ , "Telugu":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/te_IN/te_IN.aff", "path":"$hunspell$/te/te_IN.aff", "date":"2012-10-16T11:09:27-05:00", "size":160}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/te_IN/te_IN.dic", "path":"$hunspell$/te/te_IN.dic", "date":"2012-10-16T11:09:27-05:00", "size":3402272}
+ ]}
+ , "Czech":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/cs_CZ/cs_CZ.aff", "path":"$hunspell$/cs/cs_CZ.aff", "date":"2015-04-07T12:40:39+02:00", "size":97286}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/cs_CZ/cs_CZ.dic", "path":"$hunspell$/cs/cs_CZ.dic", "date":"2019-09-16T06:45:26+02:00", "size":2209232}
+ ]}
+ , "Slovak":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sk_SK/sk_SK.aff", "path":"$hunspell$/sk/sk_SK.aff", "date":"2013-05-09T10:20:05+02:00", "size":99414}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sk_SK/sk_SK.dic", "path":"$hunspell$/sk/sk_SK.dic", "date":"2013-05-09T10:20:05+02:00", "size":3289769}
+ ]}
+ , "Hebrew":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/he_IL/he_IL.aff", "path":"$hunspell$/he/he_IL.aff", "date":"2017-09-05T18:11:31+02:00", "size":78883}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/he_IL/he_IL.dic", "path":"$hunspell$/he/he_IL.dic", "date":"2017-09-05T18:11:31+02:00", "size":7796259}
+ ]}
+ , "Russian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/ru_RU/ru_RU.aff", "path":"$hunspell$/ru/ru_RU.aff", "date":"2012-10-16T11:09:27-05:00", "size":53019}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/ru_RU/ru_RU.dic", "path":"$hunspell$/ru/ru_RU.dic", "date":"2012-10-16T11:09:27-05:00", "size":1969349}
+ ]}
+ , "Arabic":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/ar/ar.aff", "path":"$hunspell$/ar/ar.aff", "date":"2018-02-04T21:34:12+01:00", "size":86949}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/ar/ar.dic", "path":"$hunspell$/ar/ar.dic", "date":"2019-03-07T11:32:58+01:00", "size":7217161}
+ ]}
+ , "Vietnamese":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/vi/vi_VN.aff", "path":"$hunspell$/vi/vi_VN.aff", "date":"2012-10-16T11:09:27-05:00", "size":788}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/vi/vi_VN.dic", "path":"$hunspell$/vi/vi_VN.dic", "date":"2012-10-16T11:09:27-05:00", "size":39852}
+ ]}
+ , "Portuguese":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/pt_PT/pt_PT.aff", "path":"$hunspell$/pt/pt_PT.aff", "date":"2016-07-07T10:15:29+02:00", "size":95089}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/pt_PT/pt_PT.dic", "path":"$hunspell$/pt/pt_PT.dic", "date":"2016-10-03T18:05:09+00:00", "size":1473077}
+ ]}
+ , "Turkish":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/tr_TR/tr_TR.aff", "path":"$hunspell$/tr/tr_TR.aff", "date":"2018-08-27T16:55:14+02:00", "size":235315}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/tr_TR/tr_TR.dic", "path":"$hunspell$/tr/tr_TR.dic", "date":"2018-08-27T16:55:14+02:00", "size":9061155}
+ ]}
+ , "Indonesian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/id/id_ID.aff", "path":"$hunspell$/id/id_ID.aff", "date":"2018-02-28T01:40:08+01:00", "size":14957}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/id/id_ID.dic", "path":"$hunspell$/id/id_ID.dic", "date":"2018-02-28T01:40:08+01:00", "size":315384}
+ ]}
+ , "Hindi":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/hi_IN/hi_IN.aff", "path":"$hunspell$/hi/hi_IN.aff", "date":"2012-10-16T11:09:27-05:00", "size":210}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/hi_IN/hi_IN.dic", "path":"$hunspell$/hi/hi_IN.dic", "date":"2012-10-16T11:09:27-05:00", "size":303963}
+ ]}
+ , "Lithuanian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/lt_LT/lt.aff", "path":"$hunspell$/lt/lt.aff", "date":"2013-01-23T11:35:37+00:00", "size":92208}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/lt_LT/lt.dic", "path":"$hunspell$/lt/lt.dic", "date":"2013-01-23T11:35:37+00:00", "size":1085291}
+ ]}
+ , "Spanish":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/es/es_ANY.aff", "path":"$hunspell$/es/es_ANY.aff", "date":"2019-10-22T21:26:40+02:00", "size":169377}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/es/es_ANY.dic", "path":"$hunspell$/es/es_ANY.dic", "date":"2019-10-22T21:26:40+02:00", "size":804058}
+ ]}
+ , "Icelandic":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/is/is.aff", "path":"$hunspell$/is/is.aff", "date":"2016-03-14T09:05:09+00:00", "size":309734}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/is/is.dic", "path":"$hunspell$/is/is.dic", "date":"2016-03-14T09:05:09+00:00", "size":2454138}
+ ]}
+ , "Lao":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/lo_LA/lo_LA.aff", "path":"$hunspell$/lo/lo_LA.aff", "date":"2013-11-24T19:21:08+01:00", "size":10}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/lo_LA/lo_LA.dic", "path":"$hunspell$/lo/lo_LA.dic", "date":"2013-11-24T19:21:08+01:00", "size":203209}
+ ]}
+ , "Romanian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/ro/ro_RO.aff", "path":"$hunspell$/ro/ro_RO.aff", "date":"2013-03-28T11:26:45+01:00", "size":55181}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/ro/ro_RO.dic", "path":"$hunspell$/ro/ro_RO.dic", "date":"2013-03-28T11:26:45+01:00", "size":2196348}
+ ]}
+ , "Serbian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sr/sr.aff", "path":"$hunspell$/sr/sr.aff", "date":"2019-04-20T11:24:57+02:00", "size":901060}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sr/sr.dic", "path":"$hunspell$/sr/sr.dic", "date":"2019-04-20T11:24:57+02:00", "size":5878745}
+ ]}
+ , "Belarusian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/be_BY/be_BY.aff", "path":"$hunspell$/be/be_BY.aff", "date":"2012-10-16T11:09:27-05:00", "size":23968}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/be_BY/be_BY.dic", "path":"$hunspell$/be/be_BY.dic", "date":"2012-10-16T11:09:27-05:00", "size":1707840}
+ ]}
+ , "English":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/en/en_US.aff", "path":"$hunspell$/en/en_US.aff", "date":"2018-05-15T00:49:14+02:00", "size":3090}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/en/en_US.dic", "path":"$hunspell$/en/en_US.dic", "date":"2019-11-13T00:44:49+01:00", "size":551260}
+ ]}
+ , "Hungarian":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/hu_HU/hu_HU.aff", "path":"$hunspell$/hu/hu_HU.aff", "date":"2018-05-22T22:26:58+02:00", "size":2106214}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/hu_HU/hu_HU.dic", "path":"$hunspell$/hu/hu_HU.dic", "date":"2018-05-22T22:26:58+02:00", "size":1653155}
+ ]}
+ , "Swedish":{"files":[
+ {"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sv_SE/sv_FI.aff", "path":"$hunspell$/sv/sv_FI.aff", "date":"2015-09-08T21:02:20+00:00", "size":18583}
+ ,{"url":"https://cgit.freedesktop.org/libreoffice/dictionaries/plain/sv_SE/sv_FI.dic", "path":"$hunspell$/sv/sv_FI.dic", "date":"2016-08-16T20:00:33+00:00", "size":2317112}
+ ]}
+}
+
+
+
,"translators":{
"bing": {"files":[
{"url":"https://raw.githubusercontent.com/OneMoreGres/ScreenTranslator/master/translators/bing.js", "path":"$translators$/bing.js", "md5":"fc69c1e05a3462a88131ee0a8422ad89"}