New Upstream Release - pygtkspellcheck

Ready changes

Summary

Merged new upstream version: 5.0.1 (was: 4.0.5).

Diff

diff --git a/LICENSE.txt b/LICENSE
similarity index 100%
rename from LICENSE.txt
rename to LICENSE
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index c970dff..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,7 +0,0 @@
-include LICENSE.txt
-include MANIFEST.in
-include README.md
-graft examples
-graft doc
-graft locale
-prune doc/build
\ No newline at end of file
diff --git a/PKG-INFO b/PKG-INFO
index 5c8dcf4..6b5be59 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,21 +1,100 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
 Name: pygtkspellcheck
-Version: 4.0.5
-Summary: a simple but quite powerful Python spell checking library for GtkTextViews based on Enchant
-Home-page: http://koehlma.github.com/projects/pygtkspellcheck.html
-Author: Maximilian Köhl & Carlos Jenkins
-Author-email: linuxmaxi@googlemail.com & carlos@jenkins.co.cr
-License: GPLv3+
-Download-URL: https://github.com/koehlma/pygtkspellcheck/tarball/master
-Description: A simple but quite powerful spellchecking library written in pure Python for Gtk based on Enchant. It supports PyGObject as well as PyGtk for Python 2 and 3 with automatic switching and binding detection. For automatic translation of the user interface it can use Gedit’s translation files.
-Platform: UNKNOWN
+Version: 5.0.1
+Summary: A simple but quite powerful spellchecking library for GTK written in pure Python.
+Home-page: https://github.com/koehlma/pygtkspellcheck
+License: GPL-3.0-or-later
+Author: Maximilian Köhl
+Author-email: mail@koehlma.de
+Requires-Python: >=3.7,<4.0
 Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: X11 Applications :: GTK
 Classifier: Environment :: X11 Applications :: Gnome
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
-Classifier: Operating System :: MacOS :: MacOS X
+Classifier: Operating System :: MacOS
 Classifier: Operating System :: Microsoft :: Windows
 Classifier: Operating System :: POSIX
-Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Topic :: Software Development :: Localization
+Provides-Extra: docs
+Requires-Dist: PyGObject (>=3.42.1,<4.0.0)
+Requires-Dist: myst-parser (>=0.18.0,<0.19.0); extra == "docs"
+Requires-Dist: pyenchant (>=3.0,<4.0)
+Requires-Dist: sphinx (>=4.5.0,<5.0.0); extra == "docs"
+Project-URL: Repository, https://github.com/koehlma/pygtkspellcheck.git
+Description-Content-Type: text/markdown
+
+# Python GTK Spellcheck
+
+[![PyPi Project Page](https://img.shields.io/pypi/v/pygtkspellcheck.svg?&label=latest%20version)](https://pypi.python.org/pypi/pygtkspellcheck)
+[![Documentation](https://readthedocs.org/projects/pygtkspellcheck/badge/?version=latest)](https://pygtkspellcheck.readthedocs.org/en/latest/)
+
+Python GTK Spellcheck is a simple but quite powerful spellchecking library for GTK written in pure Python. It's spellchecking component is based on [Enchant](http://www.abisource.com/projects/enchant/) and it supports both GTK 3 and 4 via [PyGObject](https://live.gnome.org/PyGObject/).
+
+**⚡️ News:** Thanks to [@cheywood](https://github.com/cheywood), Python GTK Spellcheck now supports GTK 4! 🎉
+
+**🟢 Status:** This project is mature, actively maintained, and open to contributions and co-maintainership.
+
+
+## ✨ Features
+
+- **spellchecking** based on [Enchant](http://www.abisource.com/projects/enchant/) for `GtkTextView`
+- support for word, line, and multiline **ignore regular expressions**
+- support for both **GTK 3 and 4** via [PyGObject](https://live.gnome.org/PyGObject/) for Python 3
+- configurable extra word characters such as `'`
+- localized names of the available languages based on [ISO-Codes](http://pkg-isocodes.alioth.debian.org/)
+- support for custom ignore tags and hot swap of `GtkTextBuffer`
+- support for Hunspell (LibreOffice) and Aspell (GNU) dictionaries
+
+<p align="center">
+  <img src="https://raw.githubusercontent.com/koehlma/pygtkspellcheck/master/docs/screenshots/screenshot.png" alt="Screenshot" />
+</p>
+
+
+## 🚀 Getting Started
+
+Python GTK Spellcheck is available from the [Python Package Index](https://pypi.python.org/pypi/pygtkspellcheck):
+```sh
+pip install pygtkspellcheck
+```
+Depending on your distribution, you may also find Python GTK Spellcheck in your package manager.
+For instance, on Debian you may want to install the [`python3-gtkspellcheck`](https://packages.debian.org/bullseye/python3-gtkspellcheck) package.
+
+
+## 🥳 Showcase
+
+Over time, several projects have used Python GTK Spellcheck or are still using it. Among those are:
+
+- [Nested Editor](http://nestededitor.sourceforge.net/about.html): “Specialized editor for structured documents.”
+- [Cherry Tree](http://www.giuspen.com/cherrytree/): “A hierarchical note taking application, […].”
+- [Zim](http://zim-wiki.org/): “Zim is a graphical text editor used to maintain a collection of wiki pages.”
+- [REMARKABLE](http://remarkableapp.github.io/): “The best markdown editor for Linux and Windows.”
+- [RedNotebook](http://rednotebook.sourceforge.net/): “RedNotebook is a modern journal.”
+- [Reportbug](https://packages.debian.org/stretch/reportbug): “Reports bugs in the Debian distribution.”
+- [UberWriter](http://uberwriter.wolfvollprecht.de/): “UberWriter is a writing application for markdown.”
+- [Gourmet](https://github.com/thinkle/gourmet): “Gourmet Recipe Manager is a manager, editor, and organizer for recipes.“
+
+
+## 🔖 Versions
+
+Version numbers follow [Semantic Versioning](http://semver.org/). However, the update from 3 to 4 pertains only API incompatible changes in `oxt_extract` and not the spellchecking component. The update from 4 to 5 removed support for Python 2, GTK 2, `pylocales`, and the `oxt_extract` API. Otherwise, the API is still compatible with version 3.
+
+
+## 📚 Documentation
+
+The documentation is available at [Read the Docs](http://pygtkspellcheck.readthedocs.org/).
+
+
+## 🏗 Contributing
+
+We welcome all kinds of contributions! ❤️
+
+For minor changes and bug fixes feel free to simply open a pull request. For major changes impacting the overall design of Python GTK Spellcheck, please first [start a discussion](https://github.com/koehlma/pygtkspellcheck/discussions/new?category=ideas) outlining your idea.
+
+By submitting a PR, you agree to license your contributions under “GPLv3 or later”.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1c5a4c9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,68 @@
+# Python GTK Spellcheck
+
+[![PyPi Project Page](https://img.shields.io/pypi/v/pygtkspellcheck.svg?&label=latest%20version)](https://pypi.python.org/pypi/pygtkspellcheck)
+[![Documentation](https://readthedocs.org/projects/pygtkspellcheck/badge/?version=latest)](https://pygtkspellcheck.readthedocs.org/en/latest/)
+
+Python GTK Spellcheck is a simple but quite powerful spellchecking library for GTK written in pure Python. It's spellchecking component is based on [Enchant](http://www.abisource.com/projects/enchant/) and it supports both GTK 3 and 4 via [PyGObject](https://live.gnome.org/PyGObject/).
+
+**⚡️ News:** Thanks to [@cheywood](https://github.com/cheywood), Python GTK Spellcheck now supports GTK 4! 🎉
+
+**🟢 Status:** This project is mature, actively maintained, and open to contributions and co-maintainership.
+
+
+## ✨ Features
+
+- **spellchecking** based on [Enchant](http://www.abisource.com/projects/enchant/) for `GtkTextView`
+- support for word, line, and multiline **ignore regular expressions**
+- support for both **GTK 3 and 4** via [PyGObject](https://live.gnome.org/PyGObject/) for Python 3
+- configurable extra word characters such as `'`
+- localized names of the available languages based on [ISO-Codes](http://pkg-isocodes.alioth.debian.org/)
+- support for custom ignore tags and hot swap of `GtkTextBuffer`
+- support for Hunspell (LibreOffice) and Aspell (GNU) dictionaries
+
+<p align="center">
+  <img src="https://raw.githubusercontent.com/koehlma/pygtkspellcheck/master/docs/screenshots/screenshot.png" alt="Screenshot" />
+</p>
+
+
+## 🚀 Getting Started
+
+Python GTK Spellcheck is available from the [Python Package Index](https://pypi.python.org/pypi/pygtkspellcheck):
+```sh
+pip install pygtkspellcheck
+```
+Depending on your distribution, you may also find Python GTK Spellcheck in your package manager.
+For instance, on Debian you may want to install the [`python3-gtkspellcheck`](https://packages.debian.org/bullseye/python3-gtkspellcheck) package.
+
+
+## 🥳 Showcase
+
+Over time, several projects have used Python GTK Spellcheck or are still using it. Among those are:
+
+- [Nested Editor](http://nestededitor.sourceforge.net/about.html): “Specialized editor for structured documents.”
+- [Cherry Tree](http://www.giuspen.com/cherrytree/): “A hierarchical note taking application, […].”
+- [Zim](http://zim-wiki.org/): “Zim is a graphical text editor used to maintain a collection of wiki pages.”
+- [REMARKABLE](http://remarkableapp.github.io/): “The best markdown editor for Linux and Windows.”
+- [RedNotebook](http://rednotebook.sourceforge.net/): “RedNotebook is a modern journal.”
+- [Reportbug](https://packages.debian.org/stretch/reportbug): “Reports bugs in the Debian distribution.”
+- [UberWriter](http://uberwriter.wolfvollprecht.de/): “UberWriter is a writing application for markdown.”
+- [Gourmet](https://github.com/thinkle/gourmet): “Gourmet Recipe Manager is a manager, editor, and organizer for recipes.“
+
+
+## 🔖 Versions
+
+Version numbers follow [Semantic Versioning](http://semver.org/). However, the update from 3 to 4 pertains only API incompatible changes in `oxt_extract` and not the spellchecking component. The update from 4 to 5 removed support for Python 2, GTK 2, `pylocales`, and the `oxt_extract` API. Otherwise, the API is still compatible with version 3.
+
+
+## 📚 Documentation
+
+The documentation is available at [Read the Docs](http://pygtkspellcheck.readthedocs.org/).
+
+
+## 🏗 Contributing
+
+We welcome all kinds of contributions! ❤️
+
+For minor changes and bug fixes feel free to simply open a pull request. For major changes impacting the overall design of Python GTK Spellcheck, please first [start a discussion](https://github.com/koehlma/pygtkspellcheck/discussions/new?category=ideas) outlining your idea.
+
+By submitting a PR, you agree to license your contributions under “GPLv3 or later”.
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 13d6551..0000000
--- a/README.rst
+++ /dev/null
@@ -1,71 +0,0 @@
-Python GTK Spellcheck
-=====================
-
-|pypi| |docs|
-
-Python GTK Spellcheck is a simple but quite powerful spellchecking library for GTK written
-in pure Python. It's spellchecking component is based on Enchant_ and it supports both GTK
-bindings (PyGObject_, PyGTK_) as well as Python 3 and 2.
-
-
-Features
---------
-- **spellchecking** based on Enchant_ for `GtkTextViews`
-- support for word, line and multiple line **ignore regular expressions**
-- PyGObject_ and PyGtk_ (automatic detection) as well as Python 3 and 2 compatible
-- localized names of the available languages based on ISO-Codes_
-- support for custom ignore tags and hot swap of `GtkTextBuffers`
-- enable and disable of spellchecking with preferences memory
-- support for Hunspell (LibreOffice) and Aspell (GNU) dictionaries
-- supports extraction of dictionaries out of LibreOffice extension files
-- legacy API for Python GtkSpell
-
-.. image:: https://raw.githubusercontent.com/koehlma/pygtkspellcheck/master/doc/screenshots/screenshot.png
-    :alt: Python GTK Spellcheck Screenshot
-    :align: center
-
-.. _Enchant: http://www.abisource.com/projects/enchant/
-.. _PyGObject: https://live.gnome.org/PyGObject/
-.. _PyGTK: http://www.pygtk.org/
-.. _ISO-Codes: http://pkg-isocodes.alioth.debian.org/
-
-
-Showcase
---------
-- `Nested Editor`_: “Specialized editor for structured documents.”
-- `Cherry Tree`_: “A hierarchical note taking application, […].”
-- `Zim`_: “Zim is a graphical text editor used to maintain a collection of wiki pages.”
-- `REMARKABLE`_: “The best markdown editor for Linux and Windows.”
-- `RedNotebook`_: “RedNotebook is a modern journal.”
-- `Reportbug`_: “Reports bugs in the Debian distribution.”
-- `UberWriter`_: “UberWriter is a writing application for markdown.”
-
-.. _Nested Editor: http://nestededitor.sourceforge.net/about.html
-.. _Cherry Tree: http://www.giuspen.com/cherrytree/
-.. _Zim: http://zim-wiki.org/
-.. _REMARKABLE: http://remarkableapp.github.io/
-.. _RedNotebook: http://rednotebook.sourceforge.net/
-.. _Reportbug: https://packages.debian.org/stretch/reportbug
-.. _UberWriter: http://uberwriter.wolfvollprecht.de/
-
-
-Versions
---------
-Version numbers follow `Semantic Versioning`_. However version change from 3 to 4 pertains
-only API incompatible changes in `oxt_extract` and not the spellchecking component.
-
-.. _Semantic Versioning: http://semver.org/
-
-
-Documentation
--------------
-The documentation is available at `Read the Docs`_.
-
-.. _Read the Docs: http://pygtkspellcheck.readthedocs.org/
-
-
-.. |pypi| image:: https://img.shields.io/pypi/v/pygtkspellcheck.svg?style=flat-square&label=latest%20version
-    :target: https://pypi.python.org/pypi/pygtkspellcheck
-
-.. |docs| image:: https://readthedocs.org/projects/pygtkspellcheck/badge/?version=latest&style=flat-square
-    :target: https://pygtkspellcheck.readthedocs.org/en/latest/
diff --git a/debian/changelog b/debian/changelog
index b67e701..56994e2 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+pygtkspellcheck (5.0.1-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 20 Mar 2023 18:54:42 -0000
+
 pygtkspellcheck (4.0.5-3) unstable; urgency=medium
 
   [ Ondřej Nový ]
diff --git a/doc/Makefile b/doc/Makefile
deleted file mode 100644
index 491ff46..0000000
--- a/doc/Makefile
+++ /dev/null
@@ -1,153 +0,0 @@
-# Makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
-BUILDDIR      = build
-
-# Internal variables.
-PAPEROPT_a4     = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
-
-help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  html       to make standalone HTML files"
-	@echo "  dirhtml    to make HTML files named index.html in directories"
-	@echo "  singlehtml to make a single large HTML file"
-	@echo "  pickle     to make pickle files"
-	@echo "  json       to make JSON files"
-	@echo "  htmlhelp   to make HTML files and a HTML help project"
-	@echo "  qthelp     to make HTML files and a qthelp project"
-	@echo "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
-	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@echo "  changes    to make an overview of all changed/added/deprecated items"
-	@echo "  linkcheck  to check all external links for integrity"
-	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
-
-clean:
-	-rm -rf $(BUILDDIR)/*
-
-html:
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
-	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
-	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
-	@echo
-	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
-json:
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
-	@echo
-	@echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
-	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
-	@echo
-	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
-	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
-	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonGTKSpellchecker.qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonGTKSpellchecker.qhc"
-
-devhelp:
-	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
-	@echo
-	@echo "Build finished."
-	@echo "To view the help file:"
-	@echo "# mkdir -p $$HOME/.local/share/devhelp/PythonGTKSpellchecker"
-	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonGTKSpellchecker"
-	@echo "# devhelp"
-
-epub:
-	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
-	@echo
-	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make' in that directory to run these through (pdf)latex" \
-	      "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through pdflatex..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
-	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
-	@echo
-	@echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo
-	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
-	@echo "Run \`make' in that directory to run these through makeinfo" \
-	      "(use \`make info' here to do that automatically)."
-
-info:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo "Running Texinfo files through makeinfo..."
-	make -C $(BUILDDIR)/texinfo info
-	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext:
-	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
-	@echo
-	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-changes:
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
-	@echo
-	@echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
-	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
-	@echo "Testing of doctests in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/doc/insert_metadata.py b/doc/insert_metadata.py
deleted file mode 100755
index 9cce46e..0000000
--- a/doc/insert_metadata.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env python
-# -*- coding:utf-8 -*-
-#
-# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
-# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-ENCODING = 'UTF-8'
-
-import sys
-import argparse
-
-# Python 2/3 unicode
-import sys
-if sys.version_info.major == 3:
-    io_in = lambda x: x
-    io_out = io_in
-else:
-    io_in = lambda x: x.decode(ENCODING)
-    io_out = lambda x: x.encode(ENCODING)   
-
-# Pipes Python enconding nightmare
-if sys.stdout.encoding is None:
-    import codecs
-    sys.stdout = codecs.getwriter(ENCODING)(sys.stdout)
-
-# Find metadata dict
-from os.path import join, dirname
-sys.path.append(join(dirname(__file__), '../src/'))
-from gtkspellcheck import __metadata__
-
-# Parse command line
-parser = argparse.ArgumentParser(description='Insert metadata into plain text files.')
-parser.add_argument('infile', type=argparse.FileType('r'),
-                     help='path to the template file or stdin pipe.')
-parser.add_argument('-w', '--writeback', action='store_true',
-                     help='write the output back to the input file.')
-args = parser.parse_args()
-
-# Read content
-out_content = io_in(args.infile.read())
-args.infile.close()
-
-# Replace variables
-# FIXME: Stop wasting memory like crazy!
-for key, value in __metadata__.items():
-    out_content = out_content.replace(key, value)
-
-# Print/Write new content
-if args.writeback:
-    try:
-        with open(args.infile.name, 'w') as out_handler:
-            out_handler.write(io_out(out_content))
-    except Exception as e:
-        sys.stderr.write(str(e) + '\n')
-        sys.exit(-1)
-else:
-    print(out_content)
-
-sys.exit(0)
diff --git a/doc/make.bat b/doc/make.bat
deleted file mode 100644
index 5c010b1..0000000
--- a/doc/make.bat
+++ /dev/null
@@ -1,190 +0,0 @@
-@ECHO OFF
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
-	set SPHINXBUILD=sphinx-build2
-)
-set BUILDDIR=build
-set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
-set I18NSPHINXOPTS=%SPHINXOPTS% source
-if NOT "%PAPER%" == "" (
-	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
-	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
-)
-
-if "%1" == "" goto help
-
-if "%1" == "help" (
-	:help
-	echo.Please use `make ^<target^>` where ^<target^> is one of
-	echo.  html       to make standalone HTML files
-	echo.  dirhtml    to make HTML files named index.html in directories
-	echo.  singlehtml to make a single large HTML file
-	echo.  pickle     to make pickle files
-	echo.  json       to make JSON files
-	echo.  htmlhelp   to make HTML files and a HTML help project
-	echo.  qthelp     to make HTML files and a qthelp project
-	echo.  devhelp    to make HTML files and a Devhelp project
-	echo.  epub       to make an epub
-	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
-	echo.  text       to make text files
-	echo.  man        to make manual pages
-	echo.  texinfo    to make Texinfo files
-	echo.  gettext    to make PO message catalogs
-	echo.  changes    to make an overview over all changed/added/deprecated items
-	echo.  linkcheck  to check all external links for integrity
-	echo.  doctest    to run all doctests embedded in the documentation if enabled
-	goto end
-)
-
-if "%1" == "clean" (
-	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
-	del /q /s %BUILDDIR%\*
-	goto end
-)
-
-if "%1" == "html" (
-	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
-	goto end
-)
-
-if "%1" == "dirhtml" (
-	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
-	goto end
-)
-
-if "%1" == "singlehtml" (
-	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
-	goto end
-)
-
-if "%1" == "pickle" (
-	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can process the pickle files.
-	goto end
-)
-
-if "%1" == "json" (
-	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can process the JSON files.
-	goto end
-)
-
-if "%1" == "htmlhelp" (
-	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can run HTML Help Workshop with the ^
-.hhp project file in %BUILDDIR%/htmlhelp.
-	goto end
-)
-
-if "%1" == "qthelp" (
-	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can run "qcollectiongenerator" with the ^
-.qhcp project file in %BUILDDIR%/qthelp, like this:
-	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PythonGTKSpellchecker.qhcp
-	echo.To view the help file:
-	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PythonGTKSpellchecker.ghc
-	goto end
-)
-
-if "%1" == "devhelp" (
-	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished.
-	goto end
-)
-
-if "%1" == "epub" (
-	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The epub file is in %BUILDDIR%/epub.
-	goto end
-)
-
-if "%1" == "latex" (
-	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
-	goto end
-)
-
-if "%1" == "text" (
-	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The text files are in %BUILDDIR%/text.
-	goto end
-)
-
-if "%1" == "man" (
-	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The manual pages are in %BUILDDIR%/man.
-	goto end
-)
-
-if "%1" == "texinfo" (
-	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
-	goto end
-)
-
-if "%1" == "gettext" (
-	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
-	goto end
-)
-
-if "%1" == "changes" (
-	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.The overview file is in %BUILDDIR%/changes.
-	goto end
-)
-
-if "%1" == "linkcheck" (
-	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Link check complete; look for any errors in the above output ^
-or in %BUILDDIR%/linkcheck/output.txt.
-	goto end
-)
-
-if "%1" == "doctest" (
-	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Testing of doctests in the sources finished, look at the ^
-results in %BUILDDIR%/doctest/output.txt.
-	goto end
-)
-
-:end
diff --git a/doc/metadata/documentation.rst b/doc/metadata/documentation.rst
deleted file mode 100644
index aa1ab72..0000000
--- a/doc/metadata/documentation.rst
+++ /dev/null
@@ -1,62 +0,0 @@
-{% names['full'] %}
-{% '=' * len(names['full']) %}
-{% linkify(description['long'], 'rst') %}
-
-Features
---------
-{% '- ' + '\n- '.join(features) %}
-
-API Reference
--------------
-.. autoclass:: gtkspellcheck.spellcheck.SpellChecker
-   :members:
-   
-.. autoclass:: gtkspellcheck.spellcheck.NoDictionariesFound
-
-.. autoclass:: gtkspellcheck.spellcheck.NoGtkBindingFound
-	
-.. autofunction:: pylocales.code_to_name
-
-.. autofunction:: gtkspellcheck.oxt_extract.extract
-
-.. autofunction:: gtkspellcheck.oxt_extract.batch_extract
-
-.. autoclass:: gtkspellcheck.oxt_extract.BadXml
-
-.. autoclass:: gtkspellcheck.oxt_extract.BadExtensionFile
-
-.. autoclass:: gtkspellcheck.oxt_extract.ExtractPathIsNoDirectory
-
-Development
------------
-Development happens at `GitHub`_.
-
-.. _GitHub: {% development %}
-
-	``git clone git://github.com/koehlma/pygtkspellcheck.git``
-
-Download last sources in a `ZIP`_ or `Tarball`_ file.
-
-.. _ZIP: https://github.com/koehlma/pygtkspellcheck/zipball/master
-.. _Tarball: https://github.com/koehlma/pygtkspellcheck/tarball/master
-
-Website
--------
-Checkout the `official project website`_ for additional information.
-
-.. _official project website: {% homepage %}
-
-Examples
---------
-- `PyGObject Simple Example`_
-- `PyGtk Simple Example`_
-
-.. _PyGObject Simple Example: https://github.com/koehlma/pygtkspellcheck/blob/master/examples/simple_pygobject.py
-.. _PyGtk Simple Example: https://github.com/koehlma/pygtkspellcheck/blob/master/examples/simple_pygtk.py
-
-
-License
--------
-{% names['short'] %} is released under `GPLv3`_ or at your opinion any later version.
-
-.. _GPLv3: https://www.gnu.org/licenses/gpl-3.0.html    
\ No newline at end of file
diff --git a/doc/metadata/metadata.py b/doc/metadata/metadata.py
deleted file mode 100644
index 61bbff1..0000000
--- a/doc/metadata/metadata.py
+++ /dev/null
@@ -1,102 +0,0 @@
-# -*- coding:utf-8 -*-
-#
-# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import os.path
-import re
-
-__path__ = os.path.dirname(__file__)
-
-names = {'full': 'Python GTK Spellchecker',
-         'short': 'PyGtkSpellcheck',
-         'url': 'pygtkspellcheck'}
-
-features = ['localized names of the available languages',
-            'supports word, line and multiple line ignore regular expressions',
-            'supports ignore custom tags on GtkTextBuffer',
-            'enable and disable of spellchecking with preferences memory',
-            'supports hotswap of GtkTextBuffers',
-            'PyGObject and PyGtk compatible with automatic detection',
-            'Python 2 and 3 support'
-            'as Enchant, support for Hunspell (LibreOffice) and Aspell (GNU) dictionaries',
-            'extract dictionaries out of LibreOffice extension files',
-            'legacy API for Python GtkSpell']
-
-description = {'short': 'a simple but quite powerful Python spell checking library for GtkTextViews based on Enchant',
-               'long': ('A simple but quite powerful spellchecking library written '
-                        'in pure Python for Gtk based on Enchant. It supports PyGObject '
-                        'as well as PyGtk for Python 2 and 3 with automatic switching '
-                        'and binding detection. For automatic translation of the user '
-                        'interface it can use Gedit’s translation files.')}
-
-screenshot = os.path.join(__path__, 'screenshot.png')
-
-development = 'https://github.com/koehlma/pygtkspellcheck'
-
-documentation = 'http://pygtkspellcheck.readthedocs.org/'
-
-homepage = 'http://koehlma.github.com/projects/pygtkspellcheck.html'
-
-links = {'Enchant': 'http://www.abisource.com/projects/enchant/',
-         'PyGObject': 'https://live.gnome.org/PyGObject/',
-         'PyGtk': 'http://www.pygtk.org/'}
-
-with open(os.path.join(__path__, 'readme.md'), 'rb') as _readme:
-    readme = _readme.read().decode('utf-8')
-    
-with open(os.path.join(__path__, 'pypi.rst'), 'rb') as _pypi:
-    pypi = _pypi.read().decode('utf-8')
-    
-with open(os.path.join(__path__, 'documentation.rst'), 'rb') as _documentation:
-    docs = _documentation.read().decode('utf-8')
-
-with open(os.path.join(__path__, 'website.md'), 'rb') as _website:
-    website = _website.read().decode('utf-8')
-
-replace = re.compile('\{%\s*(.+?)\s*%\}')
-
-def linkify(text, format):
-    if format == 'markdown':
-        for name, url in links.items():
-            text = text.replace(name, '[{}]({})'.format(name, url))
-    elif format == 'rst':
-        for name in links:
-            text = text.replace(name, '`{}`_'.format(name))
-        text += '\n'
-        for name, url in links.items():
-            if text.find(name) > -1:
-                text += '\n.. _{}: {}'.format(name, url)            
-    return text
-
-def template(match):
-    code = match.group(1)
-    return str(eval(code))    
-
-if __name__ == '__main__':
-    print('creating readme')
-    with open(os.path.join(__path__, '..', '..', 'README.md'), 'wb') as _readme:
-        _readme.write(replace.sub(template, readme).encode('utf-8'))
-    print('creating pypi')
-    with open(os.path.join(__path__, '..', 'pypi', 'page.rst'), 'wb') as _pypi:
-        _pypi.write(replace.sub(template, pypi).encode('utf-8'))
-    print('creating documentation')
-    with open(os.path.join(__path__, '..', 'source', 'index.rst'), 'wb') as _documentation:
-        _documentation.write(replace.sub(template, docs).encode('utf-8'))
-    koehlma_github = os.path.join(__path__, '..', '..', '..', 'koehlma.github.com')
-    if os.path.exists(koehlma_github):
-        print('creating website')
-        with open(os.path.join(koehlma_github, 'projects', 'pygtkspellcheck.md'), 'wb') as _website:
-            _website.write(replace.sub(template, website).encode('utf-8'))
\ No newline at end of file
diff --git a/doc/metadata/pypi.rst b/doc/metadata/pypi.rst
deleted file mode 100644
index aebb83c..0000000
--- a/doc/metadata/pypi.rst
+++ /dev/null
@@ -1,85 +0,0 @@
-{% names['full'] %}
-{% '=' * len(names['full']) %}
-{% linkify(description['long'], 'rst') %}
-
-Features
-========
-{% '- ' + '\n- '.join(features) %}
-
-Documentation
-=============
-The documentation is available at `Read the Docs`_.
-
-.. _Read the Docs: {% documentation %}
-
-Distribution
-============
-Cheeseshop
-^^^^^^^^^^
-`PyPI package`_ is available:
-
-.. _PyPI package: http://pypi.python.org/pypi/pygtkspellcheck/
-
-::
-
-    pip install pygtkspellcheck
-
-Archlinux - AUR
-^^^^^^^^^^^^^^^
-Python 3
---------
-
-::
-    pacman -S python-gtkspellcheck
-
-Python 2
---------
-
-::
-    pacman -S python2-gtkspellcheck
-
-Ubuntu / Debian
-^^^^^^^^^^^^^^^
-Ubuntu - Repository
--------------------
-
-::
-
-    sudo add-apt-repository ppa:koehlma/packages
-    sudo apt-get update
-
-Debian - Repository
--------------------
-
-::
-
-    sudo su
-    echo "deb http://ppa.launchpad.net/koehlma/packages/ubuntu precise main" >> /etc/apt/sources.list
-    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 775B7DF6
-    apt-get update
-
-Python 2
---------
-
-::
-    
-    sudo apt-get install python-gtkspellcheck
-
-Python 3
---------
-
-::
-
-    sudo apt-get install python3-gtkspellcheck
-
-Development
-^^^^^^^^^^^
-Development happens at `GitHub`_.
-
-.. _GitHub: {% development %}
-
-License
-^^^^^^^
-{% names['short'] %} is released under `GPLv3`_ or at your opinion any later version.
-
-.. _GPLv3: https://www.gnu.org/licenses/gpl-3.0.html
diff --git a/doc/metadata/readme.md b/doc/metadata/readme.md
deleted file mode 100644
index 4c61641..0000000
--- a/doc/metadata/readme.md
+++ /dev/null
@@ -1,25 +0,0 @@
-## About
-{% linkify(description['long'], 'markdown') %}
-
-## Features
-{% '* ' + '\n* '.join(features) %}
-
-## Documentation
-The documentation is available at [Read the Docs]({% documentation %}).
-
-## Website
-Checkout the [official project website]({% homepage %}).
-
-## License
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
diff --git a/doc/metadata/screenshot.png b/doc/metadata/screenshot.png
deleted file mode 100644
index 1075b12..0000000
Binary files a/doc/metadata/screenshot.png and /dev/null differ
diff --git a/doc/metadata/website.md b/doc/metadata/website.md
deleted file mode 100644
index f5a02bb..0000000
--- a/doc/metadata/website.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-layout: default
-title: Python GTK Spellchecker
-repository: pygtkspellcheck
-downloads:
-- class: archlinux
-  url: https://github.com/downloads/koehlma/pygtkspellcheck/python-gtkspellcheck-3.0-1-any.pkg.tar.xz
-  text: Python 3
-- class: archlinux
-  url: https://github.com/downloads/koehlma/pygtkspellcheck/python2-gtkspellcheck-3.0-1-any.pkg.tar.xz
-  text: Python 2
-- class: debian
-  url: https://github.com/downloads/koehlma/pygtkspellcheck/python3-gtkspellcheck_3.0-1_all.deb
-  text: Python 3
-- class: debian
-  url: https://github.com/downloads/koehlma/pygtkspellcheck/python-gtkspellcheck_3.0-1_all.deb
-  text: Python 2
----
-
-{% linkify(description['long'], 'markdown') %}
-
-# Features
-{% '* ' + '\n* '.join(features) %}
-
-# Screenshots
-![Screenshot](/projects/pygtkspellcheck/screenshot.png)
-
-## Documentation
-The documentation is available at [Read the Docs]({% documentation %}).
-
-# Examples
-* [PyGObject Simple Example](https://github.com/koehlma/pygtkspellcheck/blob/master/examples/simple_pygobject.py)
-* [PyGtk Simple Example](https://github.com/koehlma/pygtkspellcheck/blob/master/examples/simple_pygtk.py)
-
-# Distribution
-## Cheeseshop
-[PyPI package](http://pypi.python.org/pypi/pygtkspellcheck/) is available: 
-
-    pip install pygtkspellcheck
-
-## Archlinux - AUR
-### Python 3
-[AUR Package](https://aur.archlinux.org/packages.php?ID=61200)
-
-    yaourt -S python-gtkspellcheck
-
-### Python 2
-[AUR Package](https://aur.archlinux.org/packages.php?ID=61199)
-
-    yaourt -S python2-gtkspellcheck
-
-## Ubuntu / Debian
-### Ubuntu - Repository
-
-    sudo add-apt-repository ppa:koehlma/packages
-    sudo apt-get update
-
-### Debian - Repository
-    
-    sudo su
-    echo "deb http://ppa.launchpad.net/koehlma/packages/ubuntu precise main" >> /etc/apt/sources.list
-    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 775B7DF6
-    apt-get update
-
-### Python 2
-    
-    sudo apt-get install python-gtkspellcheck
-
-### Python 3
-
-    sudo apt-get install python3-gtkspellcheck
diff --git a/doc/pypi/index.html b/doc/pypi/index.html
deleted file mode 100644
index c2514c2..0000000
--- a/doc/pypi/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-    <head>
-       <meta http-equiv="content-type" content="text/html; charset=utf-8">
-       <meta http-equiv="refresh" content="2; URL=http://pygtkspellcheck.readthedocs.org/">
-       
-       <title>PyGtkSpellcheck - Documentation</title>
-       
-       <meta name="author" content="Maximilian Köhl">             
-    </head>
-    <body>
-        The documentation has moved to <a href="http://pygtkspellcheck.readthedocs.org">Read the Docs</a>.
-    </body>
-</html>
diff --git a/doc/pypi/page.rst b/doc/pypi/page.rst
deleted file mode 100644
index 995c241..0000000
--- a/doc/pypi/page.rst
+++ /dev/null
@@ -1,81 +0,0 @@
-Python GTK Spellchecker
-=======================
-A simple but quite powerful spellchecking library written in pure Python for Gtk based on `Enchant`_. It supports `PyGObject`_ as well as `PyGtk`_ for Python 2 and 3 with automatic switching and binding detection. For automatic translation of the user interface it can use Gedit’s translation files.
-
-.. _PyGObject: https://live.gnome.org/PyGObject/
-.. _Enchant: http://www.abisource.com/projects/enchant/
-.. _PyGtk: http://www.pygtk.org/
-
-Features
-========
-- localized names of the available languages
-- supports word, line and multiple line ignore regular expressions
-- supports ignore custom tags on GtkTextBuffer
-- enable and disable of spellchecking with preferences memory
-- supports hotswap of GtkTextBuffers
-- PyGObject and PyGtk compatible with automatic detection
-- Python 2 and 3 supportas Enchant, support for Hunspell (LibreOffice) and Aspell (GNU) dictionaries
-- extract dictionaries out of LibreOffice extension files
-- legacy API for Python GtkSpell
-
-Documentation
-=============
-The documentation is available at `Read the Docs`_.
-
-.. _Read the Docs: http://pygtkspellcheck.readthedocs.org/
-
-Distribution
-============
-Cheeseshop
-^^^^^^^^^^
-`PyPI package`_ is available:
-
-.. _PyPI package: http://pypi.python.org/pypi/pygtkspellcheck/
-
-::
-
-    pip install pygtkspellcheck
-
-Archlinux - AUR
-^^^^^^^^^^^^^^^
-Python 3
---------
-
-::
-
-    pacman -S python-gtkspellcheck
-
-Python 2
---------
-
-::
-
-    pacman -S python2-gtkspellcheck
-
-Ubuntu / Debian
-^^^^^^^^^^^^^^^
-Python 2
---------
-
-::
-    
-    sudo apt-get install python-gtkspellcheck
-
-Python 3
---------
-
-::
-
-    sudo apt-get install python3-gtkspellcheck
-
-Development
-^^^^^^^^^^^
-Development happens at `GitHub`_.
-
-.. _GitHub: https://github.com/koehlma/pygtkspellcheck
-
-License
-^^^^^^^
-PyGtkSpellcheck is released under `GPLv3`_ or at your opinion any later version.
-
-.. _GPLv3: https://www.gnu.org/licenses/gpl-3.0.html
diff --git a/doc/screenshots/screenshot.png b/doc/screenshots/screenshot.png
deleted file mode 100644
index 1075b12..0000000
Binary files a/doc/screenshots/screenshot.png and /dev/null differ
diff --git a/doc/source/conf.py b/doc/source/conf.py
deleted file mode 100644
index 5810273..0000000
--- a/doc/source/conf.py
+++ /dev/null
@@ -1,277 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Python GTK Spellchecker documentation build configuration file, created by
-# sphinx-quickstart2 on Tue Apr 10 18:57:32 2012.
-#
-# This file is execfile()d with the current directory set to its containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
-import sys, os
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
-
-doc_directory = os.path.dirname(os.path.abspath(__file__))
-sys.path.append(os.path.join(doc_directory, '..', '..', 'src'))
-
-import sys
-
-# Support for readthedocs.org
-class Mock(object):
-    def __init__(self, *args, **kwargs):
-        pass
-
-    def __call__(self, *args, **kwargs):
-        return Mock()
-
-    @classmethod
-    def __getattr__(self, name):
-        if name in ('__file__', '__path__'):
-            return '/dev/null'
-        elif name[0] == name[0].upper():
-            return type(name, (), {})
-        else:
-            return Mock()
-
-MOCK_MODULES = ['enchant']
-for mod_name in MOCK_MODULES:
-    try:
-        __import__(mod_name)
-    except:
-        sys.modules[mod_name] = Mock()
-
-sys.modules['gtk'] = None
-import gtkspellcheck as m
-import pylocales
-
-start_file = 'index'
-
-# -- General configuration -----------------------------------------------------
-
-# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
-
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc']
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
-
-# The suffix of source filenames.
-source_suffix = '.rst'
-
-# The encoding of source files.
-#source_encoding = 'utf-8-sig'
-
-# The master toctree document.
-master_doc = start_file
-
-# General information about the project.
-project = m.__project__
-copyright = m.__authors__
-
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-version = m.__version__
-# The full version, including alpha/beta/rc tags.
-release = m.__version__
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-#language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-#today = ''
-# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-exclude_patterns = []
-
-# The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-#add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-#show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
-
-
-# -- Options for HTML output ---------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-html_theme = 'default'
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further.  For a list of options available for each theme, see the
-# documentation.
-#html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
-
-# The name for this set of Sphinx documents.  If None, it defaults to
-# "<project> v<release> documentation".
-#html_title = None
-
-# A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-#html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-#html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-#html_static_path = ['_static']
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-#html_additional_pages = {}
-
-# If false, no module index is generated.
-#html_domain_indices = True
-
-# If false, no index is generated.
-#html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-#html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
-
-# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it.  The value of this option must be the
-# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = m.__short_name__ + 'doc'
-
-
-# -- Options for LaTeX output --------------------------------------------------
-
-latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
-latex_documents = [
-  (start_file, m.__short_name__ + '.tex', m.__project__ + ' Documentation',
-   m.__authors__.replace('&', r'\&'), 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# If true, show page references after internal links.
-#latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-#latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_domain_indices = True
-
-
-# -- Options for manual page output --------------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
-    (start_file, m.__short_name__, m.__project__ + ' Documentation',
-     m.__authors__.split(' & '), 1)
-]
-
-# If true, show URL addresses after external links.
-#man_show_urls = False
-
-
-# -- Options for Texinfo output ------------------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-#  dir menu entry, description, category)
-texinfo_documents = [
-  (start_file, m.__short_name__, m.__project__ + ' Documentation',
-   m.__authors__, m.__short_name__, m.__desc_short__,
-   'Miscellaneous'),
-]
-
-# Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
-
-# If false, no module index is generated.
-#texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
diff --git a/doc/source/index.rst b/doc/source/index.rst
deleted file mode 100644
index 8cbd958..0000000
--- a/doc/source/index.rst
+++ /dev/null
@@ -1,83 +0,0 @@
-Python GTK Spellchecker
-=======================
-A simple but quite powerful spellchecking library written in pure Python for Gtk based on `Enchant`_. It supports `PyGObject`_ as well as `PyGtk`_ for Python 2 and 3 with automatic switching and binding detection. For automatic translation of the user interface it can use Gedit’s translation files.
-
-.. _PyGObject: https://live.gnome.org/PyGObject/
-.. _Enchant: http://www.abisource.com/projects/enchant/
-.. _PyGtk: http://www.pygtk.org/
-
-Features
---------
-- localized names of the available languages
-- supports word, line and multiple line ignore regular expressions
-- supports ignore custom tags on GtkTextBuffer
-- enable and disable of spellchecking with preferences memory
-- supports hotswap of GtkTextBuffers
-- PyGObject and PyGtk compatible with automatic detection
-- Python 2 and 3 supportas Enchant, support for Hunspell (LibreOffice) and Aspell (GNU) dictionaries
-- extract dictionaries out of LibreOffice extension files
-- legacy API for Python GtkSpell
-
-API Reference
--------------
-.. autoclass:: gtkspellcheck.spellcheck.SpellChecker
-   :members:
-   
-.. autoclass:: gtkspellcheck.spellcheck.NoDictionariesFound
-
-.. autoclass:: gtkspellcheck.spellcheck.NoGtkBindingFound
-
-
-Deprecated API Reference
-------------------------
-.. warning::
-
-   The following functions are deprecated since version 4.0.5, they will be removed
-   from "pygtkspellcheck" in 5.0.
-
-
-.. autofunction:: pylocales.code_to_name
-
-.. autofunction:: gtkspellcheck.oxt_extract.extract
-
-.. autofunction:: gtkspellcheck.oxt_extract.batch_extract
-
-.. autoclass:: gtkspellcheck.oxt_extract.BadXml
-
-.. autoclass:: gtkspellcheck.oxt_extract.BadExtensionFile
-
-.. autoclass:: gtkspellcheck.oxt_extract.ExtractPathIsNoDirectory
-
-Development
------------
-Development happens at `GitHub`_.
-
-.. _GitHub: https://github.com/koehlma/pygtkspellcheck
-
-	``git clone git://github.com/koehlma/pygtkspellcheck.git``
-
-Download last sources in a `ZIP`_ or `Tarball`_ file.
-
-.. _ZIP: https://github.com/koehlma/pygtkspellcheck/zipball/master
-.. _Tarball: https://github.com/koehlma/pygtkspellcheck/tarball/master
-
-Website
--------
-Checkout the `official project website`_ for additional information.
-
-.. _official project website: http://koehlma.github.com/projects/pygtkspellcheck.html
-
-Examples
---------
-- `PyGObject Simple Example`_
-- `PyGtk Simple Example`_
-
-.. _PyGObject Simple Example: https://github.com/koehlma/pygtkspellcheck/blob/master/examples/simple_pygobject.py
-.. _PyGtk Simple Example: https://github.com/koehlma/pygtkspellcheck/blob/master/examples/simple_pygtk.py
-
-
-License
--------
-PyGtkSpellcheck is released under `GPLv3`_ or at your opinion any later version.
-
-.. _GPLv3: https://www.gnu.org/licenses/gpl-3.0.html    
\ No newline at end of file
diff --git a/examples/large_pygobject.py b/examples/large_pygobject.py
deleted file mode 100644
index 4607b5e..0000000
--- a/examples/large_pygobject.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- coding:utf-8 -*-
-#
-# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
-# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-# Load example if running from source, ignore this
-import sys
-from os.path import join, dirname
-sys.path.append(join(dirname(__file__), '../src/'))
-
-import locale
-
-from gi.repository import Gtk as gtk
-
-from gtkspellcheck import SpellChecker
-
-if __name__ == '__main__':
-    def quit(*args):
-        gtk.main_quit()
-        
-    window = gtk.Window.new(gtk.WindowType.TOPLEVEL)
-    window.set_title('PyGtkSpellCheck Example')
-    view = gtk.TextView.new()
-    
-    spellchecker = SpellChecker(view, locale.getdefaultlocale()[0], collapse=False)
-    
-    for code, name in spellchecker.languages:
-        print('code: %5s, language: %s' % (code, name))
-    
-    window.set_default_size(600, 400)
-    window.add(view)
-    window.show_all()
-    window.connect('delete-event', quit)
-    gtk.main()
\ No newline at end of file
diff --git a/examples/large_pygtk.py b/examples/large_pygtk.py
deleted file mode 100644
index 35a1479..0000000
--- a/examples/large_pygtk.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- coding:utf-8 -*-
-#
-# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
-# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-# Load example if running from source, ignore this
-import sys
-from os.path import join, dirname
-sys.path.append(join(dirname(__file__), '../src/'))
-
-import locale
-
-import gtk
-
-from gtkspellcheck import SpellChecker
-
-if __name__ == '__main__':
-    def quit(*args):
-        gtk.main_quit()
-    
-    window = gtk.Window(gtk.WINDOW_TOPLEVEL)
-    window.set_title('PyGtkSpellCheck Example')
-    view = gtk.TextView()
-    
-    spellchecker = SpellChecker(view, locale.getdefaultlocale()[0], collapse=False)
-        
-    for code, name in spellchecker.languages:
-        print('code: %5s, language: %s' % (code, name))
-    
-    window.set_default_size(600, 400)
-    window.add(view)
-    window.show_all()
-    window.connect('delete-event', quit)
-    gtk.main()
\ No newline at end of file
diff --git a/examples/simple_pygobject.py b/examples/simple_pygobject.py
deleted file mode 100644
index 8bf7bab..0000000
--- a/examples/simple_pygobject.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- coding:utf-8 -*-
-#
-# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
-# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-# Load example if running from source, ignore this
-import sys
-from os.path import join, dirname
-sys.path.append(join(dirname(__file__), '../src/'))
-
-import locale
-
-from gi.repository import Gtk as gtk
-
-from gtkspellcheck import SpellChecker
-
-if __name__ == '__main__':
-    def quit(*args):
-        gtk.main_quit()
-        
-    window = gtk.Window.new(gtk.WindowType.TOPLEVEL)
-    window.set_title('PyGtkSpellCheck Example')
-    view = gtk.TextView.new()
-    
-    spellchecker = SpellChecker(view, locale.getdefaultlocale()[0])
-    
-    for code, name in spellchecker.languages:
-        print('code: %5s, language: %s' % (code, name))
-    
-    window.set_default_size(600, 400)
-    window.add(view)
-    window.show_all()
-    window.connect('delete-event', quit)
-    gtk.main()
\ No newline at end of file
diff --git a/examples/simple_pygtk.py b/examples/simple_pygtk.py
deleted file mode 100644
index 9e26982..0000000
--- a/examples/simple_pygtk.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- coding:utf-8 -*-
-#
-# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
-# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-# Load example if running from source, ignore this
-import sys
-from os.path import join, dirname
-sys.path.append(join(dirname(__file__), '../src/'))
-
-import locale
-
-import gtk
-
-from gtkspellcheck import SpellChecker
-
-if __name__ == '__main__':
-    def quit(*args):
-        gtk.main_quit()
-    
-    window = gtk.Window(gtk.WINDOW_TOPLEVEL)
-    window.set_title('PyGtkSpellCheck Example')
-    view = gtk.TextView()
-    
-    spellchecker = SpellChecker(view, locale.getdefaultlocale()[0])
-        
-    for code, name in spellchecker.languages:
-        print('code: %5s, language: %s' % (code, name))
-    
-    window.set_default_size(600, 400)
-    window.add(view)
-    window.show_all()
-    window.connect('delete-event', quit)
-    gtk.main()
diff --git a/locale/de/pygtkspellcheck.mo b/locale/de/pygtkspellcheck.mo
deleted file mode 100644
index f660023..0000000
Binary files a/locale/de/pygtkspellcheck.mo and /dev/null differ
diff --git a/locale/de/pygtkspellcheck.po b/locale/de/pygtkspellcheck.po
deleted file mode 100644
index 4537de0..0000000
--- a/locale/de/pygtkspellcheck.po
+++ /dev/null
@@ -1,58 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: gtkspellchecker\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-01-07 23:30+0100\n"
-"PO-Revision-Date: 2013-01-07 23:30+0100\n"
-"Last-Translator: Maximilian Köhl <linuxmaxi@googlemail.com>\n"
-"Language-Team: \n"
-"Language: de\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Poedit-KeywordsList: _;gettext;gettext_noop\n"
-"X-Poedit-Basepath: /home/maximilian/Entwicklung/pygtkspellcheck/src\n"
-"X-Poedit-SourceCharset: UTF-8\n"
-"X-Poedit-SearchPath-0: ./gtkspellcheck\n"
-
-#: gtkspellcheck/spellcheck.py:98
-msgid "Unknown"
-msgstr "Unbekannt"
-
-#: gtkspellcheck/spellcheck.py:480
-msgid "(no suggestions)"
-msgstr "(keine Vorschläge)"
-
-#: gtkspellcheck/spellcheck.py:502 gtkspellcheck/spellcheck.py:505
-msgid "Add \"{}\" to Dictionary"
-msgstr "\"{}\" zum Wörterbuch hinzufügen"
-
-#: gtkspellcheck/spellcheck.py:509 gtkspellcheck/spellcheck.py:511
-msgid "Ignore All"
-msgstr "Alles ignorieren"
-
-#: gtkspellcheck/spellcheck.py:526 gtkspellcheck/spellcheck.py:528
-msgid "Languages"
-msgstr "Sprachen"
-
-#: gtkspellcheck/spellcheck.py:544 gtkspellcheck/spellcheck.py:547
-msgid "Suggestions"
-msgstr "Vorschläge"
-
-#: gtkspellcheck/oxt_extract.py:244
-msgid "extension \"{}\" is not a valid ZIP file"
-msgstr "Extension  \"{}\" ist keine ZIP Datei"
-
-#: gtkspellcheck/oxt_extract.py:250
-msgid "extension \"{}\" has no valid XML dictionary registry"
-msgstr "Extension hat keine valide XML Registry"
-
-#: gtkspellcheck/oxt_extract.py:270
-msgid "unable to move extension, file with same name exists within move_path"
-msgstr ""
-"Extension kann nicht verschoben werden, es existiert bereits eine Datei mit "
-"gleichem Name in move_path"
-
-#: gtkspellcheck/oxt_extract.py:278
-msgid "unable to move extension, move_path is not a directory"
-msgstr "Extension kann nicht verschoben werden, move_path ist kein Verzeichnis"
diff --git a/locale/en/pygtkspellcheck.mo b/locale/en/pygtkspellcheck.mo
deleted file mode 100644
index 87dde81..0000000
Binary files a/locale/en/pygtkspellcheck.mo and /dev/null differ
diff --git a/locale/en/pygtkspellcheck.po b/locale/en/pygtkspellcheck.po
deleted file mode 100644
index 070a130..0000000
--- a/locale/en/pygtkspellcheck.po
+++ /dev/null
@@ -1,60 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: gtkspellcheck\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-01-07 23:31+0100\n"
-"PO-Revision-Date: 2013-01-07 23:31+0100\n"
-"Last-Translator: Maximilian Köhl <linuxmaxi@googlemail.com>\n"
-"Language-Team: \n"
-"Language: en\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Poedit-Basepath: /home/maximilian/Entwicklung/pygtkspellcheck/src\n"
-"X-Poedit-SourceCharset: UTF-8\n"
-"X-Poedit-SearchPath-0: ./gtkspellcheck\n"
-
-#: gtkspellcheck/spellcheck.py:98
-msgid "Unknown"
-msgstr ""
-
-#: gtkspellcheck/spellcheck.py:480
-msgid "(no suggestions)"
-msgstr ""
-
-#: gtkspellcheck/spellcheck.py:502 gtkspellcheck/spellcheck.py:505
-msgid "Add \"{}\" to Dictionary"
-msgstr ""
-
-#: gtkspellcheck/spellcheck.py:509 gtkspellcheck/spellcheck.py:511
-msgid "Ignore All"
-msgstr ""
-
-#: gtkspellcheck/spellcheck.py:526 gtkspellcheck/spellcheck.py:528
-msgid "Languages"
-msgstr ""
-
-#: gtkspellcheck/spellcheck.py:544 gtkspellcheck/spellcheck.py:547
-msgid "Suggestions"
-msgstr ""
-
-#: gtkspellcheck/oxt_extract.py:244
-msgid "extension \"{}\" is not a valid ZIP file"
-msgstr ""
-
-#: gtkspellcheck/oxt_extract.py:250
-msgid "extension \"{}\" has no valid XML dictionary registry"
-msgstr ""
-
-#: gtkspellcheck/oxt_extract.py:270
-msgid "unable to move extension, file with same name exists within move_path"
-msgstr ""
-
-#: gtkspellcheck/oxt_extract.py:278
-msgid "unable to move extension, move_path is not a directory"
-msgstr ""
diff --git a/locale/es/pygtkspellcheck.mo b/locale/es/pygtkspellcheck.mo
deleted file mode 100644
index f2dd84a..0000000
Binary files a/locale/es/pygtkspellcheck.mo and /dev/null differ
diff --git a/locale/es/pygtkspellcheck.po b/locale/es/pygtkspellcheck.po
deleted file mode 100644
index e0e58bf..0000000
--- a/locale/es/pygtkspellcheck.po
+++ /dev/null
@@ -1,56 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: gtkspellcheck\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2012-09-15 10:16+0100\n"
-"PO-Revision-Date: 2012-09-15 10:16+0100\n"
-"Last-Translator: Maximilian Köhl <linuxmaxi@googlemail.com>\n"
-"Language-Team: Carlos Jenkins <carlos@jenkins.co.cr>\n"
-"Language: es\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Poedit-SourceCharset: UTF-8\n"
-"X-Poedit-Basepath: /home/maximilian/development/pygtkspellcheck/src\n"
-"X-Poedit-SearchPath-0: ./gtkspellcheck\n"
-
-#: gtkspellcheck/oxt_extract.py:242
-msgid "extension \"{}\" is not a valid ZIP file"
-msgstr ""
-
-#: gtkspellcheck/oxt_extract.py:248
-msgid "extension \"{}\" has no valid XML dictionary registry"
-msgstr ""
-
-#: gtkspellcheck/oxt_extract.py:268
-msgid "unable to move extension, file with same name exists within move_path"
-msgstr ""
-
-#: gtkspellcheck/oxt_extract.py:276
-msgid "unable to move extension, move_path is not a directory"
-msgstr ""
-
-#: gtkspellcheck/spellcheck.py:467
-msgid "(no suggestions)"
-msgstr "(sin sugerencias)"
-
-#: gtkspellcheck/spellcheck.py:489 gtkspellcheck/spellcheck.py:492
-msgid "Add \"{}\" to Dictionary"
-msgstr "Agregar \"{}\" al Diccionario"
-
-#: gtkspellcheck/spellcheck.py:496 gtkspellcheck/spellcheck.py:498
-msgid "Ignore All"
-msgstr "Ignorar Todos"
-
-#: gtkspellcheck/spellcheck.py:513 gtkspellcheck/spellcheck.py:515
-msgid "Languages"
-msgstr "Idiomas"
-
-#: gtkspellcheck/spellcheck.py:527 gtkspellcheck/spellcheck.py:530
-msgid "Suggestions"
-msgstr "Sugerencias"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c3e87f4
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,46 @@
+[tool.poetry]
+name = "pygtkspellcheck"
+version = "5.0.1"
+description = "A simple but quite powerful spellchecking library for GTK written in pure Python."
+authors = ["Maximilian Köhl <mail@koehlma.de>"]
+license = "GPL-3.0-or-later"
+readme = "README.md"
+repository = "https://github.com/koehlma/pygtkspellcheck.git"
+homepage = "https://github.com/koehlma/pygtkspellcheck"
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Environment :: X11 Applications :: Gnome",
+    "Environment :: X11 Applications :: GTK",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
+    "Operating System :: MacOS",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Programming Language :: Python :: 3",
+    "Topic :: Software Development :: Localization"
+]
+packages = [
+    { include = "gtkspellcheck", from = "src" }
+]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+pyenchant = "^3.0"
+PyGObject = "^3.42.1"
+sphinx = { version = "^4.5.0", optional = true }
+myst-parser = { version = "^0.18.0", optional = true }
+
+[tool.poetry.dev-dependencies]
+black = "^22.3.0"
+flake8 = "*"
+flake8-bugbear = "*"
+pep8-naming = "*"
+mypy = "*"
+sphinx-rtd-theme = "^1.0.0"
+
+[tool.poetry.extras]
+docs = ["sphinx", "myst-parser"]
+
+[build-system]
+requires = ["poetry_core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 861a9f5..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[egg_info]
-tag_build = 
-tag_date = 0
-tag_svn_revision = 0
-
diff --git a/setup.py b/setup.py
index d7eecc6..6840681 100644
--- a/setup.py
+++ b/setup.py
@@ -1,110 +1,38 @@
-# -*- coding:utf-8 -*-
-#
-# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
-# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import print_function
-
-import distutils.cmd
-import distutils.command.install
-import distutils.command.install_data
-import os
-import sys
-
-try:
-    from setuptools import setup
-except ImportError:
-    from distutils.core import setup
-
-commands = {}
-try:
-    from sphinx.setup_command import BuildDoc
-    commands['build_sphinx'] = BuildDoc
-except ImportError:
-    print('build_sphinx command is unavailable, please install Sphinx to solve this')
-
-__path__ = os.path.dirname(__file__)
-
-sys.path.insert(0, os.path.join(__path__, 'src'))
-
-sys.modules['gtk'] = None
-import gtkspellcheck
-    
-if len(sys.argv) > 1 and sys.argv[1] == 'register':
-    with open(os.path.join(__path__, 'doc', 'pypi', 'page.rst'), 'rb') as _pypi:
-        gtkspellcheck.__desc_long__ = _pypi.read().decode('utf-8')
-    print('pypi registration: override `long_description`')
-
-class InstallLocale(distutils.command.install_data.install_data):
-    def run(self):
-        locale_name = 'py{}gtkspellcheck.mo'.format(sys.version_info.major)
-        base = os.path.join(self.install_dir, 'share', 'locale')
-        self.mkpath(base)
-        for lang in os.listdir(os.path.join(__path__, 'locale')):
-            path = os.path.join(base, lang, 'LC_MESSAGES')
-            self.mkpath(path)
-            self.copy_file(os.path.join(__path__, 'locale', lang,
-                                        'pygtkspellcheck.mo'),
-                           os.path.join(path, locale_name))
-            
-commands['install_locale'] = InstallLocale
-distutils.command.install.install.sub_commands.append(('install_locale',
-                                                       lambda self: True))
-
-data_files = []
-if len(sys.argv) > 1 and sys.argv[1] == 'bdist_wininst':
-    windows_locale = os.path.join('dist', 'windows', 'locale')
-    for lang in os.listdir(windows_locale):
-        data_files.append((os.path.join('share', 'locale', lang, 'LC_MESSAGES'),
-                           [os.path.join(windows_locale, lang, 'LC_MESSAGES', message_file)
-                            for message_file in os.listdir(os.path.join(windows_locale, lang, 'LC_MESSAGES'))
-                            if message_file.endswith('.mo')]))
-    print('windows bdist_wininst include iso message files')
-
-py_modules = []
-gtkspell = os.getenv('GTKSPELL')
-if sys.version_info.major == 2 and gtkspell is not None and gtkspell.lower() == 'true':
-    py_modules.append('gtkspell')
-
-setup(name=gtkspellcheck.__short_name__,
-      version=gtkspellcheck.__version__,
-      description=gtkspellcheck.__desc_short__,
-      long_description=gtkspellcheck.__desc_long__,
-      author=gtkspellcheck.__authors__,
-      author_email=gtkspellcheck.__emails__,
-      url=gtkspellcheck.__website__,
-      download_url=gtkspellcheck.__download_url__,
-      license='GPLv3+',
-      py_modules=py_modules,
-      packages=['gtkspellcheck', 'pylocales'],
-      package_dir={'': 'src'},
-      package_data={'pylocales' : ['locales.db']},
-      data_files=data_files,
-      install_requires=['pyenchant'],
-      extras_require={
-          'building the documentation': ['sphinx']
-      },
-      classifiers=['Development Status :: 5 - Production/Stable',
-                   'Environment :: X11 Applications :: Gnome',
-                   'Intended Audience :: Developers',
-                   'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
-                   'Operating System :: MacOS :: MacOS X',
-                   'Operating System :: Microsoft :: Windows',
-                   'Operating System :: POSIX',
-                   'Programming Language :: Python :: 2',
-                   'Programming Language :: Python :: 3',
-                   'Topic :: Software Development :: Localization'],
-      cmdclass=commands)
+# -*- coding: utf-8 -*-
+from setuptools import setup
+
+package_dir = \
+{'': 'src'}
+
+packages = \
+['gtkspellcheck', 'gtkspellcheck._pylocales']
+
+package_data = \
+{'': ['*']}
+
+install_requires = \
+['PyGObject>=3.42.1,<4.0.0', 'pyenchant>=3.0,<4.0']
+
+extras_require = \
+{'docs': ['sphinx>=4.5.0,<5.0.0', 'myst-parser>=0.18.0,<0.19.0']}
+
+setup_kwargs = {
+    'name': 'pygtkspellcheck',
+    'version': '5.0.1',
+    'description': 'A simple but quite powerful spellchecking library for GTK written in pure Python.',
+    'long_description': '# Python GTK Spellcheck\n\n[![PyPi Project Page](https://img.shields.io/pypi/v/pygtkspellcheck.svg?&label=latest%20version)](https://pypi.python.org/pypi/pygtkspellcheck)\n[![Documentation](https://readthedocs.org/projects/pygtkspellcheck/badge/?version=latest)](https://pygtkspellcheck.readthedocs.org/en/latest/)\n\nPython GTK Spellcheck is a simple but quite powerful spellchecking library for GTK written in pure Python. It\'s spellchecking component is based on [Enchant](http://www.abisource.com/projects/enchant/) and it supports both GTK 3 and 4 via [PyGObject](https://live.gnome.org/PyGObject/).\n\n**⚡️ News:** Thanks to [@cheywood](https://github.com/cheywood), Python GTK Spellcheck now supports GTK 4! 🎉\n\n**🟢 Status:** This project is mature, actively maintained, and open to contributions and co-maintainership.\n\n\n## ✨ Features\n\n- **spellchecking** based on [Enchant](http://www.abisource.com/projects/enchant/) for `GtkTextView`\n- support for word, line, and multiline **ignore regular expressions**\n- support for both **GTK 3 and 4** via [PyGObject](https://live.gnome.org/PyGObject/) for Python 3\n- configurable extra word characters such as `\'`\n- localized names of the available languages based on [ISO-Codes](http://pkg-isocodes.alioth.debian.org/)\n- support for custom ignore tags and hot swap of `GtkTextBuffer`\n- support for Hunspell (LibreOffice) and Aspell (GNU) dictionaries\n\n<p align="center">\n  <img src="https://raw.githubusercontent.com/koehlma/pygtkspellcheck/master/docs/screenshots/screenshot.png" alt="Screenshot" />\n</p>\n\n\n## 🚀 Getting Started\n\nPython GTK Spellcheck is available from the [Python Package Index](https://pypi.python.org/pypi/pygtkspellcheck):\n```sh\npip install pygtkspellcheck\n```\nDepending on your distribution, you may also find Python GTK Spellcheck in your package manager.\nFor instance, on Debian you may want to install the [`python3-gtkspellcheck`](https://packages.debian.org/bullseye/python3-gtkspellcheck) package.\n\n\n## 🥳 Showcase\n\nOver time, several projects have used Python GTK Spellcheck or are still using it. Among those are:\n\n- [Nested Editor](http://nestededitor.sourceforge.net/about.html): “Specialized editor for structured documents.”\n- [Cherry Tree](http://www.giuspen.com/cherrytree/): “A hierarchical note taking application, […].”\n- [Zim](http://zim-wiki.org/): “Zim is a graphical text editor used to maintain a collection of wiki pages.”\n- [REMARKABLE](http://remarkableapp.github.io/): “The best markdown editor for Linux and Windows.”\n- [RedNotebook](http://rednotebook.sourceforge.net/): “RedNotebook is a modern journal.”\n- [Reportbug](https://packages.debian.org/stretch/reportbug): “Reports bugs in the Debian distribution.”\n- [UberWriter](http://uberwriter.wolfvollprecht.de/): “UberWriter is a writing application for markdown.”\n- [Gourmet](https://github.com/thinkle/gourmet): “Gourmet Recipe Manager is a manager, editor, and organizer for recipes.“\n\n\n## 🔖 Versions\n\nVersion numbers follow [Semantic Versioning](http://semver.org/). However, the update from 3 to 4 pertains only API incompatible changes in `oxt_extract` and not the spellchecking component. The update from 4 to 5 removed support for Python 2, GTK 2, `pylocales`, and the `oxt_extract` API. Otherwise, the API is still compatible with version 3.\n\n\n## 📚 Documentation\n\nThe documentation is available at [Read the Docs](http://pygtkspellcheck.readthedocs.org/).\n\n\n## 🏗 Contributing\n\nWe welcome all kinds of contributions! ❤️\n\nFor minor changes and bug fixes feel free to simply open a pull request. For major changes impacting the overall design of Python GTK Spellcheck, please first [start a discussion](https://github.com/koehlma/pygtkspellcheck/discussions/new?category=ideas) outlining your idea.\n\nBy submitting a PR, you agree to license your contributions under “GPLv3 or later”.\n',
+    'author': 'Maximilian Köhl',
+    'author_email': 'mail@koehlma.de',
+    'maintainer': None,
+    'maintainer_email': None,
+    'url': 'https://github.com/koehlma/pygtkspellcheck',
+    'package_dir': package_dir,
+    'packages': packages,
+    'package_data': package_data,
+    'install_requires': install_requires,
+    'extras_require': extras_require,
+    'python_requires': '>=3.7,<4.0',
+}
+
+
+setup(**setup_kwargs)
diff --git a/src/gtkspellcheck/__init__.py b/src/gtkspellcheck/__init__.py
index 3b77d17..9624946 100644
--- a/src/gtkspellcheck/__init__.py
+++ b/src/gtkspellcheck/__init__.py
@@ -18,36 +18,43 @@
 
 from __future__ import unicode_literals
 
-__version__ = '4.0.5'
-__project__ = 'Python GTK Spellcheck'
-__short_name__ = 'pygtkspellcheck'
-__authors__ = 'Maximilian Köhl & Carlos Jenkins'
-__emails__ = 'linuxmaxi@googlemail.com & carlos@jenkins.co.cr'
-__website__ = 'http://koehlma.github.com/projects/pygtkspellcheck.html'
-__download_url__ = 'https://github.com/koehlma/pygtkspellcheck/tarball/master'
-__source__ = 'https://github.com/koehlma/pygtkspellcheck/'
-__vcs__ = 'git://github.com/koehlma/pygtkspellcheck.git'
-__copyright__ = '2012, Maximilian Köhl & Carlos Jenkins'
-__desc_short__ = ('a simple but quite powerful Python spell checking library '
-                  'for GtkTextViews based on Enchant')
-__desc_long__ = ('A simple but quite powerful spellchecking library written in '
-                 'pure Python for Gtk based on Enchant. It supports PyGObject '
-                 'as well as PyGtk for Python 2 and 3 with automatic switching '
-                 'and binding detection. For automatic translation of the user '
-                 'interface it can use Gedit’s translation files.')
+__version__ = "4.0.5"
+__project__ = "Python GTK Spellcheck"
+__short_name__ = "pygtkspellcheck"
+__authors__ = "The Python GTK Spellcheck Authors"
+__emails__ = "linuxmaxi@googlemail.com & carlos@jenkins.co.cr"
+__website__ = "http://koehlma.github.com/projects/pygtkspellcheck.html"
+__download_url__ = "https://github.com/koehlma/pygtkspellcheck/tarball/master"
+__source__ = "https://github.com/koehlma/pygtkspellcheck/"
+__vcs__ = "git://github.com/koehlma/pygtkspellcheck.git"
+__copyright__ = "2012-2022, The Python GTK Spellcheck Authors"
+__desc_short__ = """
+A Python spell-checking library for GtkTextViews based on Enchant
+"""
+__desc_long__ = """
+A simple but quite powerful spellchecking library written in pure Python for
+Gtk based on Enchant. It supports PyGObject as well as PyGtk for Python 2 and
+3 with automatic switching and binding detection. For automatic translation of
+the user interface it can use Gedit’s translation files.
+"""
+__metadata__ = {
+    "__version__": __version__,
+    "__project__": __project__,
+    "__short_name__": __short_name__,
+    "__authors__": __authors__,
+    "__emails__": __emails__,
+    "__website__": __website__,
+    "__download_url__": __download_url__,
+    "__source__": __source__,
+    "__vcs__": __vcs__,
+    "__copyright__": __copyright__,
+    "__desc_short__": __desc_short__,
+    "__desc_long__": __desc_long__,
+}
 
-__metadata__ = {'__version__' : __version__,
-                '__project__' : __project__,
-                '__short_name__' : __short_name__,
-                '__authors__' : __authors__,
-                '__emails__' : __emails__,
-                '__website__' : __website__,
-                '__download_url__' : __download_url__,
-                '__source__' : __source__,
-                '__vcs__' : __vcs__,
-                '__copyright__' : __copyright__,
-                '__desc_short__' : __desc_short__,
-                '__desc_long__' : __desc_long__}
+from gtkspellcheck.spellcheck import (
+    SpellChecker,
+    NoDictionariesFound,
+)
 
-from gtkspellcheck.spellcheck import (SpellChecker, NoDictionariesFound,
-                                      NoGtkBindingFound)
+__all__ = ["SpellChecker", "NoDictionariesFound"]
diff --git a/src/gtkspellcheck/oxt_extract.py b/src/gtkspellcheck/_oxt_extract.py
similarity index 59%
rename from src/gtkspellcheck/oxt_extract.py
rename to src/gtkspellcheck/_oxt_extract.py
index ce1c45a..9661918 100644
--- a/src/gtkspellcheck/oxt_extract.py
+++ b/src/gtkspellcheck/_oxt_extract.py
@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-This module extracts the .dic and .aff (Hunspell) dictionaries from any given 
+This module extracts the .dic and .aff (Hunspell) dictionaries from any given
 .oxt extension.
 
 Extensions could be found at:
@@ -37,30 +37,40 @@ import xml.parsers.expat
 import zipfile
 
 # enable deprecation warnings
-warnings.simplefilter('always', DeprecationWarning)
+warnings.simplefilter("always", DeprecationWarning)
 
 # public objects
-__all__ = ['extract_oxt', 'batch_extract', 'BadXml', 'BadExtensionFile',
-           'ExtractPathIsNoDirectory', 'BATCH_SUCCESS', 'BATCH_ERROR',
-           'BATCH_WARNING']
+__all__ = [
+    "extract",
+    "batch_extract",
+    "BadXml",
+    "BadExtensionFile",
+    "ExtractPathIsNoDirectory",
+    "BATCH_SUCCESS",
+    "BATCH_ERROR",
+    "BATCH_WARNING",
+]
 
 # logger
 logger = logging.getLogger(__name__)
 
 # translation
-locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major)
+locale_name = "py{}gtkspellcheck".format(sys.version_info.major)
 _ = gettext.translation(locale_name, fallback=True).gettext
 
+
 class BadXml(Exception):
     """
     The XML dictionary registry is not valid XML.
     """
-    
+
+
 class BadExtensionFile(Exception):
     """
     The extension has a wrong file format, should be a ZIP file.
     """
 
+
 class ExtractPathIsNoDirectory(Exception):
     """
     The given `extract_path` is no directory.
@@ -69,44 +79,47 @@ class ExtractPathIsNoDirectory(Exception):
 
 def find_dictionaries(registry):
     def oor_name(name, element):
-        return element.attributes['oor:name'].value.lower() == name
-    
+        return element.attributes["oor:name"].value.lower() == name
+
     def get_property(name, properties):
-        property = list(filter(functools.partial(oor_name, name),
-                               properties))
+        property = list(filter(functools.partial(oor_name, name), properties))
         if property:
-            return property[0].getElementsByTagName('value')[0]
-    
+            return property[0].getElementsByTagName("value")[0]
+
     result = []
-    
+
     # find all "node" elements which have "dictionaries" as "oor:name" attribute
-    for dictionaries in filter(functools.partial(oor_name, 'dictionaries'),
-                               registry.getElementsByTagName('node')):
+    for dictionaries in filter(
+        functools.partial(oor_name, "dictionaries"),
+        registry.getElementsByTagName("node"),
+    ):
         # for all "node" elements in this dictionary nodes
-        for dictionary in dictionaries.getElementsByTagName('node'):
+        for dictionary in dictionaries.getElementsByTagName("node"):
             # get all "prop" elements
-            properties = dictionary.getElementsByTagName('prop')
+            properties = dictionary.getElementsByTagName("prop")
             # get the format property as text
-            format = get_property('format', properties).firstChild.data.strip()
-            if format and format == 'DICT_SPELL':
+            format = get_property("format", properties).firstChild.data.strip()
+            if format and format == "DICT_SPELL":
                 # find the locations property
-                locations = get_property('locations', properties)
+                locations = get_property("locations", properties)
                 # if the location property is text:
                 # %origin%/dictionary.aff %origin%/dictionary.dic
                 if locations.firstChild.nodeType == xml.dom.Node.TEXT_NODE:
                     locations = locations.firstChild.data
-                    locations = locations.replace('%origin%/', '').strip()
+                    locations = locations.replace("%origin%/", "").strip()
                     result.append(locations.split())
                 # otherwise:
                 # <i>%origin%/dictionary.aff</i> <i>%origin%/dictionary.dic</i>
                 else:
-                    locations = [item.firshChild.data.replace('%origin%/', '') \
-                                 .strip() for item in
-                                 locations.getElementsByTagName('it')]
+                    locations = [
+                        item.firshChild.data.replace("%origin%/", "").strip()
+                        for item in locations.getElementsByTagName("it")
+                    ]
                     result.append(locations)
-    
+
     return result
 
+
 def extract(filename, target, override=False):
     """
     Extract Hunspell dictionaries out of LibreOffice ``.oxt`` extensions.
@@ -124,56 +137,67 @@ def extract(filename, target, override=False):
         http://extensions.services.openoffice.org/dictionary
     """
     # TODO 5.0: remove this function
-    warnings.warn(('call to deprecated function "{}", '
-                   'moved to separate package "oxt_extract", '
-                   'will be removed in pygtkspellcheck 5.0').format(extract.__name__),
-                  category=DeprecationWarning)
+    warnings.warn(
+        (
+            'call to deprecated function "{}", '
+            'moved to separate package "oxt_extract", '
+            "will be removed in pygtkspellcheck 5.0"
+        ).format(extract.__name__),
+        category=DeprecationWarning,
+    )
     try:
-        with zipfile.ZipFile(filename, 'r') as extension:
+        with zipfile.ZipFile(filename, "r") as extension:
             files = extension.namelist()
-            
-            registry = 'dictionaries.xcu'
-            if not registry in files:
+
+            registry = "dictionaries.xcu"
+            if registry not in files:
                 for filename in files:
                     if filename.lower().endswith(registry):
                         registry = filename
-                    
+
             if registry in files:
                 registry = xml.dom.minidom.parse(extension.open(registry))
                 dictionaries = find_dictionaries(registry)
                 extracted = []
                 for dictionary in dictionaries:
                     for filename in dictionary:
-                        dict_file = os.path.join(target,
-                                                 os.path.basename(filename))
-                        if (not os.path.exists(dict_file) 
-                                or (override and os.path.isfile(dict_file))):
+                        dict_file = os.path.join(target, os.path.basename(filename))
+                        if not os.path.exists(dict_file) or (
+                            override and os.path.isfile(dict_file)
+                        ):
                             if filename in files:
-                                with open(dict_file, 'wb') as _target:
-                                    with extension.open(filename, 'r') as _source:
+                                with open(dict_file, "wb") as _target:
+                                    with extension.open(filename, "r") as _source:
                                         extracted.append(os.path.basename(filename))
                                         _target.write(_source.read())
                             else:
-                                logger.warning('dictionary exists in registry '
-                                               'but not in the extension zip')
+                                logger.warning(
+                                    "dictionary exists in registry "
+                                    "but not in the extension zip"
+                                )
                         else:
-                            logging.warning(('dictionary file "{}" already exists '
-                                             'and not overriding it'
-                                             ).format(dict_file))
+                            logging.warning(
+                                (
+                                    'dictionary file "{}" already exists '
+                                    "and not overriding it"
+                                ).format(dict_file)
+                            )
                 return extracted
     except zipfile.BadZipfile:
-        raise BadExtensionFile('extension is not a valid ZIP file')
+        raise BadExtensionFile("extension is not a valid ZIP file")
     except xml.parsers.expat.ExpatError:
-        raise BadXml('dictionary registry is not valid XML')
+        raise BadXml("dictionary registry is not valid XML")
+
+
+BATCH_SUCCESS = "success"
+BATCH_ERROR = "error"
+BATCH_WARNING = "warning"
 
-BATCH_SUCCESS = 'success'
-BATCH_ERROR = 'error'
-BATCH_WARNING = 'warning'
 
 def batch_extract(oxt_path, extract_path, override=False, move_path=None):
     """
     Uncompress, read and install LibreOffice ``.oxt`` dictionaries extensions.
-    
+
     :param oxt_path: path to a directory containing the ``.oxt`` extensions
     :param extract_path: path to extract Hunspell dictionaries files to
     :param override: override already existing files
@@ -183,30 +207,30 @@ def batch_extract(oxt_path, extract_path, override=False, move_path=None):
         would be :const:`BATCH_SUCCESS` for success, :const:`BATCH_ERROR` if
         some error happened or :const:`BATCH_WARNING` which contain some warning
         messages instead of errors
-    
+
     This function extracts the Hunspell dictionaries (``.dic`` and ``.aff``
     files) from all the ``.oxt`` extensions found on ``oxt_path`` directory to
     the ``extract_path`` directory.
-    
+
     Extensions could be found at:
-    
+
         http://extensions.services.openoffice.org/dictionary
-    
+
     In detail, this functions does the following:
-    
+
     1. find all the ``.oxt`` extension files within ``oxt_path``
     2. open (unzip) each extension
     3. find the dictionary definition file within (*dictionaries.xcu*)
     4. parse the dictionary definition file and locate the dictionaries files
     5. uncompress those files to ``extract_path``
-    
-    
+
+
     By default file overriding is disabled, set ``override`` parameter to True
     if you want to enable it. As additional option, each processed extension can
     be moved to ``move_path``.
-    
+
     Example::
-    
+
         for result, name, error, dictionaries, message in oxt_extract.batch_extract(...):
             if result == oxt_extract.BATCH_SUCCESS:
                 print('successfully extracted extension "{}"'.format(name))
@@ -218,53 +242,74 @@ def batch_extract(oxt_path, extract_path, override=False, move_path=None):
                 print('warning during processing extension "{}"'.format(name))
                 print(message)
                 print(error)
-        
+
     """
 
     # TODO 5.0: remove this function
-    warnings.warn(('call to deprecated function "{}", '
-                   'moved to separate package "oxt_extract", '
-                   'will be removed in pygtkspellcheck 5.0').format(extract.__name__),
-                  category=DeprecationWarning)
+    warnings.warn(
+        (
+            'call to deprecated function "{}", '
+            'moved to separate package "oxt_extract", '
+            "will be removed in pygtkspellcheck 5.0"
+        ).format(extract.__name__),
+        category=DeprecationWarning,
+    )
 
     # get the real, absolute and normalized path
     oxt_path = os.path.normpath(os.path.abspath(os.path.realpath(oxt_path)))
-    
+
     # check that the input directory exists
     if not os.path.isdir(oxt_path):
         return
-        
+
     # create extract directory if not exists
     if not os.path.exists(extract_path):
         os.makedirs(extract_path)
 
     # check that the extract path is a directory
     if not os.path.isdir(extract_path):
-        raise ExtractPathIsNoDirectory('extract path is not a valid directory')
-    
+        raise ExtractPathIsNoDirectory("extract path is not a valid directory")
+
     # get all .oxt extension at given path
-    oxt_files = [extension for extension in os.listdir(oxt_path)
-                 if extension.lower().endswith('.oxt')]
-    
+    oxt_files = [
+        extension
+        for extension in os.listdir(oxt_path)
+        if extension.lower().endswith(".oxt")
+    ]
+
     for extension_name in oxt_files:
         extension_path = os.path.join(oxt_path, extension_name)
-        
+
         try:
             dictionaries = extract(extension_path, extract_path, override)
-            yield BATCH_SUCCESS, extension_name, None, dictionaries, ''
+            yield BATCH_SUCCESS, extension_name, None, dictionaries, ""
         except BadExtensionFile as error:
-            logger.error(('extension "{}" is not a valid ZIP file'
-                          ).format(extension_name))
-            yield (BATCH_ERROR, extension_name, error, [],
-                   _('extension "{}" is not a valid ZIP file'
-                     ).format(extension_name))
+            logger.error(
+                ('extension "{}" is not a valid ZIP file').format(extension_name)
+            )
+            yield (
+                BATCH_ERROR,
+                extension_name,
+                error,
+                [],
+                _('extension "{}" is not a valid ZIP file').format(extension_name),
+            )
         except BadXml as error:
-            logger.error(('extension "{}" has no valid XML dictionary registry'
-                          ).format(extension_name))
-            yield (BATCH_ERROR, extension_name, error, [],
-                   _('extension "{}" has no valid XML dictionary registry'
-                     ).format(extension_name)) 
-        
+            logger.error(
+                ('extension "{}" has no valid XML dictionary registry').format(
+                    extension_name
+                )
+            )
+            yield (
+                BATCH_ERROR,
+                extension_name,
+                error,
+                [],
+                _('extension "{}" has no valid XML dictionary registry').format(
+                    extension_name
+                ),
+            )
+
         # move the extension after processing if user requires it
         if move_path is not None:
             # create move path if it doesn't exists
@@ -273,22 +318,39 @@ def batch_extract(oxt_path, extract_path, override=False, move_path=None):
             # move to the given path only if it is a directory and target
             # doesn't exists
             if os.path.isdir(move_path):
-                if (not os.path.exists(os.path.join(move_path, extension_name))
-                        or override):
+                if (
+                    not os.path.exists(os.path.join(move_path, extension_name))
+                    or override
+                ):
                     shutil.move(extension_path, move_path)
                 else:
-                    logger.warning(('unable to move extension, file with same '
-                                    'name exists within move_path'))
-                    yield (BATCH_WARNING, extension_name,
-                           ('unable to move extension, file with same name '
-                            'exists within move_path'), [],
-                           _('unable to move extension, file with same name '
-                             'exists within move_path'))
+                    logger.warning(
+                        (
+                            "unable to move extension, file with same "
+                            "name exists within move_path"
+                        )
+                    )
+                    yield (
+                        BATCH_WARNING,
+                        extension_name,
+                        (
+                            "unable to move extension, file with same name "
+                            "exists within move_path"
+                        ),
+                        [],
+                        _(
+                            "unable to move extension, file with same name "
+                            "exists within move_path"
+                        ),
+                    )
             else:
-                logger.warning(('unable to move extension, move_path is not a '
-                                'directory'))
-                yield (BATCH_WARNING, extension_name,
-                       ('unable to move extension, move_path is not a '
-                        'directory'), [],
-                       _('unable to move extension, move_path is not a '
-                         'directory'))                
\ No newline at end of file
+                logger.warning(
+                    ("unable to move extension, move_path is not a " "directory")
+                )
+                yield (
+                    BATCH_WARNING,
+                    extension_name,
+                    ("unable to move extension, move_path is not a " "directory"),
+                    [],
+                    _("unable to move extension, move_path is not a " "directory"),
+                )
diff --git a/src/gtkspellcheck/_pylocales/__init__.py b/src/gtkspellcheck/_pylocales/__init__.py
new file mode 100644
index 0000000..16fcb44
--- /dev/null
+++ b/src/gtkspellcheck/_pylocales/__init__.py
@@ -0,0 +1,60 @@
+# -*- coding:utf-8 -*-
+#
+# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+
+__version__ = "1.2"
+__project__ = "Python Locales"
+__short_name__ = "pylocales"
+__authors__ = "Maximilian Köhl"
+__emails__ = "linuxmaxi@googlemail.com"
+__website__ = "http://koehlma.github.com/projects/pygtkspellcheck.html"
+__source__ = "https://github.com/koehlma/pygtkspellcheck/"
+__vcs__ = "git://github.com/koehlma/pygtkspellcheck.git"
+__copyright__ = "2012, Maximilian Köhl"
+__desc_short__ = "query the ISO 639/3166 database about a country or a language."
+__desc_long__ = (
+    "Query the ISO 639/3166 database about a country or a"
+    "language. The locales database contains ISO 639 language"
+    "definitions and ISO 3166 country definitions. This package"
+    "provides translation for country and language names if"
+    "the iso-code messages are installed on your system."
+)
+
+__metadata__ = {
+    "__version__": __version__,
+    "__project__": __project__,
+    "__short_name__": __short_name__,
+    "__authors__": __authors__,
+    "__emails__": __emails__,
+    "__website__": __website__,
+    "__source__": __source__,
+    "__vcs__": __vcs__,
+    "__copyright__": __copyright__,
+    "__desc_short__": __desc_short__,
+    "__desc_long__": __desc_long__,
+}
+
+from .locales import (
+    Country,
+    Language,
+    LanguageNotFound,
+    CountryNotFound,
+    code_to_name,
+)
+
+__all__ = ["Country", "Language", "LanguageNotFound", "CountryNotFound", "code_to_name"]
diff --git a/src/pylocales/locales.db b/src/gtkspellcheck/_pylocales/locales.db
similarity index 100%
rename from src/pylocales/locales.db
rename to src/gtkspellcheck/_pylocales/locales.db
diff --git a/src/pylocales/locales.py b/src/gtkspellcheck/_pylocales/locales.py
similarity index 67%
rename from src/pylocales/locales.py
rename to src/gtkspellcheck/_pylocales/locales.py
index 9d4f10f..07749fc 100644
--- a/src/pylocales/locales.py
+++ b/src/gtkspellcheck/_pylocales/locales.py
@@ -30,119 +30,124 @@ import sqlite3
 import sys
 
 # public objects
-__all__ = ['Country', 'Language', 'LanguageNotFound',
-           'CountryNotFound', 'code_to_name']
+__all__ = ["Country", "Language", "LanguageNotFound", "CountryNotFound", "code_to_name"]
 
 # translation
-_translator_language = gettext.translation('iso_639', fallback=True).gettext
-_translator_country = gettext.translation('iso_3166', fallback=True).gettext
+_translator_language = gettext.translation("iso_639", fallback=True).gettext
+_translator_country = gettext.translation("iso_3166", fallback=True).gettext
 
 # Decides where the database is located. If an application provides an
 # os.path.get_module_path monkey patch to determine the path where the module
 # is located it uses this. If not it searches in the directory of this source
 # code file.
 __path__ = None
-if hasattr(os.path, 'get_module_path'):
+if hasattr(os.path, "get_module_path"):
     __path__ = os.path.get_module_path(__file__)
-    if not os.path.isfile(os.path.join(__path__, 'locales.db')):
+    if not os.path.isfile(os.path.join(__path__, "locales.db")):
         __path__ = None
 if __path__ is None:
-    frozen = getattr(sys, 'frozen', None)
-    if frozen in ('dll', 'console_exe', 'windows_exe'):
+    frozen = getattr(sys, "frozen", None)
+    if frozen in ("dll", "console_exe", "windows_exe"):
         __path__ = os.path.abspath(os.path.dirname(sys.executable))
-    elif frozen == 'macosx_app':
-        __path__ = os.path.abspath(os.environ['RESOURCEPATH'])
+    elif frozen == "macosx_app":
+        __path__ = os.path.abspath(os.environ["RESOURCEPATH"])
     elif frozen is True:
         # Handle executables produced by PyInstaller.
         __path__ = sys._MEIPASS
     else:
         __path__ = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
-    
+
 
 # loading the database
-_database = sqlite3.connect(os.path.join(__path__, 'locales.db'))
+_database = sqlite3.connect(os.path.join(__path__, "locales.db"))
 
 logger = logging.getLogger(__name__)
 
+
 class LanguageNotFound(Exception):
     """
     The specified language wasn't found in the database.
     """
-    
+
+
 class CountryNotFound(Exception):
     """
     The specified country wasn't found in the database.
     """
 
+
 class Country(object):
     def __init__(self, rowid):
-        country = _database.execute('SELECT * FROM countries WHERE rowid == ?',
-                                    (rowid,)).fetchone()
+        country = _database.execute(
+            "SELECT * FROM countries WHERE rowid == ?", (rowid,)
+        ).fetchone()
         self.name = country[0]
         self.official_name = country[1]
         self.alpha_2 = country[2]
         self.alpha_3 = country[3]
         self.numeric = country[4]
         self.translation = _translator_country(self.name)
-        
+
     @classmethod
     def get_country(cls, code, codec):
         country = _database.execute(
-            'SELECT rowid FROM countries WHERE %s == ?' % (codec),
-            (code,)).fetchone()
+            "SELECT rowid FROM countries WHERE %s == ?" % (codec), (code,)
+        ).fetchone()
         if country:
             return cls(country[0])
-        raise CountryNotFound('code: %s, codec: %s' % (code, codec))
-    
+        raise CountryNotFound("code: %s, codec: %s" % (code, codec))
+
     @classmethod
     def by_alpha_2(cls, code):
-        return Country.get_country(code, 'alpha_2')
-    
+        return Country.get_country(code, "alpha_2")
+
     @classmethod
     def by_alpha_3(cls, code):
-        return Country.get_country(code, 'alpha_3')
-    
+        return Country.get_country(code, "alpha_3")
+
     @classmethod
     def by_numeric(cls, code):
-        return Country.get_country(code, 'numeric')
-   
+        return Country.get_country(code, "numeric")
+
+
 class Language(object):
     def __init__(self, rowid):
-        language = _database.execute('SELECT * FROM languages WHERE rowid == ?',
-                                     (rowid,)).fetchone()
+        language = _database.execute(
+            "SELECT * FROM languages WHERE rowid == ?", (rowid,)
+        ).fetchone()
         self.name = language[0]
         self.iso_639_2B = language[1]
         self.iso_639_2T = language[2]
         self.iso_639_1 = language[3]
         self.translation = _translator_language(self.name)
-        
+
     @classmethod
     def get_language(cls, code, codec):
         language = _database.execute(
-            'SELECT rowid FROM languages WHERE %s == ?' % (codec),
-            (code,)).fetchone()
+            "SELECT rowid FROM languages WHERE %s == ?" % (codec), (code,)
+        ).fetchone()
         if language:
             return cls(language[0])
-        raise LanguageNotFound('code: %s, codec: %s' % (code, codec))
-        
+        raise LanguageNotFound("code: %s, codec: %s" % (code, codec))
+
     @classmethod
-    def by_iso_639_2B(cls, code):
-        return Language.get_language(code, 'iso_639_2B')
-    
+    def by_iso_639_2b(cls, code):
+        return Language.get_language(code, "iso_639_2B")
+
     @classmethod
-    def by_iso_639_2T(cls, code):
-        return Language.get_language(code, 'iso_639_2T')
-    
+    def by_iso_639_2t(cls, code):
+        return Language.get_language(code, "iso_639_2T")
+
     @classmethod
     def by_iso_639_1(cls, code):
-        return Language.get_language(code, 'iso_639_1')
+        return Language.get_language(code, "iso_639_1")
 
 
-def code_to_name(code, separator='_'):
-    """  
+def code_to_name(code, separator="_"):
+    """
     Get the human readable and translated name of a language based on it's code.
-    
-    :param code: the code of the language (e.g. de_DE, en_US) 
+
+    :param code: the code of the language (e.g. de_DE, en_US)
     :param target: separator used to separate language from country
     :rtype: human readable and translated language name
     """
@@ -151,6 +156,6 @@ def code_to_name(code, separator='_'):
     if len(code) > 1:
         lang = Language.by_iso_639_1(code[0]).translation
         country = Country.by_alpha_2(code[1]).translation
-        return '{} ({})'.format(lang, country)
+        return "{} ({})".format(lang, country)
     else:
         return Language.by_iso_639_1(code[0]).translation
diff --git a/src/gtkspellcheck/spellcheck.py b/src/gtkspellcheck/spellcheck.py
index ca1e4f8..1bf9a28 100644
--- a/src/gtkspellcheck/spellcheck.py
+++ b/src/gtkspellcheck/spellcheck.py
@@ -18,9 +18,8 @@
 
 """
 A simple but quite powerful spellchecking library written in pure Python for Gtk
-based on Enchant. It supports PyGObject as well as PyGtk for Python 2 and 3 with
-automatic switching and binding detection. For automatic translation of the user
-interface it can use Gedit’s translation files.
+based on Enchant. It supports both GTK 3 and 4 via PyGObject with Python 3. For
+automatic translation of the user interface it can use Gedit’s translation files.
 """
 
 import enchant
@@ -28,83 +27,71 @@ import gettext
 import logging
 import re
 import sys
+from collections import UserList
 
-from pylocales import code_to_name as _code_to_name
-from pylocales import LanguageNotFound, CountryNotFound
+from ._pylocales import code_to_name as _code_to_name
+from ._pylocales import LanguageNotFound, CountryNotFound
+
+from gi.repository import Gio, GLib, GObject
+
+# find any loaded gtk binding
+if "gi.repository.Gtk" in sys.modules:
+    Gtk = sys.modules["gi.repository.Gtk"]
+else:
+    import gi
+
+    gi.require_version("Gtk", "3.0")
+    from gi.repository import Gtk  # noqa: N813
+
+_IS_GTK3 = Gtk.MAJOR_VERSION < 4
 
 # public objects
-__all__ = ['SpellChecker', 'NoDictionariesFound', 'NoGtkBindingFound']
+__all__ = ["SpellChecker", "NoDictionariesFound"]
 
 # logger
 logger = logging.getLogger(__name__)
 
+
 class NoDictionariesFound(Exception):
     """
     There aren't any dictionaries installed on the current system so
     spellchecking could not work in any way.
     """
 
-class NoGtkBindingFound(Exception):
-    """
-    Could not find any loaded Gtk binding.
-    """
-
-if sys.version_info.major == 3:
-    _py3k = True
-else:
-    _py3k = False
-    
-if _py3k:
-    # there is only the gi binding for Python 3
-    from gi.repository import Gtk as gtk
-    _pygobject = True
-else:
-    # find any loaded gtk binding
-    if 'gi.repository.Gtk' in sys.modules:
-        gtk = sys.modules['gi.repository.Gtk']
-        _pygobject = True
-    elif 'gtk' in sys.modules:
-        gtk = sys.modules['gtk']
-        _pygobject = False
-    else:
-        raise NoGtkBindingFound('could not find any loaded Gtk binding')
-
-# select base list class
-try:
-    from collections import UserList
-    _list = UserList
-except ImportError:
-    _list = list
-
-
-
-# select base string
-if _py3k:
-    basestring = str
 
 # map between Gedit's translation and PyGtkSpellcheck's
-_GEDIT_MAP = {'Languages' : 'Languages',
-              'Ignore All' : 'Ignore _All',
-              'Suggestions' : 'Suggestions',
-              '(no suggestions)' : '(no suggested words)',
-              'Add "{}" to Dictionary' : 'Add w_ord',
-              'Unknown' : 'Unknown'}
+_GEDIT_MAP = {
+    "Languages": "Languages",
+    "Ignore All": "Ignore _All",
+    "Suggestions": "Suggestions",
+    "(no suggestions)": "(no suggested words)",
+    'Add "{}" to Dictionary': "Add w_ord",
+    "Unknown": "Unknown",
+}
+
+_BATCHING_THRESHOLD_CHARS = 1500
+_BATCH_SIZE_CHARS = 1000
 
 # translation
-if gettext.find('gedit'):
-    _gedit = gettext.translation('gedit', fallback=True).gettext
-    _ = lambda message: _gedit(_GEDIT_MAP[message]).replace('_', '')
+if gettext.find("gedit"):
+    _gedit = gettext.translation("gedit", fallback=True).gettext
+
+    def _(message):
+        return _gedit(_GEDIT_MAP[message]).replace("_", "")
+
 else:
-    locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major)
+    locale_name = "py{}gtkspellcheck".format(sys.version_info.major)
     _ = gettext.translation(locale_name, fallback=True).gettext
 
-def code_to_name(code, separator='_'):
+
+def code_to_name(code, separator="_"):
     try:
         return _code_to_name(code, separator)
     except (LanguageNotFound, CountryNotFound):
-        return '{} ({})'.format(_('Unknown'), code)
+        return "{} ({})".format(_("Unknown"), code)
+
 
-class SpellChecker(object):
+class SpellChecker(GObject.Object):
     """
     Main spellchecking class, everything important happens here.
 
@@ -127,38 +114,48 @@ class SpellChecker(object):
 
             :param language: language to check
     """
-    FILTER_WORD = 'word'
-    FILTER_LINE = 'line'
-    FILTER_TEXT = 'text'
 
-    DEFAULT_FILTERS = {FILTER_WORD : [r'[0-9.,]+'],
-                       FILTER_LINE : [(r'(https?|ftp|file):((//)|(\\\\))+[\w\d:'
-                                       r'#@%/;$()~_?+-=\\.&]+'),
-                                      r'[\w\d]+@[\w\d.]+'],
-                       FILTER_TEXT : []}
+    FILTER_WORD = "word"
+    FILTER_LINE = "line"
+    FILTER_TEXT = "text"
 
-    class _LanguageList(_list):
+    DEFAULT_FILTERS = {
+        FILTER_WORD: [r"[0-9.,]+"],
+        FILTER_LINE: [
+            (r"(https?|ftp|file):((//)|(\\\\))+[\w\d:" r"#@%/;$()~_?+-=\\.&]+"),
+            r"[\w\d]+@[\w\d.]+",
+        ],
+        FILTER_TEXT: [],
+    }
+
+    DEFAULT_EXTRA_CHARS = "'"
+
+    class _LanguageList(UserList):
         def __init__(self, *args, **kwargs):
-            if sys.version_info.major == 3:
-                super().__init__(*args, **kwargs)
-            else:
-                _list.__init__(self, *args, **kwargs)
+            super().__init__(*args, **kwargs)
             self.mapping = dict(self)
 
         @classmethod
         def from_broker(cls, broker):
-            return cls(sorted([(language, code_to_name(language))
-                               for language in broker.list_languages()],
-                              key=lambda language: language[1]))
+            return cls(
+                sorted(
+                    [
+                        (language, code_to_name(language))
+                        for language in broker.list_languages()
+                    ],
+                    key=lambda language: language[1],
+                )
+            )
 
         def exists(self, language):
             return language in self.mapping
 
-    class _Mark():
-        def __init__(self, buffer, name, start):
+    class _Mark:
+        def __init__(self, buffer, name, start, iter_worker):
             self._buffer = buffer
             self._name = name
             self._mark = self._buffer.create_mark(self._name, start, True)
+            self._iter_worker = iter_worker
 
         @property
         def iter(self):
@@ -166,63 +163,191 @@ class SpellChecker(object):
 
         @property
         def inside_word(self):
-            return self.iter.inside_word()
+            return self._iter_worker.inside_word(self.iter)
 
         @property
         def word(self):
             start = self.iter
-            if not start.starts_word():
-                start.backward_word_start()
+            if not self._iter_worker.starts_word(start):
+                self._iter_worker.backward_word_start(start)
             end = self.iter
-            if end.inside_word():
-                end.forward_word_end()
+            if self._iter_worker.inside_word(end):
+                self._iter_worker.forward_word_end(end)
             return start, end
 
         def move(self, location):
             self._buffer.move_mark(self._mark, location)
 
-    def __init__(self, view, language='en', prefix='gtkspellchecker',
-                 collapse=True, params={}):
+    class _IterWorker:
+        def __init__(self, extra_word_chars):
+            self._extra_word_chars = extra_word_chars
+
+        def is_extra_word_char(self, loc):
+            # Language extra chararacters should also be processed once Enchant's
+            # enchant_dict_get_extra_word_characters is exposed in PyEnchant
+
+            char = loc.get_char()
+            return char != "" and char in self._extra_word_chars
+
+        def inside_word(self, loc):
+            if loc.inside_word():
+                return True
+            elif self.starts_word(loc):
+                return True
+            elif loc.ends_word() and not self.ends_word(loc):
+                return True
+            else:
+                return False
+
+        def starts_word(self, loc):
+            if loc.starts_word():
+                if loc.is_start():
+                    return True
+                else:
+                    tmp = loc.copy()
+                    tmp.backward_char()
+                    return not self.is_extra_word_char(tmp)
+            else:
+                return False
+
+        def ends_word(self, loc):
+            if loc.ends_word():
+                if loc.is_end():
+                    return True
+                else:
+                    tmp = loc.copy()
+                    tmp.forward_char()
+                    return not self.is_extra_word_char(tmp)
+            else:
+                return False
+
+        def forward_word_end(self, loc):
+            def move_through_extra_chars():
+                moved = False
+                while self.is_extra_word_char(loc):
+                    if not loc.forward_char():
+                        break
+                    moved = True
+                return moved
+
+            tmp = loc.copy()
+            tmp.backward_char()
+            loc.forward_word_end()
+            while move_through_extra_chars():
+                if loc.is_end() or not loc.inside_word() or not loc.forward_word_end():
+                    break
+
+        def backward_word_start(self, loc):
+            def move_through_extra_chars():
+                tmp = loc.copy()
+                tmp.backward_char()
+                moved = False
+                while self.is_extra_word_char(tmp):
+                    moved = True
+                    loc.assign(tmp)
+                    if not tmp.backward_char():
+                        break
+                return moved
+
+            loc.backward_word_start()
+            while move_through_extra_chars():
+                tmp = loc.copy()
+                tmp.backward_char()
+                if (
+                    loc.is_start()
+                    or not tmp.inside_word()
+                    or not loc.backward_word_start()
+                ):
+                    break
+
+        def sync_extra_chars(self, obj, value):
+            self._extra_word_chars = obj.extra_chars
+
+    def __init__(
+        self, view, language="en", prefix="gtkspellchecker", collapse=True, params=None
+    ):
+        super().__init__()
         self._view = view
         self.collapse = collapse
-        self._view.connect('populate-popup',
-                           lambda entry, menu:self._extend_menu(menu))
-        self._view.connect('popup-menu', self._click_move_popup)
-        self._view.connect('button-press-event', self._click_move_button)
+        # GTK 3-only signals. GTK 4 uses actions, below.
+        if _IS_GTK3:
+            self._view.connect(
+                "populate-popup", lambda entry, menu: self._extend_menu(menu)
+            )
+            self._view.connect("popup-menu", self._click_move_popup)
+            self._view.connect("button-press-event", self._click_move_button)
         self._prefix = prefix
         self._broker = enchant.Broker()
-        for param, value in params.items(): self._broker.set_param(param, value)
+        if params is not None:
+            for param, value in params.items():
+                self._broker.set_param(param, value)
         self.languages = SpellChecker._LanguageList.from_broker(self._broker)
         if self.languages.exists(language):
             self._language = language
-        elif self.languages.exists('en'):
-            logger.warning(('no installed dictionary for language "{}", '
-                            'fallback to english'.format(language)))
-            self._language = 'en'
+        elif self.languages.exists("en"):
+            logger.warning(
+                (
+                    'no installed dictionary for language "{}", '
+                    "fallback to english".format(language)
+                )
+            )
+            self._language = "en"
         else:
             if self.languages:
                 self._language = self.languages[0][0]
-                logger.warning(('no installed dictionary for language "{}" '
-                                'and english, fallback to first language in'
-                                'language list ("{}")').format(language,
-                                                                self._language))
+                logger.warning(
+                    (
+                        'no installed dictionary for language "{}" '
+                        "and english, fallback to first language in"
+                        'language list ("{}")'
+                    ).format(language, self._language)
+                )
             else:
-                logger.critical('no dictionaries found')
+                logger.critical("no dictionaries found")
                 raise NoDictionariesFound()
         self._dictionary = self._broker.request_dict(self._language)
         self._deferred_check = False
         self._filters = dict(SpellChecker.DEFAULT_FILTERS)
-        self._regexes = {SpellChecker.FILTER_WORD : re.compile('|'.join(
-                             self._filters[SpellChecker.FILTER_WORD])),
-                         SpellChecker.FILTER_LINE : re.compile('|'.join(
-                             self._filters[SpellChecker.FILTER_LINE])),
-                         SpellChecker.FILTER_TEXT : re.compile('|'.join(
-                             self._filters[SpellChecker.FILTER_TEXT]),
-                                                               re.MULTILINE)}
+        self._regexes = {
+            SpellChecker.FILTER_WORD: re.compile(
+                "|".join(self._filters[SpellChecker.FILTER_WORD])
+            ),
+            SpellChecker.FILTER_LINE: re.compile(
+                "|".join(self._filters[SpellChecker.FILTER_LINE])
+            ),
+            SpellChecker.FILTER_TEXT: re.compile(
+                "|".join(self._filters[SpellChecker.FILTER_TEXT]), re.MULTILINE
+            ),
+        }
+
+        self._extra_chars = SpellChecker.DEFAULT_EXTRA_CHARS
+        self._iter_worker = SpellChecker._IterWorker(self._extra_chars)
+        self.connect("notify::extra-chars", self._iter_worker.sync_extra_chars)
+
+        self._batched_rechecking = False
+
+        self._languages_menu = None
+        # GTK 4-only extra menu population, gesture creation and action setup. GTK 3
+        # uses signals, above.
+        if not _IS_GTK3:
+            extra_menu = self._view.get_extra_menu()
+            if extra_menu is None:
+                extra_menu = Gio.Menu()
+                self._view.set_extra_menu(extra_menu)
+            self._spelling_menu = Gio.Menu()
+            extra_menu.append_section(None, self._spelling_menu)
+
+            controller = Gtk.GestureClick()
+            controller.set_button(0)
+            controller.connect("pressed", self._gtk4_on_textview_click)
+            self._view.add_controller(controller)
+
+            self._gtk4_setup_actions()
+
         self._enabled = True
         self.buffer_initialize()
 
-    @property
+    @GObject.Property(type=str, default="")
     def language(self):
         """
         The language used for spellchecking.
@@ -236,7 +361,7 @@ class SpellChecker(object):
             self._dictionary = self._broker.request_dict(language)
             self.recheck()
 
-    @property
+    @GObject.Property(type=bool, default=False)
     def enabled(self):
         """
         Enable or disable spellchecking.
@@ -250,48 +375,82 @@ class SpellChecker(object):
         elif not enabled and self._enabled:
             self.disable()
 
+    @GObject.Property(type=bool, default=False)
+    def batched_rechecking(self):
+        """
+        Whether to enable batched rechecking of large buffers.
+        """
+        return self._batched_rechecking
+
+    @batched_rechecking.setter
+    def batched_rechecking(self, val):
+        self._batched_rechecking = val
+
+    @GObject.Property(type=str, default=",")
+    def extra_chars(self):
+        """
+        Fetch the list of extra characters beyond which words are extended.
+        """
+        return self._extra_chars
+
+    @extra_chars.setter
+    def extra_chars(self, chars):
+        """
+        Set the list of extra characters beyond which words are extended.
+
+        :param val: String containing list of characters
+        """
+        self._extra_chars = chars
+
     def buffer_initialize(self):
         """
         Initialize the GtkTextBuffer associated with the GtkTextView. If you
         have associated a new GtkTextBuffer with the GtkTextView call this
         method.
         """
-        if _pygobject:
-            self._misspelled = gtk.TextTag.new('{}-misspelled'\
-                                               .format(self._prefix))
-        else:
-            self._misspelled = gtk.TextTag('{}-misspelled'.format(self._prefix))
-        self._misspelled.set_property('underline', 4)
+        self._misspelled = Gtk.TextTag.new("{}-misspelled".format(self._prefix))
+        self._misspelled.set_property("underline", 4)
         self._buffer = self._view.get_buffer()
-        self._buffer.connect('insert-text', self._before_text_insert)
-        self._buffer.connect_after('insert-text', self._after_text_insert)
-        self._buffer.connect_after('delete-range', self._range_delete)
-        self._buffer.connect_after('mark-set', self._mark_set)
+        self._buffer.connect("insert-text", self._before_text_insert)
+        self._buffer.connect_after("insert-text", self._after_text_insert)
+        self._buffer.connect_after("delete-range", self._range_delete)
+        self._buffer.connect_after("mark-set", self._mark_set)
         start = self._buffer.get_bounds()[0]
-        self._marks = {'insert-start' : SpellChecker._Mark(self._buffer,
-                           '{}-insert-start'.format(self._prefix), start),
-                       'insert-end' : SpellChecker._Mark(self._buffer,
-                           '{}-insert-end'.format(self._prefix), start),
-                       'click' : SpellChecker._Mark(self._buffer,
-                           '{}-click'.format(self._prefix), start)}
+        self._marks = {
+            "insert-start": SpellChecker._Mark(
+                self._buffer,
+                "{}-insert-start".format(self._prefix),
+                start,
+                self._iter_worker,
+            ),
+            "insert-end": SpellChecker._Mark(
+                self._buffer,
+                "{}-insert-end".format(self._prefix),
+                start,
+                self._iter_worker,
+            ),
+            "click": SpellChecker._Mark(
+                self._buffer, "{}-click".format(self._prefix), start, self._iter_worker
+            ),
+        }
         self._table = self._buffer.get_tag_table()
         self._table.add(self._misspelled)
         self.ignored_tags = []
+
         def tag_added(tag, *args):
-            if hasattr(tag, 'spell_check') and not getattr(tag, 'spell_check'):
+            if hasattr(tag, "spell_check") and not tag.spell_check:
                 self.ignored_tags.append(tag)
+
         def tag_removed(tag, *args):
             if tag in self.ignored_tags:
                 self.ignored_tags.remove(tag)
-        self._table.connect('tag-added', tag_added)
-        self._table.connect('tag-removed', tag_removed)
+
+        self._table.connect("tag-added", tag_added)
+        self._table.connect("tag-removed", tag_removed)
         self._table.foreach(tag_added, None)
-        self.no_spell_check = self._table.lookup('no-spell-check')
+        self.no_spell_check = self._table.lookup("no-spell-check")
         if not self.no_spell_check:
-            if _pygobject:
-                self.no_spell_check = gtk.TextTag.new('no-spell-check')
-            else:
-                self.no_spell_check = gtk.TextTag('no-spell-check')
+            self.no_spell_check = Gtk.TextTag.new("no-spell-check")
             self._table.add(self.no_spell_check)
         self.recheck()
 
@@ -300,7 +459,12 @@ class SpellChecker(object):
         Rechecks the spelling of the whole text.
         """
         start, end = self._buffer.get_bounds()
-        self.check_range(start, end, True)
+
+        if self._batched_rechecking and end.get_offset() > _BATCHING_THRESHOLD_CHARS:
+            start_mark = self._buffer.create_mark(None, start)
+            self._continue_batched_recheck(start_mark)
+        else:
+            self.check_range(start, end, True)
 
     def disable(self):
         """
@@ -343,11 +507,13 @@ class SpellChecker(object):
         """
         self._filters[filter_type].append(regex)
         if filter_type == SpellChecker.FILTER_TEXT:
-            self._regexes[filter_type] = re.compile('|'.join(
-                self._filters[filter_type]), re.MULTILINE)
+            self._regexes[filter_type] = re.compile(
+                "|".join(self._filters[filter_type]), re.MULTILINE
+            )
         else:
-            self._regexes[filter_type] = re.compile('|'.join(
-                self._filters[filter_type]))
+            self._regexes[filter_type] = re.compile(
+                "|".join(self._filters[filter_type])
+            )
 
     def remove_filter(self, regex, filter_type):
         """
@@ -358,11 +524,13 @@ class SpellChecker(object):
         """
         self._filters[filter_type].remove(regex)
         if filter_type == SpellChecker.FILTER_TEXT:
-            self._regexes[filter_type] = re.compile('|'.join(
-                self._filters[filter_type]), re.MULTILINE)
+            self._regexes[filter_type] = re.compile(
+                "|".join(self._filters[filter_type]), re.MULTILINE
+            )
         else:
-            self._regexes[filter_type] = re.compile('|'.join(
-                self._filters[filter_type]))
+            self._regexes[filter_type] = re.compile(
+                "|".join(self._filters[filter_type])
+            )
 
     def append_ignore_tag(self, tag):
         """
@@ -371,7 +539,7 @@ class SpellChecker(object):
 
         :param tag: Tag object or tag name.
         """
-        if isinstance(tag, basestring):
+        if isinstance(tag, str):
             tag = self._table.lookup(tag)
         self.ignored_tags.append(tag)
 
@@ -382,7 +550,7 @@ class SpellChecker(object):
 
         :param tag: Tag object or tag name.
         """
-        if isinstance(tag, basestring):
+        if isinstance(tag, str):
             tag = self._table.lookup(tag)
         self.ignored_tags.remove(tag)
 
@@ -411,27 +579,39 @@ class SpellChecker(object):
         :param start: Start iter - checking starts here.
         :param end: End iter - checking ends here.
         """
+        logger.debug(
+            "Check range called with range %d:%d to %d:%d and force all set to %s.",
+            start.get_line(),
+            start.get_line_offset(),
+            end.get_line(),
+            end.get_line_offset(),
+            force_all,
+        )
         if not self._enabled:
             return
-        if end.inside_word(): end.forward_word_end()
-        if not start.starts_word() and (start.inside_word() or
-                                        start.ends_word()):
-            start.backward_word_start()
+        start = start.copy()
+        end = end.copy()
+        if self._iter_worker.inside_word(end):
+            self._iter_worker.forward_word_end(end)
+        if self._iter_worker.inside_word(start) or self._iter_worker.ends_word(start):
+            self._iter_worker.backward_word_start(start)
+        if not self._iter_worker.starts_word(start):
+            self._iter_worker.forward_word_end(start)
+            self._iter_worker.backward_word_start(start)
         self._buffer.remove_tag(self._misspelled, start, end)
         cursor = self._buffer.get_iter_at_mark(self._buffer.get_insert())
         precursor = cursor.copy()
         precursor.backward_char()
-        highlight = (cursor.has_tag(self._misspelled) or
-                     precursor.has_tag(self._misspelled))
-        if not start.get_offset():
-            start.forward_word_end()
-            start.backward_word_start()
+        highlight = cursor.has_tag(self._misspelled) or precursor.has_tag(
+            self._misspelled
+        )
         word_start = start.copy()
         while word_start.compare(end) < 0:
             word_end = word_start.copy()
-            word_end.forward_word_end()
-            in_word = ((word_start.compare(cursor) < 0) and
-                       (cursor.compare(word_end) <= 0))
+            self._iter_worker.forward_word_end(word_end)
+            in_word = (word_start.compare(cursor) < 0) and (
+                cursor.compare(word_end) <= 0
+            )
             if in_word and not force_all:
                 if highlight:
                     self._check_word(word_start, word_end)
@@ -440,153 +620,225 @@ class SpellChecker(object):
             else:
                 self._check_word(word_start, word_end)
                 self._deferred_check = False
-            word_end.forward_word_end()
-            word_end.backward_word_start()
+            self._iter_worker.forward_word_end(word_end)
+            self._iter_worker.backward_word_start(word_end)
             if word_start.equal(word_end):
                 break
             word_start = word_end.copy()
 
-    def _languages_menu(self):
-        def _set_language(item, code):
-            self.language = code
-        if _pygobject:
-            menu = gtk.Menu.new()
+    def _gtk4_setup_actions(self) -> None:
+        action_group = Gio.SimpleActionGroup.new()
+
+        action = Gio.SimpleAction.new("ignore-all", GLib.VariantType("s"))
+        action.connect(
+            "activate", lambda _action, word: self.ignore_all(word.get_string())
+        )
+        action_group.add_action(action)
+
+        action = Gio.SimpleAction.new("add-to-dictionary", GLib.VariantType("s"))
+        action.connect(
+            "activate", lambda _action, word: self.add_to_dictionary(word.get_string())
+        )
+        action_group.add_action(action)
+
+        action = Gio.SimpleAction.new("replace-word", GLib.VariantType("s"))
+        action.connect(
+            "activate",
+            lambda _action, suggestion: self._replace_word(suggestion.get_string()),
+        )
+        action_group.add_action(action)
+
+        language = Gio.PropertyAction.new("language", self, "language")
+        action_group.add_action(language)
+
+        self._view.insert_action_group("spelling", action_group)
+
+    def _get_languages_menu(self):
+        if _IS_GTK3:
+            return self._build_languages_menu()
+        else:
+            if self._languages_menu is None:
+                self._languages_menu = self._build_languages_menu()
+            return self._languages_menu
+
+    def _build_languages_menu(self):
+        if _IS_GTK3:
+
+            def _set_language(item, code):
+                self.language = code
+
+            menu = Gtk.Menu.new()
             group = []
+            connect = []
         else:
-            menu = gtk.Menu()
-            group = gtk.RadioMenuItem()
-        connect = []
+            menu = Gio.Menu.new()
+
         for code, name in self.languages:
-            if _pygobject:
-                item = gtk.RadioMenuItem.new_with_label(group, name)
+            if _IS_GTK3:
+                item = Gtk.RadioMenuItem.new_with_label(group, name)
                 group.append(item)
+                if code == self.language:
+                    item.set_active(True)
+                connect.append((item, code))
+                menu.append(item)
             else:
-                item = gtk.RadioMenuItem(group, name)
-            if code == self.language:
-                item.set_active(True)
-            connect.append((item, code))
-            menu.append(item)
-        for item, code in connect:
-            item.connect('activate', _set_language, code)
-        return menu
+                item = Gio.MenuItem.new(name, None)
+                item.set_action_and_target_value(
+                    "spelling.language", GLib.Variant.new_string(code)
+                )
+                menu.append_item(item)
+        if _IS_GTK3:
+            for item, code in connect:
+                item.connect("activate", _set_language, code)
+            return menu
+        else:
+            return Gio.MenuItem.new_submenu(_("Languages"), menu)
 
     def _suggestion_menu(self, word):
         menu = []
         suggestions = self._dictionary.suggest(word)
         if not suggestions:
-            if _pygobject:
-                item = gtk.MenuItem.new()
-                label = gtk.Label.new('')
-            else:
-                item = gtk.MenuItem()
-                label = gtk.Label()
-            try:
-                label.set_halign(gtk.Align.LEFT)
-            except AttributeError:
-                label.set_alignment(0.0, 0.5)
-            label.set_markup('<i>{text}</i>'.format(text=_('(no suggestions)')))
-            item.add(label)
-            menu.append(item)
-        else:
-            for suggestion in suggestions:
-                if _pygobject:
-                    item = gtk.MenuItem.new()
-                    label = gtk.Label.new('')
-                else:
-                    item = gtk.MenuItem()
-                    label = gtk.Label()
-                label.set_markup('<b>{text}</b>'.format(text=suggestion))
+            # Show GTK 3 no suggestions item (removed for GTK 4)
+            if _IS_GTK3:
+                item = Gtk.MenuItem.new()
+                label = Gtk.Label.new("")
                 try:
-                    label.set_halign(gtk.Align.LEFT)
+                    label.set_halign(Gtk.Align.LEFT)
                 except AttributeError:
                     label.set_alignment(0.0, 0.5)
+                label.set_markup("<i>{text}</i>".format(text=_("(no suggestions)")))
                 item.add(label)
-                item.connect('activate', self._replace_word, word, suggestion)
                 menu.append(item)
-        if _pygobject:
-            menu.append(gtk.SeparatorMenuItem.new())
-            item = gtk.MenuItem.new_with_label(
-                _('Add "{}" to Dictionary').format(word))
         else:
-            menu.append(gtk.SeparatorMenuItem())
-            item = gtk.MenuItem(_('Add "{}" to Dictionary').format(word))
-        item.connect('activate', lambda *args: self.add_to_dictionary(word))
+            for suggestion in suggestions:
+                if _IS_GTK3:
+                    item = Gtk.MenuItem.new()
+                    label = Gtk.Label.new("")
+                    label.set_markup("<b>{text}</b>".format(text=suggestion))
+                    try:
+                        label.set_halign(Gtk.Align.LEFT)
+                    except AttributeError:
+                        label.set_alignment(0.0, 0.5)
+                    item.add(label)
+
+                    def _make_on_activate(word):
+                        return lambda *args: self._replace_word(word)
+
+                    item.connect("activate", _make_on_activate(word))
+                else:
+                    escaped = suggestion.replace("'", "\\'")
+                    item = Gio.MenuItem.new(
+                        suggestion, f"spelling.replace-word('{escaped}')"
+                    )
+                menu.append(item)
+        add_to_dict_menu_label = _("Add to Dictionary")
+        word_escaped = word.replace("'", "\\'")
+        if _IS_GTK3:
+            menu.append(Gtk.SeparatorMenuItem.new())
+            item = Gtk.MenuItem.new_with_label(add_to_dict_menu_label)
+            item.connect("activate", lambda *args: self.add_to_dictionary(word))
+        else:
+            item = Gio.MenuItem.new(
+                add_to_dict_menu_label, f"spelling.add-to-dictionary('{word_escaped}')"
+            )
         menu.append(item)
-        if _pygobject:
-            item = gtk.MenuItem.new_with_label(_('Ignore All'))
+        ignore_menu_label = _("Ignore All")
+        if _IS_GTK3:
+            item = Gtk.MenuItem.new_with_label(ignore_menu_label)
+            item.connect("activate", lambda *args: self.ignore_all(word))
         else:
-            item = gtk.MenuItem(_('Ignore All'))
-        item.connect('activate', lambda *args: self.ignore_all(word))
+            item = Gio.MenuItem.new(
+                ignore_menu_label, f"spelling.ignore-all('{word_escaped}')"
+            )
         menu.append(item)
         return menu
 
     def _extend_menu(self, menu):
+        # In GTK 4 our existing menu needs to be cleared, providing for disabling
+        if not _IS_GTK3:
+            menu.remove_all()
+
         if not self._enabled:
             return
-        if _pygobject:
-            separator = gtk.SeparatorMenuItem.new()
-        else:
-            separator = gtk.SeparatorMenuItem()
-        separator.show()
-        menu.prepend(separator)
-        if _pygobject:
-            languages = gtk.MenuItem.new_with_label(_('Languages'))
+
+        if _IS_GTK3:
+            separator = Gtk.SeparatorMenuItem.new()
+            separator.show()
+            menu.prepend(separator)
+            languages = Gtk.MenuItem.new_with_label(_("Languages"))
+            languages.set_submenu(self._get_languages_menu())
+            languages.show_all()
+            menu.prepend(languages)
         else:
-            languages = gtk.MenuItem(_('Languages'))
-        languages.set_submenu(self._languages_menu())
-        languages.show_all()
-        menu.prepend(languages)
-        if self._marks['click'].inside_word:
-            start, end = self._marks['click'].word
+            menu.append_item(self._get_languages_menu())
+
+        if self._marks["click"].inside_word:
+            start, end = self._marks["click"].word
             if start.has_tag(self._misspelled):
-                if _py3k:
-                    word = self._buffer.get_text(start, end, False)
-                else:
-                    word = self._buffer.get_text(start, end,
-                                                 False).decode('utf-8')
+                word = self._buffer.get_text(start, end, False)
                 items = self._suggestion_menu(word)
                 if self.collapse:
-                    if _pygobject:
-                        suggestions = gtk.MenuItem.new_with_label(
-                            _('Suggestions'))
-                        submenu = gtk.Menu.new()
+                    menu_label = _("Suggestions")
+                    if _IS_GTK3:
+                        suggestions = Gtk.MenuItem.new_with_label(menu_label)
+                        submenu = Gtk.Menu.new()
                     else:
-                        suggestions = gtk.MenuItem(_('Suggestions'))
-                        submenu = gtk.Menu()
+                        suggestions = Gio.MenuItem.new(menu_label, None)
+                        submenu = Gio.Menu.new()
                     for item in items:
-                        submenu.append(item)
+                        if _IS_GTK3:
+                            submenu.append(item)
+                        else:
+                            submenu.append_item(item)
                     suggestions.set_submenu(submenu)
-                    suggestions.show_all()
-                    menu.prepend(suggestions)
+                    if _IS_GTK3:
+                        suggestions.show_all()
+                        menu.prepend(suggestions)
+                    else:
+                        menu.prepend_item(suggestions)
                 else:
                     items.reverse()
                     for item in items:
-                        menu.prepend(item)
-                        menu.show_all()
+                        if _IS_GTK3:
+                            menu.prepend(item)
+                            menu.show_all()
+                        else:
+                            menu.prepend_item(item)
 
     def _click_move_popup(self, *args):
-        self._marks['click'].move(self._buffer.get_iter_at_mark(
-            self._buffer.get_insert()))
+        self._marks["click"].move(
+            self._buffer.get_iter_at_mark(self._buffer.get_insert())
+        )
         return False
 
     def _click_move_button(self, widget, event):
         if event.button == 3:
-            if self._deferred_check:  self._check_deferred_range(True)
-            x, y = self._view.window_to_buffer_coords(2, int(event.x),
-                                                      int(event.y))
-            iter = self._view.get_iter_at_location(x, y)
-            if isinstance(iter, tuple):
-                iter = iter[1]
-            self._marks['click'].move(iter)
+            self._move_mark_for_input(event.x, event.y)
         return False
 
+    def _move_mark_for_input(self, input_x, input_y):
+        if self._deferred_check:
+            self._check_deferred_range(True)
+        x, y = self._view.window_to_buffer_coords(2, int(input_x), int(input_y))
+        iter = self._view.get_iter_at_location(x, y)
+        if isinstance(iter, tuple):
+            iter = iter[1]
+        self._marks["click"].move(iter)
+
+    def _gtk4_on_textview_click(self, click, n_press, x, y) -> None:
+        if n_press != 1 or click.get_current_button() != 3:
+            return
+
+        self._move_mark_for_input(x, y)
+        self._extend_menu(self._spelling_menu)
+
     def _before_text_insert(self, textbuffer, location, text, length):
-        self._marks['insert-start'].move(location)
+        self._marks["insert-start"].move(location)
 
     def _after_text_insert(self, textbuffer, location, text, length):
-        start = self._marks['insert-start'].iter
+        start = self._marks["insert-start"].iter
         self.check_range(start, location)
-        self._marks['insert-end'].move(location)
+        self._marks["insert-end"].move(location)
 
     def _range_delete(self, textbuffer, start, end):
         self.check_range(start, end)
@@ -595,8 +847,9 @@ class SpellChecker(object):
         if mark == self._buffer.get_insert() and self._deferred_check:
             self._check_deferred_range(False)
 
-    def _replace_word(self, item, old_word, new_word):
-        start, end = self._marks['click'].word
+    def _replace_word(self, new_word):
+        start, end = self._marks["click"].word
+        old_word = start.get_text(end)
         offset = start.get_offset()
         self._buffer.begin_user_action()
         self._buffer.delete(start, end)
@@ -605,8 +858,8 @@ class SpellChecker(object):
         self._dictionary.store_replacement(old_word, new_word)
 
     def _check_deferred_range(self, force_all):
-        start = self._marks['insert-start'].iter
-        end = self._marks['insert-end'].iter
+        start = self._marks["insert-start"].iter
+        end = self._marks["insert-end"].iter
         self.check_range(start, end, force_all)
 
     def _check_word(self, start, end):
@@ -615,39 +868,51 @@ class SpellChecker(object):
         for tag in self.ignored_tags:
             if start.has_tag(tag):
                 return
-        if _py3k:
-            word = self._buffer.get_text(start, end, False).strip()
-        else:
-            word = self._buffer.get_text(start, end, False).decode('utf-8').strip()
+        word = self._buffer.get_text(start, end, False).strip()
+        logger.debug(
+            "Checking word %s in range %d:%d to %d:%d.",
+            word,
+            start.get_line(),
+            start.get_line_offset(),
+            end.get_line(),
+            end.get_line_offset(),
+        )
         if not word:
             return
         if len(self._filters[SpellChecker.FILTER_WORD]):
             if self._regexes[SpellChecker.FILTER_WORD].match(word):
                 return
         if len(self._filters[SpellChecker.FILTER_LINE]):
-            line_start = self._buffer.get_iter_at_line(start.get_line())
+            if _IS_GTK3:
+                line_start = self._buffer.get_iter_at_line(start.get_line())
+            else:
+                _success, line_start = self._buffer.get_iter_at_line(start.get_line())
             line_end = end.copy()
             line_end.forward_to_line_end()
-            if _py3k:
-                line = self._buffer.get_text(line_start, line_end, False)
-            else:
-                line = self._buffer.get_text(line_start, line_end,
-                                             False).decode('utf-8')
+            line = self._buffer.get_text(line_start, line_end, False)
             for match in self._regexes[SpellChecker.FILTER_LINE].finditer(line):
                 if match.start() <= start.get_line_offset() <= match.end():
-                    start = self._buffer.get_iter_at_line_offset(
-                        start.get_line(), match.start())
-                    end = self._buffer.get_iter_at_line_offset(start.get_line(),
-                                                               match.end())
+                    if _IS_GTK3:
+                        start = self._buffer.get_iter_at_line_offset(
+                            start.get_line(), match.start()
+                        )
+                        end = self._buffer.get_iter_at_line_offset(
+                            start.get_line(), match.end()
+                        )
+                    else:
+                        # Success is not verified here as the locations come directly
+                        # from the buffer
+                        _success, start = self._buffer.get_iter_at_line_offset(
+                            start.get_line(), match.start()
+                        )
+                        _success, end = self._buffer.get_iter_at_line_offset(
+                            start.get_line(), match.end()
+                        )
                     self._buffer.remove_tag(self._misspelled, start, end)
                     return
         if len(self._filters[SpellChecker.FILTER_TEXT]):
             text_start, text_end = self._buffer.get_bounds()
-            if _py3k:
-                text = self._buffer.get_text(text_start, text_end, False)
-            else:
-                text = self._buffer.get_text(text_start, text_end,
-                                             False).decode('utf-8')
+            text = self._buffer.get_text(text_start, text_end, False)
             for match in self._regexes[SpellChecker.FILTER_TEXT].finditer(text):
                 if match.start() <= start.get_offset() <= match.end():
                     start = self._buffer.get_iter_at_offset(match.start())
@@ -656,3 +921,23 @@ class SpellChecker(object):
                     return
         if not self._dictionary.check(word):
             self._buffer.apply_tag(self._misspelled, start, end)
+
+    def _continue_batched_recheck(self, start_mark):
+        if start_mark.get_buffer() != self._buffer:
+            return
+        start = self._buffer.get_iter_at_mark(start_mark)
+        self._buffer.delete_mark(start_mark)
+
+        if not self._enabled:
+            return
+
+        end = start.copy()
+        end.forward_chars(_BATCH_SIZE_CHARS)
+        self._iter_worker.forward_word_end(end)
+
+        self.check_range(start, end, True)
+
+        if not end.is_end():
+            end.forward_char()
+            start_mark = self._buffer.create_mark(None, end)
+            GLib.idle_add(self._continue_batched_recheck, start_mark)
diff --git a/src/pygtkspellcheck.egg-info/PKG-INFO b/src/pygtkspellcheck.egg-info/PKG-INFO
deleted file mode 100644
index 5c8dcf4..0000000
--- a/src/pygtkspellcheck.egg-info/PKG-INFO
+++ /dev/null
@@ -1,21 +0,0 @@
-Metadata-Version: 1.1
-Name: pygtkspellcheck
-Version: 4.0.5
-Summary: a simple but quite powerful Python spell checking library for GtkTextViews based on Enchant
-Home-page: http://koehlma.github.com/projects/pygtkspellcheck.html
-Author: Maximilian Köhl & Carlos Jenkins
-Author-email: linuxmaxi@googlemail.com & carlos@jenkins.co.cr
-License: GPLv3+
-Download-URL: https://github.com/koehlma/pygtkspellcheck/tarball/master
-Description: A simple but quite powerful spellchecking library written in pure Python for Gtk based on Enchant. It supports PyGObject as well as PyGtk for Python 2 and 3 with automatic switching and binding detection. For automatic translation of the user interface it can use Gedit’s translation files.
-Platform: UNKNOWN
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: X11 Applications :: Gnome
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
-Classifier: Operating System :: MacOS :: MacOS X
-Classifier: Operating System :: Microsoft :: Windows
-Classifier: Operating System :: POSIX
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 3
-Classifier: Topic :: Software Development :: Localization
diff --git a/src/pygtkspellcheck.egg-info/SOURCES.txt b/src/pygtkspellcheck.egg-info/SOURCES.txt
deleted file mode 100644
index e7dd6ca..0000000
--- a/src/pygtkspellcheck.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,39 +0,0 @@
-LICENSE.txt
-MANIFEST.in
-README.rst
-setup.py
-doc/Makefile
-doc/insert_metadata.py
-doc/make.bat
-doc/metadata/documentation.rst
-doc/metadata/metadata.py
-doc/metadata/pypi.rst
-doc/metadata/readme.md
-doc/metadata/screenshot.png
-doc/metadata/website.md
-doc/pypi/index.html
-doc/pypi/page.rst
-doc/screenshots/screenshot.png
-doc/source/conf.py
-doc/source/index.rst
-examples/large_pygobject.py
-examples/large_pygtk.py
-examples/simple_pygobject.py
-examples/simple_pygtk.py
-locale/de/pygtkspellcheck.mo
-locale/de/pygtkspellcheck.po
-locale/en/pygtkspellcheck.mo
-locale/en/pygtkspellcheck.po
-locale/es/pygtkspellcheck.mo
-locale/es/pygtkspellcheck.po
-src/gtkspellcheck/__init__.py
-src/gtkspellcheck/oxt_extract.py
-src/gtkspellcheck/spellcheck.py
-src/pygtkspellcheck.egg-info/PKG-INFO
-src/pygtkspellcheck.egg-info/SOURCES.txt
-src/pygtkspellcheck.egg-info/dependency_links.txt
-src/pygtkspellcheck.egg-info/requires.txt
-src/pygtkspellcheck.egg-info/top_level.txt
-src/pylocales/__init__.py
-src/pylocales/locales.db
-src/pylocales/locales.py
\ No newline at end of file
diff --git a/src/pygtkspellcheck.egg-info/dependency_links.txt b/src/pygtkspellcheck.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/src/pygtkspellcheck.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/pygtkspellcheck.egg-info/requires.txt b/src/pygtkspellcheck.egg-info/requires.txt
deleted file mode 100644
index b8f9eb4..0000000
--- a/src/pygtkspellcheck.egg-info/requires.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-pyenchant
-
-[building the documentation]
-sphinx
diff --git a/src/pygtkspellcheck.egg-info/top_level.txt b/src/pygtkspellcheck.egg-info/top_level.txt
deleted file mode 100644
index 54ac8a6..0000000
--- a/src/pygtkspellcheck.egg-info/top_level.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-gtkspellcheck
-pylocales
diff --git a/src/pylocales/__init__.py b/src/pylocales/__init__.py
deleted file mode 100644
index ee32ec0..0000000
--- a/src/pylocales/__init__.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# -*- coding:utf-8 -*-
-#
-# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-
-__version__ = '1.2'
-__project__ = 'Python Locales'
-__short_name__ = 'pylocales'
-__authors__ = 'Maximilian Köhl'
-__emails__ = 'linuxmaxi@googlemail.com'
-__website__ = 'http://koehlma.github.com/projects/pygtkspellcheck.html'
-__source__ = 'https://github.com/koehlma/pygtkspellcheck/'
-__vcs__ = 'git://github.com/koehlma/pygtkspellcheck.git'
-__copyright__ = '2012, Maximilian Köhl'
-__desc_short__ = 'query the ISO 639/3166 database about a country or a language.'
-__desc_long__ = ('Query the ISO 639/3166 database about a country or a'
-                 'language. The locales database contains ISO 639 language'
-                 'definitions and ISO 3166 country definitions. This package'
-                 'provides translation for country and language names if'
-                 'the iso-code messages are installed on your system.')
-
-__metadata__ = {'__version__' : __version__,
-                '__project__' : __project__,
-                '__short_name__' : __short_name__,
-                '__authors__' : __authors__,
-                '__emails__' : __emails__,
-                '__website__' : __website__,
-                '__source__' : __source__,
-                '__vcs__' : __vcs__,
-                '__copyright__' : __copyright__,
-                '__desc_short__' : __desc_short__,
-                '__desc_long__' : __desc_long__}
-
-from pylocales.locales import (Country, Language, LanguageNotFound,
-                               CountryNotFound, code_to_name)

More details

Full run details

Historical runs