Add auto correction with hunspell

This commit is contained in:
Gres 2020-04-08 23:17:23 +03:00
parent 23cdc3e57c
commit 48a01a3992
21 changed files with 899 additions and 55 deletions

View File

@ -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 \

118
share/ci/get_hunspell.py Normal file
View File

@ -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)

View File

@ -14,12 +14,17 @@
<translation>Сохранить (можно будет захватывать горячими клавишами)</translation>
</message>
<message>
<location filename="../../src/capture/captureareaeditor.cpp" line="25"/>
<location filename="../../src/capture/captureareaeditor.cpp" line="18"/>
<source>Use auto corrections</source>
<translation>Использовать автокоррекцию</translation>
</message>
<message>
<location filename="../../src/capture/captureareaeditor.cpp" line="26"/>
<source>Recognize:</source>
<translation>Распознать:</translation>
</message>
<message>
<location filename="../../src/capture/captureareaeditor.cpp" line="27"/>
<location filename="../../src/capture/captureareaeditor.cpp" line="28"/>
<source></source>
<translation></translation>
</message>
@ -50,6 +55,14 @@ Ctrl - продолжить выделять</translation>
<translation>Отменить</translation>
</message>
</context>
<context>
<name>CorrectorWorker</name>
<message>
<location filename="../../src/correct/correctorworker.cpp" line="24"/>
<source>Failed to init hunspell engine: %1</source>
<translation>Ошибка инициализации hunspell: %1</translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
@ -589,38 +602,43 @@ Ctrl - продолжить выделять</translation>
</message>
<message>
<location filename="../../src/manager.cpp" line="44"/>
<source>hunspell</source>
<translation>hunspell</translation>
</message>
<message>
<location filename="../../src/manager.cpp" line="45"/>
<source>translators</source>
<translation>перевод</translation>
</message>
<message>
<location filename="../../src/manager.cpp" line="59"/>
<location filename="../../src/manager.cpp" line="60"/>
<source>Screen translator started</source>
<translation>Экранный переводчик запущен</translation>
</message>
<message>
<location filename="../../src/manager.cpp" line="67"/>
<location filename="../../src/manager.cpp" line="68"/>
<source>Update completed</source>
<translation>Обновление завершено</translation>
</message>
<message>
<location filename="../../src/manager.cpp" line="71"/>
<location filename="../../src/manager.cpp" line="72"/>
<source>Updates available</source>
<translation>Доступны обновления</translation>
</message>
<message>
<location filename="../../src/manager.cpp" line="97"/>
<location filename="../../src/manager.cpp" line="98"/>
<source>Current version might be outdated.
Check for updates to silence this warning</source>
<translation>Текущая версия может быть устаревшей.
Проверьте обновления, чтобы отключить это сообщение</translation>
</message>
<message>
<location filename="../../src/manager.cpp" line="181"/>
<location filename="../../src/manager.cpp" line="184"/>
<source>Failed to set log file: %1</source>
<translation>Ошибка установки лог-файла: %1</translation>
</message>
<message>
<location filename="../../src/manager.cpp" line="187"/>
<location filename="../../src/manager.cpp" line="190"/>
<source>Started logging to file: %1</source>
<translation>Начата запись в лог-файл: %1</translation>
</message>
@ -630,7 +648,7 @@ Check for updates to silence this warning</source>
&lt;p&gt;Version: %1&lt;/p&gt;
&lt;p&gt;Author: Gres (&lt;a href=&quot;mailto:%2&quot;&gt;%2&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Issues: &lt;a href=&quot;%3&quot;&gt;%3&lt;/a&gt;&lt;/p&gt;</source>
<translation type="unfinished">&lt;p&gt;Инструмент оптического распознавания текста (OCR) и перевода&lt;/p&gt;
<translation>&lt;p&gt;Инструмент оптического распознавания текста (OCR) и перевода&lt;/p&gt;
&lt;p&gt;Версия: %1&lt;/p&gt;
&lt;p&gt;Автор: Gres (&lt;a href=&quot;mailto:%2&quot;&gt;%2&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Поддержка: &lt;a href=&quot;%3&quot;&gt;%3&lt;/a&gt;&lt;/p&gt;</translation>
@ -650,6 +668,20 @@ Check for updates to silence this warning</source>
<source>Failed to recognize text or no text selected</source>
<translation>Ошибка распознавания текста или нет текста в выделенной зоне</translation>
</message>
<message>
<location filename="../../src/correct/hunspellcorrector.cpp" line="72"/>
<source>Hunspell path not exists
%1</source>
<translation>Путь к словарям hunspell не существует
%1</translation>
</message>
<message>
<location filename="../../src/correct/hunspellcorrector.cpp" line="90"/>
<source>No .aff or .dic files for hunspell
in %1</source>
<translation>Нет .aff или .dic файлов для hunspell
в %1</translation>
</message>
</context>
<context>
<name>RecognizeWorker</name>
@ -790,17 +822,37 @@ Check for updates to silence this warning</source>
<translation>сохранять пароль (небезопасно)</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="371"/>
<location filename="../../src/settingseditor.ui" line="292"/>
<source>User substitutions</source>
<translation>Пользовательская коррекция</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="312"/>
<source>Use auto corrections (hunspell)</source>
<translation>Использовать автокоррекцию (hunspell)</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="319"/>
<source>Use user substitutions</source>
<translation>Использовать пользовательскую коррекцию</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="326"/>
<source>Hunspell dictionaries path:</source>
<translation>Путь к словарям Hunspell:</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="392"/>
<source>Language:</source>
<translation>Язык:</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="354"/>
<location filename="../../src/settingseditor.ui" line="375"/>
<source> secs</source>
<translation> сек</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="337"/>
<location filename="../../src/settingseditor.ui" line="358"/>
<source>Ignore SSL errors</source>
<translation>Игнорировать ошибки SSL</translation>
</message>
@ -840,102 +892,92 @@ Check for updates to silence this warning</source>
<translation>Путь к языкам (tessdata):</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="292"/>
<source>Substitutions</source>
<translation>Замены</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="302"/>
<source>Substitute recognized text</source>
<translation>Заменять распознанный текст</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="323"/>
<location filename="../../src/settingseditor.ui" line="344"/>
<source>Translators path:</source>
<translation>Путь к переводчикам:</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="344"/>
<location filename="../../src/settingseditor.ui" line="365"/>
<source>Translators</source>
<translation>Переводчики</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="452"/>
<location filename="../../src/settingseditor.ui" line="473"/>
<source>Result window</source>
<translation>Окно результата</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="458"/>
<location filename="../../src/settingseditor.ui" line="479"/>
<source>Font:</source>
<translation>Шрифт:</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="468"/>
<location filename="../../src/settingseditor.ui" line="489"/>
<source>Font size:</source>
<translation>Размер шрифта:</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="485"/>
<location filename="../../src/settingseditor.ui" line="506"/>
<source>Font color:</source>
<translation>Цвет шрифта:</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="502"/>
<location filename="../../src/settingseditor.ui" line="523"/>
<source>Background:</source>
<translation>Фон:</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="519"/>
<location filename="../../src/settingseditor.ui" line="540"/>
<source>Show image</source>
<translation>Показывать изображение</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="526"/>
<location filename="../../src/settingseditor.ui" line="547"/>
<source>Show recognized</source>
<translation>Показывать распознанное</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="595"/>
<location filename="../../src/settingseditor.ui" line="616"/>
<source>Update check interval (days):</source>
<translation>Интервал проверки обновления (дней):</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="602"/>
<location filename="../../src/settingseditor.ui" line="623"/>
<source>0 - disabled</source>
<translation>- отключено</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="635"/>
<location filename="../../src/settingseditor.ui" line="656"/>
<source>Apply updates</source>
<translation>Применить изменения</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="330"/>
<location filename="../../src/settingseditor.ui" line="351"/>
<source>Translate text</source>
<translation>Переводить текст</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="364"/>
<location filename="../../src/settingseditor.ui" line="385"/>
<source>Single translator timeout:</source>
<translation>Переходить к следующему переводчику после:</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="414"/>
<location filename="../../src/settingseditor.ui" line="435"/>
<source>Result type</source>
<translation>Тип результата</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="426"/>
<location filename="../../src/settingseditor.ui" line="447"/>
<source>Tray</source>
<translation>Трей</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="439"/>
<location filename="../../src/settingseditor.ui" line="460"/>
<source>Window</source>
<translation>Окно</translation>
</message>
<message>
<location filename="../../src/settingseditor.ui" line="618"/>
<location filename="../../src/settingseditor.ui" line="639"/>
<source>Check now</source>
<translation>Проверить сейчас</translation>
</message>
@ -1000,7 +1042,7 @@ Check for updates to silence this warning</source>
<translation>Текст для проверки</translation>
</message>
<message>
<location filename="../../src/settingseditor.cpp" line="317"/>
<location filename="../../src/settingseditor.cpp" line="321"/>
<source>Portable changed. Apply settings first</source>
<translation>Portable режиме изменени. Сначала применить настройки</translation>
</message>

101
share/updates/hunspell.py Normal file
View File

@ -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], "<dict_dir> [<download_url>]")
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)

