Plugin Settings and Localization

Two things separate a plugin people keep from a plugin people try once. The first is memory: it remembers the folder you worked in, the layer you picked, the options you set, so the second run costs three clicks instead of ten. The second is language: QGIS ships in more than forty of them, and a plugin whose buttons are only in English is a plugin half its potential users will close.

Neither is hard. Both are routinely skipped, because they are the last thing on the list and nothing breaks without them. This guide sits inside QGIS Plugin Development and covers where plugin state belongs, how to store it without colliding with anybody else, how to publish an options page inside QGIS's own settings dialog, and the complete translation workflow from a marked string to a loaded .qm file.

Three homes for plugin state, and how to chooseUser settings hold preferences that belong to the person and the machine, such as the last used folder and a default output format. Project entries hold facts about this particular map, such as which layer holds the survey points. In-memory state holds the current run's working values and disappears when QGIS closes. Choosing wrongly produces either state that fails to travel or state that travels when it should not.Ask who the value belongs to: the person, the map, or the momentQgsSettings — the personlast used folderdefault output formatAPI endpoint and credentials idwindow size, checkbox statessurvives restartsnever leaves the machineproject entries — the mapwhich layer holds the surveythe analysis parameters useda per-project schema nameanything a colleague needs tootravels with the fileshared with whoever opens itin memory — the momentthe layer being processedprogress and cancellation flagsa cached lookup tableanything cheap to rebuildgone on unloadand that is correctA local path in the project file breaks for everyone else — a project fact in user settings breaks on the next project

Where each kind of state belongs

The mistake that causes the most confusing bug reports is putting a value in the wrong place, and it is easy to make because both mechanisms work perfectly in testing on one machine.

User settings are per installation, per person. They belong to preferences: the folder somebody last exported to, whether they want the result added to the map automatically, which server their organisation uses. They persist forever and never travel, which is exactly right for a local path and exactly wrong for anything a colleague needs.

Project entries are written into the project file and travel with it. They belong to facts about that map: which layer is the coverage layer, what parameters produced the current output, which database schema this project reads. A plugin that stores those in user settings appears to work until the user opens a second project, at which point it confidently uses the first project's values.

In-memory state lives on your plugin object and dies with it. Anything cheap to rebuild belongs here, and putting it anywhere else creates staleness — a cached layer id that survives a restart is a reference to a layer that may no longer exist.

The rule of thumb is a question: if the user emailed this project to a colleague, should the value go with it? Yes means a project entry, no means a setting. The project side of that is covered in Working with QGIS Projects in PyQGIS; the settings side is the rest of this guide.

QgsSettings: keys, defaults and types

QgsSettings is QGIS's wrapper over Qt's settings mechanism, with the addition that it also reads QGIS's own global defaults. Its API is small.

from qgis.core import QgsSettings

settings = QgsSettings()
settings.setValue("parcel_tools/output_folder", "/data/exports")
settings.setValue("parcel_tools/add_to_map", True)
settings.setValue("parcel_tools/buffer_distance", 25.0)

folder = settings.value("parcel_tools/output_folder", "", type=str)
add_to_map = settings.value("parcel_tools/add_to_map", True, type=bool)
distance = settings.value("parcel_tools/buffer_distance", 10.0, type=float)

Breakdown: Keys are paths, and the first segment should be your plugin's folder name so nothing collides with QGIS or another plugin — settings are a single flat namespace shared by everything, and a key called output_folder at the top level is a genuine hazard. Always pass a default to value(), because a missing key otherwise returns None and the failure lands somewhere far away. Passing type= is not decoration: on some platforms every value comes back as a string, so value("parcel_tools/add_to_map") can return the string "false", which is truthy, and the checkbox appears stuck on. That one line prevents a bug that is very hard to reproduce on the machine that wrote it.

Grouping keeps long key paths readable and makes it easy to clear everything at uninstall:

settings.beginGroup("parcel_tools")
settings.setValue("output_folder", "/data/exports")
settings.setValue("last_layer", "parcels")
settings.endGroup()

settings.remove("parcel_tools")        # tidy up on uninstall

Breakdown: Every beginGroup() must be matched by an endGroup(), so a try/finally or a small context manager is worth having in a long method. remove() on the group deletes the whole subtree, which is the polite thing to do when a user uninstalls your plugin — leaving orphaned keys behind is untidy and occasionally confusing when they reinstall a much later version.

The full set of patterns, including storing lists and structured values, is in Store Plugin Settings with QgsSettings.

An options page inside QGIS settings

A plugin with more than two or three preferences deserves a page in Settings → Options rather than a bespoke dialog nobody can find. QGIS exposes this through a widget factory the plugin registers at startup.

from qgis.gui import QgsOptionsWidgetFactory, QgsOptionsPageWidget

