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.
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.
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.
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=tovalue(). - 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
.qmfiles 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.
Related Guides
- Up: QGIS Plugin Development — the parent guide for this topic
- Plugin Boilerplate and Structure
- Qt Designer for GIS Interfaces
- Publishing to the QGIS Plugin Repository
- Store Plugin Settings with QgsSettings
- Add a Plugin Options Page to QGIS Settings
- Translate a QGIS Plugin with Qt Linguist
- Remember the Last Used Folder in a Plugin