View File

@ -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>();
task->generation = generation_;
task->useHunspell = useHunspell_;
task->captured = pixmap.copy(rect_);
task->capturePoint = rect_.topLeft();
task->sourceLanguage = sourceLanguage_;

View File

@ -28,6 +28,7 @@ private:
QRect rect_;
bool doTranslation_;
bool isLocked_{false};
bool useHunspell_{false};
LanguageId sourceLanguage_;
LanguageId targetLanguage_;
QStringList translators_;

View File

@ -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());

View File

@ -23,6 +23,7 @@ private:
QCheckBox* doTranslation_;
QCheckBox* isLocked_;
QCheckBox* useHunspell_;
QComboBox* sourceLanguage_;
QComboBox* targetLanguage_;
};

View File

@ -1,13 +1,37 @@
#include "corrector.h"
#include "correctorworker.h"
#include "debug.h"
#include "manager.h"
#include "settings.h"
#include "task.h"
#include <QThread>
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);
}

View File

@ -2,17 +2,28 @@
#include "stfwd.h"
class Corrector
#include <QObject>
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_;
};

View File

@ -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<HunspellCorrector>(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);
}
}
}

View File

@ -0,0 +1,32 @@
#pragma once
#include "stfwd.h"
#include <QObject>
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<HunspellCorrector> hunspell;
int usesLeft;
};
void removeUnused(Generation current);
std::map<QString, Bundle> bundles_;
Generation lastGeneration_{};
QString hunspellDir_;
};