class ParcelToolsOptionsPage(QgsOptionsPageWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # build the form here, load current values from QgsSettings

    def apply(self):
        # called when the user presses OK — write the values back
        ...

class ParcelToolsOptionsFactory(QgsOptionsWidgetFactory):
    def __init__(self):
        super().__init__()
        self.setTitle("Parcel Tools")

    def createWidget(self, parent=None):
        return ParcelToolsOptionsPage(parent)

Breakdown: QgsOptionsPageWidget is a plain widget with one contract: apply() is called when the user accepts the dialog, and that is where settings are written. The factory is registered in your plugin's initGui() with iface.registerOptionsWidgetFactory() and unregistered in unload() — forgetting the second half leaves a page pointing at a plugin that no longer exists, which crashes on the next visit to the options dialog. Users find preferences where the application keeps preferences, and a page here also inherits QGIS's search, its layout conventions and its styling for free. The complete implementation is in Add a Plugin Options Page to QGIS Settings.

An options page is registered, used, then unregisteredDuring initGui the plugin registers an options widget factory. The page then appears in the QGIS options dialog alongside the built-in pages. When the user presses OK, the page's apply method writes the values to settings. During unload the factory is unregistered, without which a reloaded or removed plugin leaves a page that crashes when opened.Every register needs its unregisterinitGui()register the factorypage appearsin Settings and Optionsapply()write to settingsunload()unregisterskip the unregister and the dialog crashesthe page belongs to a plugin object that has been torn down

Making the plugin translatable

Qt's translation system is mature and, once wired up, almost invisible in day-to-day work. Three pieces: mark the strings, extract and translate them, load the result at startup.

Mark every user-visible string with self.tr(). Strings that are not marked cannot be translated, and there is no warning — they simply stay English forever.

from qgis.PyQt.QtCore import QCoreApplication

class ParcelTools:
    def tr(self, message):
        return QCoreApplication.translate("ParcelTools", message)

    def run(self):
        self.iface.messageBar().pushMessage(
            self.tr("Parcel Tools"),
            self.tr("Select a polygon layer before running the analysis."),
        )

Breakdown: The first argument to translate() is the context, conventionally the class name, and it must match the context recorded in the translation file — which it will, because the extraction tool reads it from this same call. Defining a small tr() method per class is the standard convention and keeps call sites short. Strings built by concatenation cannot be translated properly because word order differs between languages: use a format placeholder inside the translated string, self.tr("Processed {} parcels").format(count), so the translator controls the whole sentence.

Extract, translate and compile. pylupdate5 scans the source and .ui files and produces a .ts file per language; translators edit it in Qt Linguist; lrelease compiles it to a binary .qm that ships in the plugin.

Load the translation in your plugin's __init__, before any user-visible string is created — a translator installed after the interface is built changes nothing already on screen. The full command sequence, the .pro file it needs, and the folder layout are covered in Translate a QGIS Plugin with Qt Linguist.

From a marked string to a translated interfaceSource code and user interface files containing tr calls are scanned by the extraction tool, producing one translation source file per language. A translator edits that file in Qt Linguist. The release tool compiles it into a compact binary file shipped inside the plugin. At startup the plugin installs a translator for the current locale, and every marked string appears in the user's language.Five steps, four of them run once per releasetr() in codeand in .ui filespylupdate5writes a .ts fileQt Linguista person translateslreleasecompiles a .qm filestartupinstall translatorShip the .qm files, not the .ts filesand re-run the extraction before every release, or new strings stay untranslated

Beyond words: locale-aware behaviour

Translation is the visible half of localization. The other half is everything that varies with locale even when the words do not.

Numbers and dates. A user with a German locale expects 1.234,56 and 15.08.2026. Qt's QLocale formats both correctly, and Python's str() does not. This matters most in exported reports, where a comma decimal separator can silently corrupt a CSV somebody opens in a spreadsheet.

String assembly. Never build a sentence from fragments. self.tr("Found") + " " + str(n) + " " + self.tr("parcels") cannot be translated into any language whose word order differs from English, which is most of them. One string with a placeholder can.

Layout. German text runs roughly a third longer than English, and Finnish longer still. A dialog laid out with fixed widths against English labels will clip its own buttons in translation; Qt's layout managers exist to prevent exactly this, so use them rather than absolute positions, as described in Load a .ui File at Runtime in PyQGIS.

Plurals. "1 parcels" is the mark of a plugin nobody localized. Qt handles this with translate()'s numeric argument, which lets each language define its own plural rules — several Slavic languages have three forms, not two.

Encoding of user data. Locale affects your interface, not the data. A shapefile written by a Windows user in Greece may be in a legacy code page, and that is a data problem to solve with an explicit encoding on the layer, not a translation problem.

Settings and translation in the plugin lifecycle

Both concerns touch the same three methods, and the ordering matters more than the code.

__init__ installs the translator, because strings created later must already be translatable — this is also where the plugin reads settings it needs before any interface exists. initGui builds actions and menus, whose labels are wrapped in tr(), and registers the options page factory. unload removes the actions, unregisters the factory, and disconnects any signals; it is also the last chance to write out state that should survive.

def unload(self):
    settings = QgsSettings()
    settings.setValue("parcel_tools/last_output_folder", self.output_folder)

    self.iface.unregisterOptionsWidgetFactory(self.options_factory)
    for action in self.actions:
        self.iface.removePluginMenu(self.tr("Parcel Tools"), action)
        self.iface.removeToolBarIcon(action)

Breakdown: Saving on unload rather than on every change is a reasonable compromise for values that change often, though anything the user would be annoyed to lose after a crash should be written when it changes. The unregister and the menu cleanup are not optional: unload() runs on every plugin reload during development, and skipping either leaves duplicate menu items and a stale options page pointing at a dead object. The reload workflow that exercises this constantly is described in Reload a QGIS Plugin Without Restarting.

Migrating settings between versions

Settings outlive releases. Version 2 of your plugin will read keys written by version 1, on a machine where the user never noticed an upgrade happened, and the shape of what you store will eventually change: a single output folder becomes a list of recent folders, a boolean becomes a three-way choice.

The pattern that copes is a schema version stored alongside the values.

from qgis.core import QgsSettings

SETTINGS_VERSION = 2

def migrate_settings():
    settings = QgsSettings()
    stored = settings.value("parcel_tools/settings_version", 1, type=int)

    if stored < 2:
        legacy = settings.value("parcel_tools/output_folder", "", type=str)
        if legacy:
            settings.setValue("parcel_tools/recent_folders", [legacy])
        settings.remove("parcel_tools/output_folder")

    settings.setValue("parcel_tools/settings_version", SETTINGS_VERSION)

Breakdown: Running this once at startup means every later read can assume the current shape, rather than each call site defending against both. Migrations are written as a chain of if stored < n blocks so a user upgrading from version 1 to version 4 passes through all of them in order — which is what actually happens, because people skip releases. Removing the old key after migrating prevents a downgrade-then-upgrade cycle from resurrecting stale data. Writing the version last means an exception mid-migration leaves the old version recorded and the migration retried, rather than half-applied and marked done.

The related discipline is not to change the meaning of an existing key. A key called buffer_distance that meant metres in version 1 and degrees in version 2 will produce results that are wrong by a factor of a hundred thousand on somebody's machine, silently. Add a new key instead, and migrate.

Testing what the user will actually see

Both settings and translations fail in ways that pass a normal test run, because the test machine has the developer's locale and the developer's settings.

Test with a clean settings profile. Launch QGIS with --profile test_profile to get an empty configuration, then run the plugin. Anything that only works because of a value left over from development shows up immediately — and this is the state every new user is in.

Test with a different locale. Setting QGIS_LOCALE or overriding the user interface language in QGIS's own settings switches the interface without changing the operating system. A plugin that has been translated and never viewed in translation usually has at least one clipped label and one string that was never marked.

Test the defaults path in code. A unit test that reads every setting from an empty QgsSettings and asserts the returned types is short, and catches the string-instead-of-boolean problem before a user does. The plugin test setup that makes this practical is described in Unit Test a QGIS Plugin with pytest, and running it across locales in continuous integration costs one matrix entry.

Check the plugin reloads cleanly. Settings written in unload() and read in __init__ make the reload cycle a real test of both. If a reloaded plugin loses its preferences or duplicates its menu, something in that pair is wrong.

Key takeaways

  • Ask whether the value should travel with the project. Yes means a project entry; no means QgsSettings; neither means keep it in memory.
  • Namespace every settings key under your plugin's folder name, and always pass both a default and a type= to value().
  • Publish preferences as an options page in QGIS's own settings dialog, and unregister the factory in unload().
  • Mark every user-visible string with tr() and use placeholders rather than concatenation, so translators control word order.
  • Ship compiled .qm files and re-extract before each release, or new strings quietly stay English.
  • Localization is more than words — number formats, plural rules and text expansion all change with the user's locale.

Frequently Asked Questions

Where does QgsSettings actually store values? In the platform's normal location — the registry on Windows, an INI file under the user's profile on Linux and macOS. You should not depend on the location; use the API, which also reads QGIS's own defaults.

How do I store a list of values? Set a Python list directly and read it back with type=list. For anything structured, serialise to JSON and store the string — it is explicit, portable, and survives a settings backend that only really understands strings.

Can a plugin read QGIS's own settings? Yes, and it is often the right thing to do: the proxy configuration, the default CRS and the locale are all readable, and honouring them is better than asking the user again.

Which language does a plugin use if there is no translation for the user's locale? The strings as written in the source, which is why the source language should be clear, plain English. Qt falls back gracefully; nothing breaks when a .qm file is missing.

Do I need to translate the plugin metadata too?metadata.txt supports localized description and about fields with a language suffix, and the plugin repository shows them. It is a small addition that makes the plugin findable in another language.

Should settings be validated? Yes. A value read from settings is user input that arrived earlier: a folder that no longer exists, a server that has been decommissioned. Validate on read and fall back to the default rather than failing three steps later.

How do I let an organisation preset defaults for everybody? Ship the values in a QGIS user profile, or read an optional configuration file from a known location at startup and use it as the default when no setting exists yet. Both keep the user free to override, which a hard-coded organisational value does not.