[{"data":1,"prerenderedAt":1803},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization":3},{"id":4,"title":5,"body":6,"description":1793,"extension":1794,"meta":1795,"navigation":257,"path":1799,"seo":1800,"stem":1801,"__hash__":1802},"docs\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Findex.md","Plugin Settings and Localization",{"type":7,"value":8,"toc":1780},"minimark",[9,13,17,31,179,184,187,194,200,206,214,218,224,412,442,445,510,535,543,547,550,734,762,876,880,883,892,995,1017,1041,1058,1165,1169,1172,1194,1204,1214,1223,1229,1233,1236,1252,1349,1361,1365,1368,1371,1540,1549,1560,1564,1567,1577,1587,1601,1613,1617,1672,1676,1682,1692,1698,1707,1716,1722,1728,1732,1776],[10,11,5],"h1",{"id":12},"plugin-settings-and-localization",[14,15,16],"p",{},"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.",[14,18,19,20,25,26,30],{},"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 ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002F","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 ",[27,28,29],"code",{},".qm"," file.",[14,32,33],{},[34,35,40,44,48,55,64,74,80,86,90,94,98,106,111,115,120,123,126,129,132,135,138,141,144,149,153,156,159,162,165,168,171,174],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 292","img","Diagram of the three places plugin state can be stored, comparing user settings that persist per installation, project entries that travel with the project file, and temporary in-memory state that lasts one session","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Three homes for plugin state, and how to choose",[45,46,47],"desc",{},"User 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.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","292","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Ask who the value belongs to: the person, the map, or the moment",[49,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"16","46","232","196","10","#eff3ff","#2563eb","stroke-width:2.5",[56,75,79],{"x":76,"y":77,"style":78,"fill":72,"textAnchor":62},"132","72","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","QgsSettings — the person",[56,81,85],{"x":76,"y":82,"style":83,"fill":84,"textAnchor":62},"98","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","last used folder",[56,87,89],{"x":76,"y":88,"style":83,"fill":84,"textAnchor":62},"118","default output format",[56,91,93],{"x":76,"y":92,"style":83,"fill":84,"textAnchor":62},"138","API endpoint and credentials id",[56,95,97],{"x":76,"y":96,"style":83,"fill":84,"textAnchor":62},"158","window size, checkbox states",[49,99],{"x":100,"y":101,"width":102,"height":67,"rx":103,"fill":104,"stroke":72,"style":105},"36","178","192","6","#fffdf7","stroke-width:1.5",[56,107,110],{"x":76,"y":108,"style":109,"fill":72,"textAnchor":62},"199","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","survives restarts",[56,112,114],{"x":76,"y":113,"style":83,"fill":84,"textAnchor":62},"217","never leaves the machine",[49,116],{"x":117,"y":67,"width":68,"height":69,"rx":70,"fill":118,"stroke":119,"style":73},"264","#eef7f4","#0f766e",[56,121,122],{"x":58,"y":77,"style":78,"fill":119,"textAnchor":62},"project entries — the map",[56,124,125],{"x":58,"y":82,"style":83,"fill":84,"textAnchor":62},"which layer holds the survey",[56,127,128],{"x":58,"y":88,"style":83,"fill":84,"textAnchor":62},"the analysis parameters used",[56,130,131],{"x":58,"y":92,"style":83,"fill":84,"textAnchor":62},"a per-project schema name",[56,133,134],{"x":58,"y":96,"style":83,"fill":84,"textAnchor":62},"anything a colleague needs too",[49,136],{"x":137,"y":101,"width":102,"height":67,"rx":103,"fill":104,"stroke":119,"style":105},"284",[56,139,140],{"x":58,"y":108,"style":109,"fill":119,"textAnchor":62},"travels with the file",[56,142,143],{"x":58,"y":113,"style":83,"fill":84,"textAnchor":62},"shared with whoever opens it",[49,145],{"x":146,"y":67,"width":68,"height":69,"rx":70,"fill":147,"stroke":148,"style":73},"512","#fdf2e2","#b45309",[56,150,152],{"x":151,"y":77,"style":78,"fill":148,"textAnchor":62},"628","in memory — the moment",[56,154,155],{"x":151,"y":82,"style":83,"fill":84,"textAnchor":62},"the layer being processed",[56,157,158],{"x":151,"y":88,"style":83,"fill":84,"textAnchor":62},"progress and cancellation flags",[56,160,161],{"x":151,"y":92,"style":83,"fill":84,"textAnchor":62},"a cached lookup table",[56,163,164],{"x":151,"y":96,"style":83,"fill":84,"textAnchor":62},"anything cheap to rebuild",[49,166],{"x":167,"y":101,"width":102,"height":67,"rx":103,"fill":104,"stroke":148,"style":105},"532",[56,169,170],{"x":151,"y":108,"style":109,"fill":148,"textAnchor":62},"gone on unload",[56,172,173],{"x":151,"y":113,"style":83,"fill":84,"textAnchor":62},"and that is correct",[56,175,178],{"x":58,"y":176,"style":83,"fill":177,"textAnchor":62},"272","#59645f","A local path in the project file breaks for everyone else — a project fact in user settings breaks on the next project",[180,181,183],"h2",{"id":182},"where-each-kind-of-state-belongs","Where each kind of state belongs",[14,185,186],{},"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.",[14,188,189,193],{},[190,191,192],"strong",{},"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.",[14,195,196,199],{},[190,197,198],{},"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.",[14,201,202,205],{},[190,203,204],{},"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.",[14,207,208,209,213],{},"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 ",[21,210,212],{"href":211},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002F","Working with QGIS Projects in PyQGIS","; the settings side is the rest of this guide.",[180,215,217],{"id":216},"qgssettings-keys-defaults-and-types","QgsSettings: keys, defaults and types",[14,219,220,223],{},[27,221,222],{},"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.",[225,226,231],"pre",{"className":227,"code":228,"language":229,"meta":230,"style":230},"language-python shiki shiki-themes github-dark","from qgis.core import QgsSettings\n\nsettings = QgsSettings()\nsettings.setValue(\"parcel_tools\u002Foutput_folder\", \"\u002Fdata\u002Fexports\")\nsettings.setValue(\"parcel_tools\u002Fadd_to_map\", True)\nsettings.setValue(\"parcel_tools\u002Fbuffer_distance\", 25.0)\n\nfolder = settings.value(\"parcel_tools\u002Foutput_folder\", \"\", type=str)\nadd_to_map = settings.value(\"parcel_tools\u002Fadd_to_map\", True, type=bool)\ndistance = settings.value(\"parcel_tools\u002Fbuffer_distance\", 10.0, type=float)\n","python","",[27,232,233,252,259,271,290,306,321,326,357,384],{"__ignoreMap":230},[234,235,238,242,246,249],"span",{"class":236,"line":237},"line",1,[234,239,241],{"class":240},"snl16","from",[234,243,245],{"class":244},"s95oV"," qgis.core ",[234,247,248],{"class":240},"import",[234,250,251],{"class":244}," QgsSettings\n",[234,253,255],{"class":236,"line":254},2,[234,256,258],{"emptyLinePlaceholder":257},true,"\n",[234,260,262,265,268],{"class":236,"line":261},3,[234,263,264],{"class":244},"settings ",[234,266,267],{"class":240},"=",[234,269,270],{"class":244}," QgsSettings()\n",[234,272,274,277,281,284,287],{"class":236,"line":273},4,[234,275,276],{"class":244},"settings.setValue(",[234,278,280],{"class":279},"sU2Wk","\"parcel_tools\u002Foutput_folder\"",[234,282,283],{"class":244},", ",[234,285,286],{"class":279},"\"\u002Fdata\u002Fexports\"",[234,288,289],{"class":244},")\n",[234,291,293,295,298,300,304],{"class":236,"line":292},5,[234,294,276],{"class":244},[234,296,297],{"class":279},"\"parcel_tools\u002Fadd_to_map\"",[234,299,283],{"class":244},[234,301,303],{"class":302},"sDLfK","True",[234,305,289],{"class":244},[234,307,309,311,314,316,319],{"class":236,"line":308},6,[234,310,276],{"class":244},[234,312,313],{"class":279},"\"parcel_tools\u002Fbuffer_distance\"",[234,315,283],{"class":244},[234,317,318],{"class":302},"25.0",[234,320,289],{"class":244},[234,322,324],{"class":236,"line":323},7,[234,325,258],{"emptyLinePlaceholder":257},[234,327,329,332,334,337,339,341,344,346,350,352,355],{"class":236,"line":328},8,[234,330,331],{"class":244},"folder ",[234,333,267],{"class":240},[234,335,336],{"class":244}," settings.value(",[234,338,280],{"class":279},[234,340,283],{"class":244},[234,342,343],{"class":279},"\"\"",[234,345,283],{"class":244},[234,347,349],{"class":348},"s9osk","type",[234,351,267],{"class":240},[234,353,354],{"class":302},"str",[234,356,289],{"class":244},[234,358,360,363,365,367,369,371,373,375,377,379,382],{"class":236,"line":359},9,[234,361,362],{"class":244},"add_to_map ",[234,364,267],{"class":240},[234,366,336],{"class":244},[234,368,297],{"class":279},[234,370,283],{"class":244},[234,372,303],{"class":302},[234,374,283],{"class":244},[234,376,349],{"class":348},[234,378,267],{"class":240},[234,380,381],{"class":302},"bool",[234,383,289],{"class":244},[234,385,387,390,392,394,396,398,401,403,405,407,410],{"class":236,"line":386},10,[234,388,389],{"class":244},"distance ",[234,391,267],{"class":240},[234,393,336],{"class":244},[234,395,313],{"class":279},[234,397,283],{"class":244},[234,399,400],{"class":302},"10.0",[234,402,283],{"class":244},[234,404,349],{"class":348},[234,406,267],{"class":240},[234,408,409],{"class":302},"float",[234,411,289],{"class":244},[14,413,414,417,418,421,422,425,426,429,430,433,434,437,438,441],{},[190,415,416],{},"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 ",[27,419,420],{},"output_folder"," at the top level is a genuine hazard. Always pass a default to ",[27,423,424],{},"value()",", because a missing key otherwise returns ",[27,427,428],{},"None"," and the failure lands somewhere far away. Passing ",[27,431,432],{},"type="," is not decoration: on some platforms every value comes back as a string, so ",[27,435,436],{},"value(\"parcel_tools\u002Fadd_to_map\")"," can return the string ",[27,439,440],{},"\"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.",[14,443,444],{},"Grouping keeps long key paths readable and makes it easy to clear everything at uninstall:",[225,446,448],{"className":227,"code":447,"language":229,"meta":230,"style":230},"settings.beginGroup(\"parcel_tools\")\nsettings.setValue(\"output_folder\", \"\u002Fdata\u002Fexports\")\nsettings.setValue(\"last_layer\", \"parcels\")\nsettings.endGroup()\n\nsettings.remove(\"parcel_tools\")        # tidy up on uninstall\n",[27,449,450,460,473,487,492,496],{"__ignoreMap":230},[234,451,452,455,458],{"class":236,"line":237},[234,453,454],{"class":244},"settings.beginGroup(",[234,456,457],{"class":279},"\"parcel_tools\"",[234,459,289],{"class":244},[234,461,462,464,467,469,471],{"class":236,"line":254},[234,463,276],{"class":244},[234,465,466],{"class":279},"\"output_folder\"",[234,468,283],{"class":244},[234,470,286],{"class":279},[234,472,289],{"class":244},[234,474,475,477,480,482,485],{"class":236,"line":261},[234,476,276],{"class":244},[234,478,479],{"class":279},"\"last_layer\"",[234,481,283],{"class":244},[234,483,484],{"class":279},"\"parcels\"",[234,486,289],{"class":244},[234,488,489],{"class":236,"line":273},[234,490,491],{"class":244},"settings.endGroup()\n",[234,493,494],{"class":236,"line":292},[234,495,258],{"emptyLinePlaceholder":257},[234,497,498,501,503,506],{"class":236,"line":308},[234,499,500],{"class":244},"settings.remove(",[234,502,457],{"class":279},[234,504,505],{"class":244},")        ",[234,507,509],{"class":508},"sjoCn","# tidy up on uninstall\n",[14,511,512,514,515,518,519,522,523,526,527,530,531,534],{},[190,513,416],{}," Every ",[27,516,517],{},"beginGroup()"," must be matched by an ",[27,520,521],{},"endGroup()",", so a ",[27,524,525],{},"try","\u002F",[27,528,529],{},"finally"," or a small context manager is worth having in a long method. ",[27,532,533],{},"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.",[14,536,537,538,542],{},"The full set of patterns, including storing lists and structured values, is in ",[21,539,541],{"href":540},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fstore-plugin-settings-with-qgssettings\u002F","Store Plugin Settings with QgsSettings",".",[180,544,546],{"id":545},"an-options-page-inside-qgis-settings","An options page inside QGIS settings",[14,548,549],{},"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.",[225,551,553],{"className":227,"code":552,"language":229,"meta":230,"style":230},"from qgis.gui import QgsOptionsWidgetFactory, QgsOptionsPageWidget\n\nclass ParcelToolsOptionsPage(QgsOptionsPageWidget):\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        # build the form here, load current values from QgsSettings\n\n    def apply(self):\n        # called when the user presses OK — write the values back\n        ...\n\nclass ParcelToolsOptionsFactory(QgsOptionsWidgetFactory):\n    def __init__(self):\n        super().__init__()\n        self.setTitle(\"Parcel Tools\")\n\n    def createWidget(self, parent=None):\n        return ParcelToolsOptionsPage(parent)\n",[27,554,555,567,571,589,606,620,625,629,639,644,649,654,669,678,690,704,709,725],{"__ignoreMap":230},[234,556,557,559,562,564],{"class":236,"line":237},[234,558,241],{"class":240},[234,560,561],{"class":244}," qgis.gui ",[234,563,248],{"class":240},[234,565,566],{"class":244}," QgsOptionsWidgetFactory, QgsOptionsPageWidget\n",[234,568,569],{"class":236,"line":254},[234,570,258],{"emptyLinePlaceholder":257},[234,572,573,576,580,583,586],{"class":236,"line":261},[234,574,575],{"class":240},"class",[234,577,579],{"class":578},"svObZ"," ParcelToolsOptionsPage",[234,581,582],{"class":244},"(",[234,584,585],{"class":578},"QgsOptionsPageWidget",[234,587,588],{"class":244},"):\n",[234,590,591,594,597,600,602,604],{"class":236,"line":273},[234,592,593],{"class":240},"    def",[234,595,596],{"class":302}," __init__",[234,598,599],{"class":244},"(self, parent",[234,601,267],{"class":240},[234,603,428],{"class":302},[234,605,588],{"class":244},[234,607,608,611,614,617],{"class":236,"line":292},[234,609,610],{"class":302},"        super",[234,612,613],{"class":244},"().",[234,615,616],{"class":302},"__init__",[234,618,619],{"class":244},"(parent)\n",[234,621,622],{"class":236,"line":308},[234,623,624],{"class":508},"        # build the form here, load current values from QgsSettings\n",[234,626,627],{"class":236,"line":323},[234,628,258],{"emptyLinePlaceholder":257},[234,630,631,633,636],{"class":236,"line":328},[234,632,593],{"class":240},[234,634,635],{"class":578}," apply",[234,637,638],{"class":244},"(self):\n",[234,640,641],{"class":236,"line":359},[234,642,643],{"class":508},"        # called when the user presses OK — write the values back\n",[234,645,646],{"class":236,"line":386},[234,647,648],{"class":302},"        ...\n",[234,650,652],{"class":236,"line":651},11,[234,653,258],{"emptyLinePlaceholder":257},[234,655,657,659,662,664,667],{"class":236,"line":656},12,[234,658,575],{"class":240},[234,660,661],{"class":578}," ParcelToolsOptionsFactory",[234,663,582],{"class":244},[234,665,666],{"class":578},"QgsOptionsWidgetFactory",[234,668,588],{"class":244},[234,670,672,674,676],{"class":236,"line":671},13,[234,673,593],{"class":240},[234,675,596],{"class":302},[234,677,638],{"class":244},[234,679,681,683,685,687],{"class":236,"line":680},14,[234,682,610],{"class":302},[234,684,613],{"class":244},[234,686,616],{"class":302},[234,688,689],{"class":244},"()\n",[234,691,693,696,699,702],{"class":236,"line":692},15,[234,694,695],{"class":302},"        self",[234,697,698],{"class":244},".setTitle(",[234,700,701],{"class":279},"\"Parcel Tools\"",[234,703,289],{"class":244},[234,705,707],{"class":236,"line":706},16,[234,708,258],{"emptyLinePlaceholder":257},[234,710,712,714,717,719,721,723],{"class":236,"line":711},17,[234,713,593],{"class":240},[234,715,716],{"class":578}," createWidget",[234,718,599],{"class":244},[234,720,267],{"class":240},[234,722,428],{"class":302},[234,724,588],{"class":244},[234,726,728,731],{"class":236,"line":727},18,[234,729,730],{"class":240},"        return",[234,732,733],{"class":244}," ParcelToolsOptionsPage(parent)\n",[14,735,736,738,739,741,742,745,746,749,750,753,754,757,758,542],{},[190,737,416],{}," ",[27,740,585],{}," is a plain widget with one contract: ",[27,743,744],{},"apply()"," is called when the user accepts the dialog, and that is where settings are written. The factory is registered in your plugin's ",[27,747,748],{},"initGui()"," with ",[27,751,752],{},"iface.registerOptionsWidgetFactory()"," and unregistered in ",[27,755,756],{},"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 ",[21,759,761],{"href":760},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fadd-plugin-options-page-to-qgis-settings\u002F","Add a Plugin Options Page to QGIS Settings",[14,763,764],{},[34,765,768,771,774,777,793,796,802,806,810,815,819,822,827,830,833,837,840,843,849,853,857,862,867,871],{"viewBox":766,"role":37,"ariaLabel":767,"xmlns":39},"0 0 760 256","Lifecycle of a registered options page showing registration during initGui, the page appearing in the QGIS options dialog, apply being called on OK, and unregistration during unload",[41,769,770],{},"An options page is registered, used, then unregistered",[45,772,773],{},"During 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.",[49,775],{"x":51,"y":51,"width":52,"height":776,"fill":54},"256",[778,779,780],"defs",{},[781,782,789],"marker",{"id":783,"viewBox":784,"refX":785,"refY":786,"markerWidth":787,"markerHeight":787,"orient":788},"optArrow","0 0 10 10","8","5","7","auto-start-reverse",[790,791],"path",{"d":792,"fill":84},"M0 0 L10 5 L0 10 z",[56,794,795],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Every register needs its unregister",[49,797],{"x":798,"y":799,"width":800,"height":801,"rx":785,"fill":118,"stroke":119,"style":73},"20","60","164","64",[56,803,748],{"x":804,"y":805,"style":109,"fill":119,"textAnchor":62},"102","86",[56,807,809],{"x":804,"y":808,"style":83,"fill":84,"textAnchor":62},"106","register the factory",[49,811],{"x":812,"y":799,"width":813,"height":801,"rx":785,"fill":71,"stroke":72,"style":814},"216","180","stroke-width:2",[56,816,818],{"x":817,"y":805,"style":109,"fill":72,"textAnchor":62},"306","page appears",[56,820,821],{"x":817,"y":808,"style":83,"fill":84,"textAnchor":62},"in Settings and Options",[49,823],{"x":824,"y":799,"width":800,"height":801,"rx":785,"fill":825,"stroke":826,"style":814},"428","#edf8e9","#15803d",[56,828,744],{"x":829,"y":805,"style":109,"fill":826,"textAnchor":62},"510",[56,831,832],{"x":829,"y":808,"style":83,"fill":84,"textAnchor":62},"write to settings",[49,834],{"x":835,"y":799,"width":836,"height":801,"rx":785,"fill":147,"stroke":148,"style":73},"624","116",[56,838,756],{"x":839,"y":805,"style":109,"fill":148,"textAnchor":62},"682",[56,841,842],{"x":839,"y":808,"style":83,"fill":84,"textAnchor":62},"unregister",[236,844],{"x1":845,"y1":846,"x2":847,"y2":846,"stroke":84,"style":848},"184","92","210","stroke-width:2;marker-end:url(#optArrow)",[236,850],{"x1":851,"y1":846,"x2":852,"y2":846,"stroke":84,"style":848},"396","422",[236,854],{"x1":855,"y1":846,"x2":856,"y2":846,"stroke":84,"style":848},"592","618",[49,858],{"x":812,"y":859,"width":860,"height":801,"rx":785,"fill":147,"stroke":861,"style":73},"160","376","#b91c1c",[56,863,866],{"x":864,"y":865,"style":78,"fill":861,"textAnchor":62},"404","186","skip the unregister and the dialog crashes",[56,868,870],{"x":864,"y":869,"style":83,"fill":84,"textAnchor":62},"206","the page belongs to a plugin object that has been torn down",[236,872],{"x1":839,"y1":873,"x2":874,"y2":800,"stroke":861,"style":875},"124","600","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#optArrow)",[180,877,879],{"id":878},"making-the-plugin-translatable","Making the plugin translatable",[14,881,882],{},"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.",[14,884,885,749,888,891],{},[190,886,887],{},"Mark every user-visible string",[27,889,890],{},"self.tr()",". Strings that are not marked cannot be translated, and there is no warning — they simply stay English forever.",[225,893,895],{"className":227,"code":894,"language":229,"meta":230,"style":230},"from qgis.PyQt.QtCore import QCoreApplication\n\nclass ParcelTools:\n    def tr(self, message):\n        return QCoreApplication.translate(\"ParcelTools\", message)\n\n    def run(self):\n        self.iface.messageBar().pushMessage(\n            self.tr(\"Parcel Tools\"),\n            self.tr(\"Select a polygon layer before running the analysis.\"),\n        )\n",[27,896,897,909,913,923,933,946,950,959,966,979,990],{"__ignoreMap":230},[234,898,899,901,904,906],{"class":236,"line":237},[234,900,241],{"class":240},[234,902,903],{"class":244}," qgis.PyQt.QtCore ",[234,905,248],{"class":240},[234,907,908],{"class":244}," QCoreApplication\n",[234,910,911],{"class":236,"line":254},[234,912,258],{"emptyLinePlaceholder":257},[234,914,915,917,920],{"class":236,"line":261},[234,916,575],{"class":240},[234,918,919],{"class":578}," ParcelTools",[234,921,922],{"class":244},":\n",[234,924,925,927,930],{"class":236,"line":273},[234,926,593],{"class":240},[234,928,929],{"class":578}," tr",[234,931,932],{"class":244},"(self, message):\n",[234,934,935,937,940,943],{"class":236,"line":292},[234,936,730],{"class":240},[234,938,939],{"class":244}," QCoreApplication.translate(",[234,941,942],{"class":279},"\"ParcelTools\"",[234,944,945],{"class":244},", message)\n",[234,947,948],{"class":236,"line":308},[234,949,258],{"emptyLinePlaceholder":257},[234,951,952,954,957],{"class":236,"line":323},[234,953,593],{"class":240},[234,955,956],{"class":578}," run",[234,958,638],{"class":244},[234,960,961,963],{"class":236,"line":328},[234,962,695],{"class":302},[234,964,965],{"class":244},".iface.messageBar().pushMessage(\n",[234,967,968,971,974,976],{"class":236,"line":359},[234,969,970],{"class":302},"            self",[234,972,973],{"class":244},".tr(",[234,975,701],{"class":279},[234,977,978],{"class":244},"),\n",[234,980,981,983,985,988],{"class":236,"line":386},[234,982,970],{"class":302},[234,984,973],{"class":244},[234,986,987],{"class":279},"\"Select a polygon layer before running the analysis.\"",[234,989,978],{"class":244},[234,991,992],{"class":236,"line":651},[234,993,994],{"class":244},"        )\n",[14,996,997,999,1000,1003,1004,1008,1009,1012,1013,1016],{},[190,998,416],{}," The first argument to ",[27,1001,1002],{},"translate()"," is the ",[1005,1006,1007],"em",{},"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 ",[27,1010,1011],{},"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, ",[27,1014,1015],{},"self.tr(\"Processed {} parcels\").format(count)",", so the translator controls the whole sentence.",[14,1018,1019,738,1022,1025,1026,1029,1030,1033,1034,1037,1038,1040],{},[190,1020,1021],{},"Extract, translate and compile.",[27,1023,1024],{},"pylupdate5"," scans the source and ",[27,1027,1028],{},".ui"," files and produces a ",[27,1031,1032],{},".ts"," file per language; translators edit it in Qt Linguist; ",[27,1035,1036],{},"lrelease"," compiles it to a binary ",[27,1039,29],{}," that ships in the plugin.",[14,1042,1043,1046,1047,1049,1050,1053,1054,542],{},[190,1044,1045],{},"Load the translation"," in your plugin's ",[27,1048,616],{},", 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 ",[27,1051,1052],{},".pro"," file it needs, and the folder layout are covered in ",[21,1055,1057],{"href":1056},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Ftranslate-qgis-plugin-with-qt-linguist\u002F","Translate a QGIS Plugin with Qt Linguist",[14,1059,1060],{},[34,1061,1064,1067,1070,1073,1080,1083,1087,1092,1096,1099,1102,1105,1108,1112,1115,1118,1121,1124,1128,1132,1135,1141,1145,1149,1153,1158,1162],{"viewBox":1062,"role":37,"ariaLabel":1063,"xmlns":39},"0 0 760 246","The translation pipeline from marked source strings through extraction to a translation file, translation in Qt Linguist, compilation to a binary file and loading at plugin startup",[41,1065,1066],{},"From a marked string to a translated interface",[45,1068,1069],{},"Source 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.",[49,1071],{"x":51,"y":51,"width":52,"height":1072,"fill":54},"246",[778,1074,1075],{},[781,1076,1078],{"id":1077,"viewBox":784,"refX":785,"refY":786,"markerWidth":787,"markerHeight":787,"orient":788},"i18nArrow",[790,1079],{"d":792,"fill":84},[56,1081,1082],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Five steps, four of them run once per release",[49,1084],{"x":1085,"y":799,"width":76,"height":1086,"rx":785,"fill":118,"stroke":119,"style":814},"12","70",[56,1088,1091],{"x":1089,"y":1090,"style":109,"fill":119,"textAnchor":62},"78","88","tr() in code",[56,1093,1095],{"x":1089,"y":1094,"style":83,"fill":84,"textAnchor":62},"108","and in .ui files",[49,1097],{"x":1098,"y":799,"width":76,"height":1086,"rx":785,"fill":104,"stroke":177,"style":814},"168",[56,1100,1024],{"x":1101,"y":1090,"style":109,"fill":61,"textAnchor":62},"234",[56,1103,1104],{"x":1101,"y":1094,"style":83,"fill":84,"textAnchor":62},"writes a .ts file",[49,1106],{"x":1107,"y":799,"width":76,"height":1086,"rx":785,"fill":71,"stroke":72,"style":814},"324",[56,1109,1111],{"x":1110,"y":1090,"style":109,"fill":72,"textAnchor":62},"390","Qt Linguist",[56,1113,1114],{"x":1110,"y":1094,"style":83,"fill":84,"textAnchor":62},"a person translates",[49,1116],{"x":1117,"y":799,"width":76,"height":1086,"rx":785,"fill":104,"stroke":177,"style":814},"480",[56,1119,1036],{"x":1120,"y":1090,"style":109,"fill":61,"textAnchor":62},"546",[56,1122,1123],{"x":1120,"y":1094,"style":83,"fill":84,"textAnchor":62},"compiles a .qm file",[49,1125],{"x":1126,"y":799,"width":1127,"height":1086,"rx":785,"fill":825,"stroke":826,"style":73},"636","112",[56,1129,1131],{"x":1130,"y":1090,"style":109,"fill":826,"textAnchor":62},"692","startup",[56,1133,1134],{"x":1130,"y":1094,"style":83,"fill":84,"textAnchor":62},"install translator",[236,1136],{"x1":1137,"y1":1138,"x2":1139,"y2":1138,"stroke":84,"style":1140},"144","95","162","stroke-width:2;marker-end:url(#i18nArrow)",[236,1142],{"x1":1143,"y1":1138,"x2":1144,"y2":1138,"stroke":84,"style":1140},"300","318",[236,1146],{"x1":1147,"y1":1138,"x2":1148,"y2":1138,"stroke":84,"style":1140},"456","474",[236,1150],{"x1":1151,"y1":1138,"x2":1152,"y2":1138,"stroke":84,"style":1140},"612","630",[49,1154],{"x":799,"y":1155,"width":1156,"height":1157,"rx":785,"fill":147,"stroke":148,"style":814},"166","640","58",[56,1159,1161],{"x":58,"y":1160,"style":109,"fill":148,"textAnchor":62},"190","Ship the .qm files, not the .ts files",[56,1163,1164],{"x":58,"y":847,"style":83,"fill":84,"textAnchor":62},"and re-run the extraction before every release, or new strings stay untranslated",[180,1166,1168],{"id":1167},"beyond-words-locale-aware-behaviour","Beyond words: locale-aware behaviour",[14,1170,1171],{},"Translation is the visible half of localization. The other half is everything that varies with locale even when the words do not.",[14,1173,1174,1177,1178,1181,1182,1185,1186,1189,1190,1193],{},[190,1175,1176],{},"Numbers and dates."," A user with a German locale expects ",[27,1179,1180],{},"1.234,56"," and ",[27,1183,1184],{},"15.08.2026",". Qt's ",[27,1187,1188],{},"QLocale"," formats both correctly, and Python's ",[27,1191,1192],{},"str()"," does not. This matters most in exported reports, where a comma decimal separator can silently corrupt a CSV somebody opens in a spreadsheet.",[14,1195,1196,1199,1200,1203],{},[190,1197,1198],{},"String assembly."," Never build a sentence from fragments. ",[27,1201,1202],{},"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.",[14,1205,1206,1209,1210,542],{},[190,1207,1208],{},"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 ",[21,1211,1213],{"href":1212},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fload-ui-file-at-runtime-pyqgis\u002F","Load a .ui File at Runtime in PyQGIS",[14,1215,1216,1219,1220,1222],{},[190,1217,1218],{},"Plurals."," \"1 parcels\" is the mark of a plugin nobody localized. Qt handles this with ",[27,1221,1002],{},"'s numeric argument, which lets each language define its own plural rules — several Slavic languages have three forms, not two.",[14,1224,1225,1228],{},[190,1226,1227],{},"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.",[180,1230,1232],{"id":1231},"settings-and-translation-in-the-plugin-lifecycle","Settings and translation in the plugin lifecycle",[14,1234,1235],{},"Both concerns touch the same three methods, and the ordering matters more than the code.",[14,1237,1238,1240,1241,1244,1245,1247,1248,1251],{},[27,1239,616],{}," 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. ",[27,1242,1243],{},"initGui"," builds actions and menus, whose labels are wrapped in ",[27,1246,1011],{},", and registers the options page factory. ",[27,1249,1250],{},"unload"," removes the actions, unregisters the factory, and disconnects any signals; it is also the last chance to write out state that should survive.",[225,1253,1255],{"className":227,"code":1254,"language":229,"meta":230,"style":230},"def unload(self):\n    settings = QgsSettings()\n    settings.setValue(\"parcel_tools\u002Flast_output_folder\", self.output_folder)\n\n    self.iface.unregisterOptionsWidgetFactory(self.options_factory)\n    for action in self.actions:\n        self.iface.removePluginMenu(self.tr(\"Parcel Tools\"), action)\n        self.iface.removeToolBarIcon(action)\n",[27,1256,1257,1267,1276,1292,1296,1309,1326,1342],{"__ignoreMap":230},[234,1258,1259,1262,1265],{"class":236,"line":237},[234,1260,1261],{"class":240},"def",[234,1263,1264],{"class":578}," unload",[234,1266,638],{"class":244},[234,1268,1269,1272,1274],{"class":236,"line":254},[234,1270,1271],{"class":244},"    settings ",[234,1273,267],{"class":240},[234,1275,270],{"class":244},[234,1277,1278,1281,1284,1286,1289],{"class":236,"line":261},[234,1279,1280],{"class":244},"    settings.setValue(",[234,1282,1283],{"class":279},"\"parcel_tools\u002Flast_output_folder\"",[234,1285,283],{"class":244},[234,1287,1288],{"class":302},"self",[234,1290,1291],{"class":244},".output_folder)\n",[234,1293,1294],{"class":236,"line":273},[234,1295,258],{"emptyLinePlaceholder":257},[234,1297,1298,1301,1304,1306],{"class":236,"line":292},[234,1299,1300],{"class":302},"    self",[234,1302,1303],{"class":244},".iface.unregisterOptionsWidgetFactory(",[234,1305,1288],{"class":302},[234,1307,1308],{"class":244},".options_factory)\n",[234,1310,1311,1314,1317,1320,1323],{"class":236,"line":308},[234,1312,1313],{"class":240},"    for",[234,1315,1316],{"class":244}," action ",[234,1318,1319],{"class":240},"in",[234,1321,1322],{"class":302}," self",[234,1324,1325],{"class":244},".actions:\n",[234,1327,1328,1330,1333,1335,1337,1339],{"class":236,"line":323},[234,1329,695],{"class":302},[234,1331,1332],{"class":244},".iface.removePluginMenu(",[234,1334,1288],{"class":302},[234,1336,973],{"class":244},[234,1338,701],{"class":279},[234,1340,1341],{"class":244},"), action)\n",[234,1343,1344,1346],{"class":236,"line":328},[234,1345,695],{"class":302},[234,1347,1348],{"class":244},".iface.removeToolBarIcon(action)\n",[14,1350,1351,1353,1354,1356,1357,542],{},[190,1352,416],{}," 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: ",[27,1355,756],{}," 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 ",[21,1358,1360],{"href":1359},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","Reload a QGIS Plugin Without Restarting",[180,1362,1364],{"id":1363},"migrating-settings-between-versions","Migrating settings between versions",[14,1366,1367],{},"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.",[14,1369,1370],{},"The pattern that copes is a schema version stored alongside the values.",[225,1372,1374],{"className":227,"code":1373,"language":229,"meta":230,"style":230},"from qgis.core import QgsSettings\n\nSETTINGS_VERSION = 2\n\ndef migrate_settings():\n    settings = QgsSettings()\n    stored = settings.value(\"parcel_tools\u002Fsettings_version\", 1, type=int)\n\n    if stored \u003C 2:\n        legacy = settings.value(\"parcel_tools\u002Foutput_folder\", \"\", type=str)\n        if legacy:\n            settings.setValue(\"parcel_tools\u002Frecent_folders\", [legacy])\n        settings.remove(\"parcel_tools\u002Foutput_folder\")\n\n    settings.setValue(\"parcel_tools\u002Fsettings_version\", SETTINGS_VERSION)\n",[27,1375,1376,1386,1390,1401,1405,1415,1423,1451,1455,1471,1496,1504,1515,1524,1528],{"__ignoreMap":230},[234,1377,1378,1380,1382,1384],{"class":236,"line":237},[234,1379,241],{"class":240},[234,1381,245],{"class":244},[234,1383,248],{"class":240},[234,1385,251],{"class":244},[234,1387,1388],{"class":236,"line":254},[234,1389,258],{"emptyLinePlaceholder":257},[234,1391,1392,1395,1398],{"class":236,"line":261},[234,1393,1394],{"class":302},"SETTINGS_VERSION",[234,1396,1397],{"class":240}," =",[234,1399,1400],{"class":302}," 2\n",[234,1402,1403],{"class":236,"line":273},[234,1404,258],{"emptyLinePlaceholder":257},[234,1406,1407,1409,1412],{"class":236,"line":292},[234,1408,1261],{"class":240},[234,1410,1411],{"class":578}," migrate_settings",[234,1413,1414],{"class":244},"():\n",[234,1416,1417,1419,1421],{"class":236,"line":308},[234,1418,1271],{"class":244},[234,1420,267],{"class":240},[234,1422,270],{"class":244},[234,1424,1425,1428,1430,1432,1435,1437,1440,1442,1444,1446,1449],{"class":236,"line":323},[234,1426,1427],{"class":244},"    stored ",[234,1429,267],{"class":240},[234,1431,336],{"class":244},[234,1433,1434],{"class":279},"\"parcel_tools\u002Fsettings_version\"",[234,1436,283],{"class":244},[234,1438,1439],{"class":302},"1",[234,1441,283],{"class":244},[234,1443,349],{"class":348},[234,1445,267],{"class":240},[234,1447,1448],{"class":302},"int",[234,1450,289],{"class":244},[234,1452,1453],{"class":236,"line":328},[234,1454,258],{"emptyLinePlaceholder":257},[234,1456,1457,1460,1463,1466,1469],{"class":236,"line":359},[234,1458,1459],{"class":240},"    if",[234,1461,1462],{"class":244}," stored ",[234,1464,1465],{"class":240},"\u003C",[234,1467,1468],{"class":302}," 2",[234,1470,922],{"class":244},[234,1472,1473,1476,1478,1480,1482,1484,1486,1488,1490,1492,1494],{"class":236,"line":386},[234,1474,1475],{"class":244},"        legacy ",[234,1477,267],{"class":240},[234,1479,336],{"class":244},[234,1481,280],{"class":279},[234,1483,283],{"class":244},[234,1485,343],{"class":279},[234,1487,283],{"class":244},[234,1489,349],{"class":348},[234,1491,267],{"class":240},[234,1493,354],{"class":302},[234,1495,289],{"class":244},[234,1497,1498,1501],{"class":236,"line":651},[234,1499,1500],{"class":240},"        if",[234,1502,1503],{"class":244}," legacy:\n",[234,1505,1506,1509,1512],{"class":236,"line":656},[234,1507,1508],{"class":244},"            settings.setValue(",[234,1510,1511],{"class":279},"\"parcel_tools\u002Frecent_folders\"",[234,1513,1514],{"class":244},", [legacy])\n",[234,1516,1517,1520,1522],{"class":236,"line":671},[234,1518,1519],{"class":244},"        settings.remove(",[234,1521,280],{"class":279},[234,1523,289],{"class":244},[234,1525,1526],{"class":236,"line":680},[234,1527,258],{"emptyLinePlaceholder":257},[234,1529,1530,1532,1534,1536,1538],{"class":236,"line":692},[234,1531,1280],{"class":244},[234,1533,1434],{"class":279},[234,1535,283],{"class":244},[234,1537,1394],{"class":302},[234,1539,289],{"class":244},[14,1541,1542,1544,1545,1548],{},[190,1543,416],{}," 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 ",[27,1546,1547],{},"if stored \u003C 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.",[14,1550,1551,1552,1555,1556,1559],{},"The related discipline is not to change the ",[1005,1553,1554],{},"meaning"," of an existing key. A key called ",[27,1557,1558],{},"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.",[180,1561,1563],{"id":1562},"testing-what-the-user-will-actually-see","Testing what the user will actually see",[14,1565,1566],{},"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.",[14,1568,1569,1572,1573,1576],{},[190,1570,1571],{},"Test with a clean settings profile."," Launch QGIS with ",[27,1574,1575],{},"--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.",[14,1578,1579,1582,1583,1586],{},[190,1580,1581],{},"Test with a different locale."," Setting ",[27,1584,1585],{},"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.",[14,1588,1589,1592,1593,1595,1596,1600],{},[190,1590,1591],{},"Test the defaults path in code."," A unit test that reads every setting from an empty ",[27,1594,222],{}," 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 ",[21,1597,1599],{"href":1598},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","Unit Test a QGIS Plugin with pytest",", and running it across locales in continuous integration costs one matrix entry.",[14,1602,1603,1606,1607,1609,1610,1612],{},[190,1604,1605],{},"Check the plugin reloads cleanly."," Settings written in ",[27,1608,756],{}," and read in ",[27,1611,616],{}," 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.",[180,1614,1616],{"id":1615},"key-takeaways","Key takeaways",[1618,1619,1620,1630,1641,1649,1657,1666],"ul",{},[1621,1622,1623,1626,1627,1629],"li",{},[190,1624,1625],{},"Ask whether the value should travel with the project."," Yes means a project entry; no means ",[27,1628,222],{},"; neither means keep it in memory.",[1621,1631,1632,1635,1636,1638,1639,542],{},[190,1633,1634],{},"Namespace every settings key"," under your plugin's folder name, and always pass both a default and a ",[27,1637,432],{}," to ",[27,1640,424],{},[1621,1642,1643,1646,1647,542],{},[190,1644,1645],{},"Publish preferences as an options page"," in QGIS's own settings dialog, and unregister the factory in ",[27,1648,756],{},[1621,1650,1651,1656],{},[190,1652,1653,1654],{},"Mark every user-visible string with ",[27,1655,1011],{}," and use placeholders rather than concatenation, so translators control word order.",[1621,1658,1659,1665],{},[190,1660,1661,1662,1664],{},"Ship compiled ",[27,1663,29],{}," files"," and re-extract before each release, or new strings quietly stay English.",[1621,1667,1668,1671],{},[190,1669,1670],{},"Localization is more than words"," — number formats, plural rules and text expansion all change with the user's locale.",[180,1673,1675],{"id":1674},"frequently-asked-questions","Frequently Asked Questions",[14,1677,1678,1681],{},[190,1679,1680],{},"Where does QgsSettings actually store values?","\nIn 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.",[14,1683,1684,1687,1688,1691],{},[190,1685,1686],{},"How do I store a list of values?","\nSet a Python list directly and read it back with ",[27,1689,1690],{},"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.",[14,1693,1694,1697],{},[190,1695,1696],{},"Can a plugin read QGIS's own settings?","\nYes, 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.",[14,1699,1700,1703,1704,1706],{},[190,1701,1702],{},"Which language does a plugin use if there is no translation for the user's locale?","\nThe 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 ",[27,1705,29],{}," file is missing.",[14,1708,1709,1712,1715],{},[190,1710,1711],{},"Do I need to translate the plugin metadata too?",[27,1713,1714],{},"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.",[14,1717,1718,1721],{},[190,1719,1720],{},"Should settings be validated?","\nYes. 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.",[14,1723,1724,1727],{},[190,1725,1726],{},"How do I let an organisation preset defaults for everybody?","\nShip 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.",[180,1729,1731],{"id":1730},"related-guides","Related Guides",[1618,1733,1734,1740,1746,1752,1758,1762,1766,1770],{},[1621,1735,1736,1737,1739],{},"Up: ",[21,1738,24],{"href":23}," — the parent guide for this topic",[1621,1741,1742],{},[21,1743,1745],{"href":1744},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate and Structure",[1621,1747,1748],{},[21,1749,1751],{"href":1750},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for GIS Interfaces",[1621,1753,1754],{},[21,1755,1757],{"href":1756},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002F","Publishing to the QGIS Plugin Repository",[1621,1759,1760],{},[21,1761,541],{"href":540},[1621,1763,1764],{},[21,1765,761],{"href":760},[1621,1767,1768],{},[21,1769,1057],{"href":1056},[1621,1771,1772],{},[21,1773,1775],{"href":1774},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fremember-last-used-folder-in-plugin-pyqgis\u002F","Remember the Last Used Folder in a Plugin",[1777,1778,1779],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":230,"searchDepth":254,"depth":254,"links":1781},[1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792],{"id":182,"depth":254,"text":183},{"id":216,"depth":254,"text":217},{"id":545,"depth":254,"text":546},{"id":878,"depth":254,"text":879},{"id":1167,"depth":254,"text":1168},{"id":1231,"depth":254,"text":1232},{"id":1363,"depth":254,"text":1364},{"id":1562,"depth":254,"text":1563},{"id":1615,"depth":254,"text":1616},{"id":1674,"depth":254,"text":1675},{"id":1730,"depth":254,"text":1731},"Give a QGIS plugin memory and a language — where each kind of state belongs, QgsSettings namespacing and defaults, a proper options page, and the Qt translation workflow from tr() to a shipped .qm file.","md",{"slug":12,"type":1796,"breadcrumb":1797,"datePublished":1798,"dateModified":1798},"guide","Settings & Localization","2026-08-15","\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization",{"title":5,"description":1793},"qgis-plugin-development\u002Fplugin-settings-and-localization\u002Findex","TujnOpX3TIBGuPeCd6w7b_KdDfRLi3QhqeWY95X7kYs",1786789584641]