View File

@ -0,0 +1,169 @@
#include "hunspellcorrector.h"
#include "debug.h"
#include "languagecodes.h"
#include "settings.h"
#include <hunspell/hunspell.hxx>
#include <QDir>
#include <QRegularExpression>
#include <QTextCodec>
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<int> previousColumn;
previousColumn.reserve(targetCount + 1);
for (auto i = 0; i < targetCount + 1; ++i) previousColumn.append(i);
QVector<int> 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<Hunspell>(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;
}

View File

@ -0,0 +1,26 @@
#pragma once
#include "stfwd.h"
#include <QString>
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<Hunspell> engine_;
QString error_;
};

View File

@ -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<TrayIcon>(*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 );

View File

@ -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";
}

View File

@ -48,6 +48,8 @@ public:
int autoUpdateIntervalDays{0};
QDateTime lastUpdateCheck;
bool useHunspell{false};
QString hunspellDir;
Substitutions userSubstitutions;
bool useUserSubstitutions{true};

View File

@ -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();

View File

@ -286,24 +286,17 @@
</widget>
<widget class="QWidget" name="pageCorrect">
<layout class="QGridLayout" name="gridLayout_10">
<item row="1" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Substitutions</string>
<string>User substitutions</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="useUserSubstitutions">
<property name="text">
<string>Substitute recognized text</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="SubstitutionsTable" name="userSubstitutionsTable">
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
@ -313,6 +306,34 @@
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="useHunspell">
<property name="text">
<string>Use auto corrections (hunspell)</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="useUserSubstitutions">
<property name="text">
<string>Use user substitutions</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Hunspell dictionaries path:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="hunspellDir">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="pageTranslate">

View File

@ -19,6 +19,8 @@ public:
QString corrected;
QString translated;
bool useHunspell{false};
LanguageId sourceLanguage;
LanguageId targetLanguage;

View File

@ -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"}