[{"data":1,"prerenderedAt":1351},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Ftranslate-qgis-plugin-with-qt-linguist":3},{"id":4,"title":5,"body":6,"description":1340,"extension":1341,"meta":1342,"navigation":245,"path":1347,"seo":1348,"stem":1349,"__hash__":1350},"docs\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Ftranslate-qgis-plugin-with-qt-linguist\u002Findex.md","Translate a QGIS Plugin with Qt Linguist",{"type":7,"value":8,"toc":1326},"minimark",[9,13,17,26,174,179,209,213,398,425,437,441,444,451,454,468,500,504,510,522,539,650,654,854,886,964,968,971,993,1005,1020,1024,1027,1033,1043,1049,1055,1061,1067,1070,1074,1151,1154,1158,1215,1219,1240,1244,1250,1266,1276,1282,1291,1295,1322],[10,11,5],"h1",{"id":12},"translate-a-qgis-plugin-with-qt-linguist",[14,15,16],"p",{},"QGIS ships in more than forty languages, and a large share of its users work in one that is not English. A plugin whose interface is English-only is usable by them, in the way a manual in a second language is usable — slowly, and with a running cost of small misunderstandings. The translation machinery is Qt's, it is mature, and wiring it up is an afternoon that then costs almost nothing to maintain.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002F","Plugin Settings and Localization",". It walks the whole pipeline: marking strings, extracting them, translating, compiling, loading, and keeping the files current when the plugin changes.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,102,108,111,115,118,122,126,131,137,141,145,149,153,156,158,161,163,166],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 268","img","Folder layout of a translatable plugin showing source files with marked strings, an i18n folder holding translation source files per language and the compiled binary files that ship with the plugin","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"What lives where in a translatable plugin",[40,41,42],"desc",{},"The plugin folder holds the Python sources and user interface files that contain marked strings, a project file listing what to scan, and an i18n folder. Inside the i18n folder, one editable translation file per language sits beside its compiled binary counterpart. Only the compiled files are needed at run time, and both are usually kept in version control.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","268","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"i18nFolderArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Two files per language: one people edit, one QGIS loads",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"20","48","300","196","10","#eef7f4","#0f766e","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":84,"textAnchor":74},"170","74","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","the plugin folder",[44,93],{"x":94,"y":95,"width":96,"height":97,"rx":98,"fill":99,"stroke":100,"style":101},"42","90","256","30","6","#fffdf7","#59645f","stroke-width:1.5",[68,103,107],{"x":104,"y":105,"style":106,"fill":66},"58","110","font-size:11px;font-family:sans-serif","parcel tools.py — tr() calls",[44,109],{"x":94,"y":110,"width":96,"height":97,"rx":98,"fill":99,"stroke":100,"style":101},"126",[68,112,114],{"x":104,"y":113,"style":106,"fill":66},"146","dialog.ui — translatable labels",[44,116],{"x":94,"y":117,"width":96,"height":97,"rx":98,"fill":99,"stroke":100,"style":101},"162",[68,119,121],{"x":104,"y":120,"style":106,"fill":66},"182","parcel tools.pro — what to scan",[44,123],{"x":94,"y":124,"width":96,"height":97,"rx":98,"fill":83,"stroke":84,"style":125},"198","stroke-width:2",[68,127,130],{"x":104,"y":128,"style":129,"fill":84},"218","font-size:11px;font-weight:bold;font-family:sans-serif","i18n\u002F",[44,132],{"x":133,"y":79,"width":134,"height":81,"rx":82,"fill":135,"stroke":136,"style":85},"392","348","#eff3ff","#2563eb",[68,138,140],{"x":139,"y":89,"style":90,"fill":136,"textAnchor":74},"566","inside i18n\u002F",[44,142],{"x":143,"y":95,"width":144,"height":97,"rx":98,"fill":99,"stroke":100,"style":101},"414","304",[68,146,148],{"x":147,"y":105,"style":106,"fill":66},"430","parcel tools de.ts — editable, in Linguist",[44,150],{"x":143,"y":110,"width":144,"height":97,"rx":98,"fill":151,"stroke":152,"style":125},"#edf8e9","#15803d",[68,154,155],{"x":147,"y":113,"style":106,"fill":66},"parcel tools de.qm — compiled, loaded",[44,157],{"x":143,"y":117,"width":144,"height":97,"rx":98,"fill":99,"stroke":100,"style":101},[68,159,160],{"x":147,"y":120,"style":106,"fill":66},"parcel tools fr.ts",[44,162],{"x":143,"y":124,"width":144,"height":97,"rx":98,"fill":151,"stroke":152,"style":125},[68,164,165],{"x":147,"y":128,"style":106,"fill":66},"parcel tools fr.qm",[167,168],"line",{"x1":169,"y1":170,"x2":171,"y2":172,"stroke":66,"style":173},"298","213","386","150","stroke-width:2;marker-end:url(#i18nFolderArrow)",[175,176,178],"h2",{"id":177},"prerequisites","Prerequisites",[180,181,182,190,202],"ul",{},[183,184,185,189],"li",{},[186,187,188],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[183,191,192,193,197,198,201],{},"Qt tooling: ",[194,195,196],"code",{},"pylupdate5"," (from PyQt5) and ",[194,199,200],{},"lrelease"," (from Qt), both usually available in the QGIS install or through your package manager.",[183,203,204,205,208],{},"Optionally Qt Linguist, the graphical editor translators use. The ",[194,206,207],{},".ts"," format is XML, so a text editor works in a pinch.",[175,210,212],{"id":211},"mark-every-user-visible-string","Mark every user-visible string",[214,215,220],"pre",{"className":216,"code":217,"language":218,"meta":219,"style":219},"language-python shiki shiki-themes github-dark","from qgis.PyQt.QtCore import QCoreApplication\n\n\nclass ParcelTools:\n    def tr(self, message):\n        return QCoreApplication.translate(\"ParcelTools\", message)\n\n    def initGui(self):\n        self.action = QAction(self.tr(\"Analyse parcels\"), self.iface.mainWindow())\n        self.action.setStatusTip(self.tr(\"Summarise parcel areas by ward\"))\n\n    def report(self, count):\n        return self.tr(\"Processed {} parcels\").format(count)\n","python","",[194,221,222,240,247,252,265,277,293,298,309,342,360,365,376],{"__ignoreMap":219},[223,224,226,230,234,237],"span",{"class":167,"line":225},1,[223,227,229],{"class":228},"snl16","from",[223,231,233],{"class":232},"s95oV"," qgis.PyQt.QtCore ",[223,235,236],{"class":228},"import",[223,238,239],{"class":232}," QCoreApplication\n",[223,241,243],{"class":167,"line":242},2,[223,244,246],{"emptyLinePlaceholder":245},true,"\n",[223,248,250],{"class":167,"line":249},3,[223,251,246],{"emptyLinePlaceholder":245},[223,253,255,258,262],{"class":167,"line":254},4,[223,256,257],{"class":228},"class",[223,259,261],{"class":260},"svObZ"," ParcelTools",[223,263,264],{"class":232},":\n",[223,266,268,271,274],{"class":167,"line":267},5,[223,269,270],{"class":228},"    def",[223,272,273],{"class":260}," tr",[223,275,276],{"class":232},"(self, message):\n",[223,278,280,283,286,290],{"class":167,"line":279},6,[223,281,282],{"class":228},"        return",[223,284,285],{"class":232}," QCoreApplication.translate(",[223,287,289],{"class":288},"sU2Wk","\"ParcelTools\"",[223,291,292],{"class":232},", message)\n",[223,294,296],{"class":167,"line":295},7,[223,297,246],{"emptyLinePlaceholder":245},[223,299,301,303,306],{"class":167,"line":300},8,[223,302,270],{"class":228},[223,304,305],{"class":260}," initGui",[223,307,308],{"class":232},"(self):\n",[223,310,312,316,319,322,325,328,331,334,337,339],{"class":167,"line":311},9,[223,313,315],{"class":314},"sDLfK","        self",[223,317,318],{"class":232},".action ",[223,320,321],{"class":228},"=",[223,323,324],{"class":232}," QAction(",[223,326,327],{"class":314},"self",[223,329,330],{"class":232},".tr(",[223,332,333],{"class":288},"\"Analyse parcels\"",[223,335,336],{"class":232},"), ",[223,338,327],{"class":314},[223,340,341],{"class":232},".iface.mainWindow())\n",[223,343,345,347,350,352,354,357],{"class":167,"line":344},10,[223,346,315],{"class":314},[223,348,349],{"class":232},".action.setStatusTip(",[223,351,327],{"class":314},[223,353,330],{"class":232},[223,355,356],{"class":288},"\"Summarise parcel areas by ward\"",[223,358,359],{"class":232},"))\n",[223,361,363],{"class":167,"line":362},11,[223,364,246],{"emptyLinePlaceholder":245},[223,366,368,370,373],{"class":167,"line":367},12,[223,369,270],{"class":228},[223,371,372],{"class":260}," report",[223,374,375],{"class":232},"(self, count):\n",[223,377,379,381,384,386,389,392,395],{"class":167,"line":378},13,[223,380,282],{"class":228},[223,382,383],{"class":314}," self",[223,385,330],{"class":232},[223,387,388],{"class":288},"\"Processed ",[223,390,391],{"class":314},"{}",[223,393,394],{"class":288}," parcels\"",[223,396,397],{"class":232},").format(count)\n",[14,399,400,403,404,407,408,412,413,416,417,420,421,424],{},[186,401,402],{},"Breakdown:"," The first argument to ",[194,405,406],{},"translate()"," is the ",[409,410,411],"em",{},"context",", conventionally the class name; it groups strings in the translation file and lets the same English word be translated differently in different places, which matters more than it sounds — \"Filter\" as a noun and as a verb are different words in most languages. Defining a small ",[194,414,415],{},"tr()"," method per class is the standard convention. The last method shows the important discipline: the placeholder is ",[409,418,419],{},"inside"," the translated string, so the translator can move it. Building the same sentence as ",[194,422,423],{},"self.tr(\"Processed\") + str(count) + self.tr(\"parcels\")"," produces three fragments that cannot be reassembled correctly in a language with different word order.",[14,426,427,428,431,432,436],{},"Strings in ",[194,429,430],{},".ui"," files are marked automatically by Qt Designer as long as the \"translatable\" checkbox stays enabled, which it is by default — see ",[21,433,435],{"href":434},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fload-ui-file-at-runtime-pyqgis\u002F","Load a .ui File at Runtime in PyQGIS",".",[175,438,440],{"id":439},"extract-the-strings","Extract the strings",[14,442,443],{},"Create a small project file listing what to scan:",[214,445,449],{"className":446,"code":448,"language":68,"meta":219},[447],"language-text","SOURCES = parcel_tools.py \\\n          analysis_dialog.py \\\n          options_page.py\n\nFORMS = ui\u002Fanalysis_dialog.ui\n\nTRANSLATIONS = i18n\u002Fparcel_tools_de.ts \\\n               i18n\u002Fparcel_tools_fr.ts \\\n               i18n\u002Fparcel_tools_es.ts\n",[194,450,448],{"__ignoreMap":219},[14,452,453],{},"Then run the extraction:",[214,455,459],{"className":456,"code":457,"language":458,"meta":219,"style":219},"language-bash shiki shiki-themes github-dark","pylupdate5 parcel_tools.pro\n","bash",[194,460,461],{"__ignoreMap":219},[223,462,463,465],{"class":167,"line":225},[223,464,196],{"class":260},[223,466,467],{"class":288}," parcel_tools.pro\n",[14,469,470,472,473,475,476,478,479,481,482,485,486,489,490,489,493,496,497,499],{},[186,471,402],{}," ",[194,474,196],{}," parses the listed Python and ",[194,477,430],{}," files, finds the marked strings, and writes or updates one ",[194,480,207],{}," file per entry in ",[194,483,484],{},"TRANSLATIONS",". Updating is the useful part: existing translations are preserved, new strings appear as untranslated, and strings that disappeared from the source are kept but marked obsolete rather than deleted, so a rename does not throw away work. The language is taken from the file name suffix, which must be a valid locale code — ",[194,487,488],{},"de",", ",[194,491,492],{},"fr",[194,494,495],{},"pt_BR",". Adding a language is one more line in ",[194,498,484],{}," and a re-run.",[175,501,503],{"id":502},"translate-and-compile","Translate and compile",[14,505,506,507,509],{},"Open a ",[194,508,207],{}," file in Qt Linguist, work through the untranslated entries, and mark each one done. Then compile:",[214,511,513],{"className":456,"code":512,"language":458,"meta":219,"style":219},"lrelease i18n\u002Fparcel_tools_de.ts\n",[194,514,515],{"__ignoreMap":219},[223,516,517,519],{"class":167,"line":225},[223,518,200],{"class":260},[223,520,521],{"class":288}," i18n\u002Fparcel_tools_de.ts\n",[14,523,524,472,526,528,529,532,533,535,536,538],{},[186,525,402],{},[194,527,200],{}," produces a ",[194,530,531],{},".qm"," file — a compact binary form optimised for lookup, and the only file needed at run time. Entries left unfinished in Linguist are excluded from the compiled file, so a half-finished translation falls back to English string by string rather than breaking. Run ",[194,534,200],{}," on every language before packaging, and ship the ",[194,537,531],{}," files inside the plugin zip; forgetting this step is the single most common reason a plugin with complete translations still appears in English for everybody.",[14,540,541],{},[29,542,545,548,551,554,561,564,570,576,580,585,589,592,595,598,601,604,608,611,614,618,621,626,630,634,638,642,646],{"viewBox":543,"role":32,"ariaLabel":544,"xmlns":34},"0 0 760 244","Lifecycle of a single string showing it marked in source, extracted as untranslated, translated by a person, compiled into the binary file and finally displayed in the user's language",[36,546,547],{},"One string, five states",[40,549,550],{},"A string is written in English and marked with tr. Extraction records it as untranslated in the translation file. A translator supplies the German text and marks the entry finished. The release tool compiles the finished entry into the binary file. At run time the plugin displays the German text, or falls back to the English source if the entry was never finished.",[44,552],{"x":46,"y":46,"width":47,"height":553,"fill":49},"244",[51,555,556],{},[54,557,559],{"id":558,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"i18nStateArrow",[63,560],{"d":65,"fill":66},[68,562,563],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"An unfinished entry is not a broken entry",[44,565],{"x":566,"y":567,"width":568,"height":569,"rx":58,"fill":83,"stroke":84,"style":125},"14","60","136","70",[68,571,575],{"x":572,"y":573,"style":574,"fill":84,"textAnchor":74},"82","88","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","in source",[68,577,579],{"x":572,"y":105,"style":578,"fill":66,"textAnchor":74},"text-anchor:middle;font-size:11px;font-family:sans-serif","Analyse parcels",[44,581],{"x":582,"y":567,"width":568,"height":569,"rx":58,"fill":583,"stroke":584,"style":125},"166","#fdf2e2","#b45309",[68,586,588],{"x":587,"y":573,"style":574,"fill":584,"textAnchor":74},"234","extracted",[68,590,591],{"x":587,"y":105,"style":578,"fill":66,"textAnchor":74},"untranslated",[44,593],{"x":594,"y":567,"width":568,"height":569,"rx":58,"fill":135,"stroke":136,"style":125},"318",[68,596,597],{"x":171,"y":573,"style":574,"fill":136,"textAnchor":74},"translated",[68,599,600],{"x":171,"y":105,"style":578,"fill":66,"textAnchor":74},"marked finished",[44,602],{"x":603,"y":567,"width":568,"height":569,"rx":58,"fill":99,"stroke":100,"style":125},"470",[68,605,607],{"x":606,"y":573,"style":574,"fill":73,"textAnchor":74},"538","compiled",[68,609,610],{"x":606,"y":105,"style":578,"fill":66,"textAnchor":74},"inside the .qm",[44,612],{"x":613,"y":567,"width":110,"height":569,"rx":58,"fill":151,"stroke":152,"style":85},"622",[68,615,617],{"x":616,"y":573,"style":574,"fill":152,"textAnchor":74},"685","displayed",[68,619,620],{"x":616,"y":105,"style":578,"fill":66,"textAnchor":74},"in the menu",[167,622],{"x1":172,"y1":623,"x2":624,"y2":623,"stroke":66,"style":625},"95","160","stroke-width:2;marker-end:url(#i18nStateArrow)",[167,627],{"x1":628,"y1":623,"x2":629,"y2":623,"stroke":66,"style":625},"302","312",[167,631],{"x1":632,"y1":623,"x2":633,"y2":623,"stroke":66,"style":625},"454","464",[167,635],{"x1":636,"y1":623,"x2":637,"y2":623,"stroke":66,"style":625},"606","616",[44,639],{"x":582,"y":582,"width":640,"height":641,"rx":58,"fill":99,"stroke":100,"style":101},"440","56",[68,643,645],{"x":171,"y":644,"style":578,"fill":66,"textAnchor":74},"190","Never finished? The English source is displayed instead —",[68,647,649],{"x":171,"y":648,"style":578,"fill":66,"textAnchor":74},"210","a partial translation is perfectly shippable",[175,651,653],{"id":652},"load-the-translation-at-startup","Load the translation at startup",[214,655,657],{"className":216,"code":656,"language":218,"meta":219,"style":219},"import os\nfrom qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication\n\n\nclass ParcelTools:\n    def __init__(self, iface):\n        self.iface = iface\n        self.plugin_dir = os.path.dirname(__file__)\n\n        locale = QSettings().value(\"locale\u002FuserLocale\", \"en\")[0:2]\n        qm_path = os.path.join(self.plugin_dir, \"i18n\", f\"parcel_tools_{locale}.qm\")\n\n        if os.path.exists(qm_path):\n            self.translator = QTranslator()\n            self.translator.load(qm_path)\n            QCoreApplication.installTranslator(self.translator)\n",[194,658,659,666,677,681,685,693,703,715,733,737,769,809,813,821,835,843],{"__ignoreMap":219},[223,660,661,663],{"class":167,"line":225},[223,662,236],{"class":228},[223,664,665],{"class":232}," os\n",[223,667,668,670,672,674],{"class":167,"line":242},[223,669,229],{"class":228},[223,671,233],{"class":232},[223,673,236],{"class":228},[223,675,676],{"class":232}," QSettings, QTranslator, QCoreApplication\n",[223,678,679],{"class":167,"line":249},[223,680,246],{"emptyLinePlaceholder":245},[223,682,683],{"class":167,"line":254},[223,684,246],{"emptyLinePlaceholder":245},[223,686,687,689,691],{"class":167,"line":267},[223,688,257],{"class":228},[223,690,261],{"class":260},[223,692,264],{"class":232},[223,694,695,697,700],{"class":167,"line":279},[223,696,270],{"class":228},[223,698,699],{"class":314}," __init__",[223,701,702],{"class":232},"(self, iface):\n",[223,704,705,707,710,712],{"class":167,"line":295},[223,706,315],{"class":314},[223,708,709],{"class":232},".iface ",[223,711,321],{"class":228},[223,713,714],{"class":232}," iface\n",[223,716,717,719,722,724,727,730],{"class":167,"line":300},[223,718,315],{"class":314},[223,720,721],{"class":232},".plugin_dir ",[223,723,321],{"class":228},[223,725,726],{"class":232}," os.path.dirname(",[223,728,729],{"class":314},"__file__",[223,731,732],{"class":232},")\n",[223,734,735],{"class":167,"line":311},[223,736,246],{"emptyLinePlaceholder":245},[223,738,739,742,744,747,750,752,755,758,760,763,766],{"class":167,"line":344},[223,740,741],{"class":232},"        locale ",[223,743,321],{"class":228},[223,745,746],{"class":232}," QSettings().value(",[223,748,749],{"class":288},"\"locale\u002FuserLocale\"",[223,751,489],{"class":232},[223,753,754],{"class":288},"\"en\"",[223,756,757],{"class":232},")[",[223,759,46],{"class":314},[223,761,762],{"class":232},":",[223,764,765],{"class":314},"2",[223,767,768],{"class":232},"]\n",[223,770,771,774,776,779,781,784,787,789,792,795,798,801,804,807],{"class":167,"line":362},[223,772,773],{"class":232},"        qm_path ",[223,775,321],{"class":228},[223,777,778],{"class":232}," os.path.join(",[223,780,327],{"class":314},[223,782,783],{"class":232},".plugin_dir, ",[223,785,786],{"class":288},"\"i18n\"",[223,788,489],{"class":232},[223,790,791],{"class":228},"f",[223,793,794],{"class":288},"\"parcel_tools_",[223,796,797],{"class":314},"{",[223,799,800],{"class":232},"locale",[223,802,803],{"class":314},"}",[223,805,806],{"class":288},".qm\"",[223,808,732],{"class":232},[223,810,811],{"class":167,"line":367},[223,812,246],{"emptyLinePlaceholder":245},[223,814,815,818],{"class":167,"line":378},[223,816,817],{"class":228},"        if",[223,819,820],{"class":232}," os.path.exists(qm_path):\n",[223,822,824,827,830,832],{"class":167,"line":823},14,[223,825,826],{"class":314},"            self",[223,828,829],{"class":232},".translator ",[223,831,321],{"class":228},[223,833,834],{"class":232}," QTranslator()\n",[223,836,838,840],{"class":167,"line":837},15,[223,839,826],{"class":314},[223,841,842],{"class":232},".translator.load(qm_path)\n",[223,844,846,849,851],{"class":167,"line":845},16,[223,847,848],{"class":232},"            QCoreApplication.installTranslator(",[223,850,327],{"class":314},[223,852,853],{"class":232},".translator)\n",[14,855,856,858,859,862,863,866,867,870,871,873,874,877,878,881,882,885],{},[186,857,402],{}," This belongs in ",[194,860,861],{},"__init__",", before anything user-visible is created — a translator installed after the menu is built leaves the menu in English until the next restart. Taking the first two characters of the locale means ",[194,864,865],{},"de_AT"," finds ",[194,868,869],{},"parcel_tools_de.qm",", which is usually what you want; keep the full code only where you genuinely have regional variants, such as ",[194,872,495],{}," against ",[194,875,876],{},"pt",". The ",[194,879,880],{},"exists()"," check keeps a missing file from being an error: no translation simply means the English source is used. Keeping the translator as an attribute is essential, exactly as with the options factory — a local ",[194,883,884],{},"QTranslator"," is garbage-collected and the translation silently stops working.",[14,887,888],{},[29,889,892,895,898,901,908,911,917,922,925,929,931,934,937,940,943,946,948,951,954,957,961],{"viewBox":890,"role":32,"ariaLabel":891,"xmlns":34},"0 0 760 246","Timeline of plugin startup showing that a translator installed before the interface is built translates it, while one installed afterwards leaves the existing menus in English",[36,893,894],{},"Install the translator before anything is built",[40,896,897],{},"During plugin construction the translator is installed, and every string created afterwards is translated as it is made. If the translator is installed after initGui has already built the menu and toolbar, those labels were created in English and stay English until the next restart, even though later strings appear translated.",[44,899],{"x":46,"y":46,"width":47,"height":900,"fill":49},"246",[51,902,903],{},[54,904,906],{"id":905,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"i18nTimeArrow",[63,907],{"d":65,"fill":66},[68,909,910],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Strings are translated when they are created, not when they are shown",[167,912],{"x1":913,"y1":914,"x2":915,"y2":914,"stroke":100,"style":916},"40","96","726","stroke-width:2;marker-end:url(#i18nTimeArrow)",[918,919],"circle",{"cx":920,"cy":914,"r":921,"fill":84},"130","9",[68,923,924],{"x":920,"y":89,"style":574,"fill":84,"textAnchor":74},"the constructor",[68,926,928],{"x":920,"y":110,"style":927,"fill":100,"textAnchor":74},"text-anchor:middle;font-size:10px;font-family:sans-serif","install the translator here",[918,930],{"cx":70,"cy":914,"r":921,"fill":136},[68,932,933],{"x":70,"y":89,"style":574,"fill":136,"textAnchor":74},"initGui",[68,935,936],{"x":70,"y":110,"style":927,"fill":100,"textAnchor":74},"menu and toolbar labels created",[918,938],{"cx":939,"cy":914,"r":921,"fill":152},"630",[68,941,942],{"x":939,"y":89,"style":574,"fill":152,"textAnchor":74},"running",[68,944,945],{"x":939,"y":110,"style":927,"fill":100,"textAnchor":74},"dialogs and messages",[44,947],{"x":567,"y":582,"width":80,"height":641,"rx":58,"fill":151,"stroke":152,"style":125},[68,949,950],{"x":648,"y":644,"style":574,"fill":152,"textAnchor":74},"installed first",[68,952,953],{"x":648,"y":648,"style":578,"fill":66,"textAnchor":74},"everything appears translated",[44,955],{"x":956,"y":582,"width":80,"height":641,"rx":58,"fill":583,"stroke":584,"style":125},"400",[68,958,960],{"x":959,"y":644,"style":574,"fill":584,"textAnchor":74},"550","installed too late",[68,962,963],{"x":959,"y":648,"style":578,"fill":66,"textAnchor":74},"menus stay English until restart",[175,965,967],{"id":966},"keep-translations-current","Keep translations current",[14,969,970],{},"The workflow only breaks in one place: new strings added and never re-extracted. Two habits prevent it.",[14,972,973,472,976,979,980,982,983,985,986,988,989,436],{},[186,974,975],{},"Re-run extraction before every release.",[194,977,978],{},"pylupdate5 parcel_tools.pro"," followed by ",[194,981,200],{}," on each ",[194,984,207],{}," file, ideally as a step in your build script rather than a thing to remember. In continuous integration, running the extraction and failing if the ",[194,987,207],{}," files change is a neat way to catch a forgotten update — the same pattern as the checks in ",[21,990,992],{"href":991},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Frun-qgis-plugin-tests-in-github-actions\u002F","Run QGIS Plugin Tests in GitHub Actions",[14,994,995,1001,1002,1004],{},[186,996,997,998,1000],{},"Keep the ",[194,999,207],{}," files in version control"," and the ",[194,1003,531],{}," files too, unless your packaging step builds them. Translators then work through pull requests, and a translation contributed once is never lost to a rebuild.",[14,1006,1007,1008,1011,1012,1015,1016,1019],{},"Two smaller points repay attention. Add a translator comment in Linguist where a string is ambiguous — \"Filter\" alone is impossible to translate well, and one sentence of context makes it easy. And localise ",[194,1009,1010],{},"metadata.txt",": fields such as ",[194,1013,1014],{},"description[de]"," and ",[194,1017,1018],{},"about[de]"," are shown by the plugin repository in the user's language, which is what gets the plugin found in the first place.",[175,1021,1023],{"id":1022},"write-strings-that-can-be-translated-well","Write strings that can be translated well",[14,1025,1026],{},"Marking a string makes it translatable; writing it carefully makes the translation good. A few habits separate the two.",[14,1028,1029,1032],{},[186,1030,1031],{},"Whole sentences, one string."," Translators need the full sentence to choose grammar, gender and word order. A string ending in a colon that is completed by another string somewhere else is unanswerable in most languages.",[14,1034,1035,1038,1039,1042],{},[186,1036,1037],{},"Placeholders, not concatenation"," — and give them meaning where a sentence has more than one, because the order will change. ",[194,1040,1041],{},"\"{count} of {total} parcels\""," can be reordered by the translator; two positional slots often cannot be told apart.",[14,1044,1045,1048],{},[186,1046,1047],{},"No embedded formatting assumptions."," A string that starts with a capital because it always appears at the start of a sentence will be wrong the day it appears mid-sentence, and a translator has no way to know which case applies.",[14,1050,1051,1054],{},[186,1052,1053],{},"Comments for anything ambiguous."," In Linguist a note such as \"verb: the action of filtering the table\" turns an impossible entry into an easy one. English's habit of using the same word as noun and verb is a constant source of mistranslation.",[14,1056,1057,1060],{},[186,1058,1059],{},"Avoid idiom and humour."," They rarely survive, and a translator who cannot render the joke will either drop it or produce something odd.",[14,1062,1063,1066],{},[186,1064,1065],{},"Leave room."," German runs about a third longer than English and Finnish longer still, so a label that exactly fits its button in English will not fit in translation. Layout managers solve this; fixed widths do not.",[14,1068,1069],{},"None of this costs time at the point of writing. All of it costs a great deal to retrofit once a dozen languages have been translated against the original strings.",[175,1071,1073],{"id":1072},"qgis-version-compatibility","QGIS version compatibility",[1075,1076,1077,1093],"table",{},[1078,1079,1080],"thead",{},[1081,1082,1083,1087,1090],"tr",{},[1084,1085,1086],"th",{},"QGIS version",[1084,1088,1089],{},"Python",[1084,1091,1092],{},"Notes",[1094,1095,1096,1113,1123,1134],"tbody",{},[1081,1097,1098,1102,1105],{},[1099,1100,1101],"td",{},"3.22 LTR",[1099,1103,1104],{},"3.9",[1099,1106,1107,1108,1015,1110,1112],{},"PyQt5 tooling: ",[194,1109,196],{},[194,1111,200],{}," as described.",[1081,1114,1115,1118,1120],{},[1099,1116,1117],{},"3.28 LTR",[1099,1119,1104],{},[1099,1121,1122],{},"Identical.",[1081,1124,1125,1128,1131],{},[1099,1126,1127],{},"3.34 LTR",[1099,1129,1130],{},"3.12",[1099,1132,1133],{},"Baseline for this page.",[1081,1135,1136,1139,1141],{},[1099,1137,1138],{},"3.40 \u002F 3.44",[1099,1140,1130],{},[1099,1142,1143,1144,1015,1147,1150],{},"Identical workflow; builds moving to Qt 6 use ",[194,1145,1146],{},"pylupdate6",[194,1148,1149],{},"lrelease6"," with the same arguments.",[14,1152,1153],{},"Plugins targeting both Qt 5 and Qt 6 builds should keep the extraction step in a script that picks whichever tool is present, since the file formats themselves are compatible.",[175,1155,1157],{"id":1156},"troubleshooting","Troubleshooting",[180,1159,1160,1169,1178,1184,1199,1205],{},[183,1161,1162,1165,1166,1168],{},[186,1163,1164],{},"Nothing is translated."," The translator was not kept as an attribute, was installed after the interface was built, or the ",[194,1167,531],{}," file is missing from the packaged zip.",[183,1170,1171,1174,1175,1177],{},[186,1172,1173],{},"Some strings are translated and others are not."," The untranslated ones were never marked with ",[194,1176,415],{},", or their entries were left unfinished in Linguist.",[183,1179,1180,1183],{},[186,1181,1182],{},"A string is translated in one place and not another."," Two different contexts. Check that both call sites use the same context name.",[183,1185,1186,1191,1192,1195,1196,1198],{},[186,1187,1188,1190],{},[194,1189,196],{}," finds nothing."," The ",[194,1193,1194],{},".pro"," file lists the wrong paths, or the strings use a ",[194,1197,415],{}," the tool cannot see — it does static analysis, so a string built at run time cannot be extracted.",[183,1200,1201,1204],{},[186,1202,1203],{},"A sentence reads oddly in translation."," It was assembled from fragments. Rewrite it as one string with placeholders.",[183,1206,1207,1210,1211,1214],{},[186,1208,1209],{},"The plugin shows English on a German system."," The QGIS interface language, not the operating system locale, is what ",[194,1212,1213],{},"locale\u002FuserLocale"," reports. Check what QGIS itself is set to.",[175,1216,1218],{"id":1217},"conclusion","Conclusion",[14,1220,1221,1222,1224,1225,1227,1228,1230,1231,1233,1234,1236,1237,1239],{},"Mark strings with a per-class ",[194,1223,415],{},", list your sources in a ",[194,1226,1194],{}," file, extract with ",[194,1229,196],{},", translate in Qt Linguist, compile with ",[194,1232,200],{},", and install a ",[194,1235,884],{}," in ",[194,1238,861],{}," while keeping a reference to it. Re-extract before every release, keep the translation files in version control, and use placeholders rather than concatenation so translators can produce sentences that actually read well.",[175,1241,1243],{"id":1242},"frequently-asked-questions","Frequently Asked Questions",[14,1245,1246,1249],{},[186,1247,1248],{},"Do I need a translation for every language QGIS supports?","\nNo. Ship what you have; missing languages fall back to the source text. Even one additional language meaningfully widens who can use the plugin.",[14,1251,1252,1255,1256,1258,1259,1015,1262,1265],{},[186,1253,1254],{},"Can I translate the plugin name?","\nThe name in ",[194,1257,1010],{}," is an identifier and should stay stable, but ",[194,1260,1261],{},"description",[194,1263,1264],{},"about"," accept localized variants with a language suffix.",[14,1267,1268,1271,1272,1275],{},[186,1269,1270],{},"How do I handle plurals?","\nUse the numeric form of ",[194,1273,1274],{},"QCoreApplication.translate()",", which lets each language define its own plural rules — several have more than two forms.",[14,1277,1278,1281],{},[186,1279,1280],{},"What happens to a translation when I reword the English?","\nExtraction marks the old entry obsolete and the new one untranslated, keeping the previous text visible in Linguist so the translator can adapt rather than start over.",[14,1283,1284,1287,1288,1290],{},[186,1285,1286],{},"Can translations be crowdsourced?","\nYes — the ",[194,1289,207],{}," format is supported by the common translation platforms, and a plugin's translations are a low-risk first contribution for a new collaborator.",[175,1292,1294],{"id":1293},"related","Related",[180,1296,1297,1302,1306,1312,1316],{},[183,1298,1299,1301],{},[21,1300,24],{"href":23}," — the guide this recipe belongs to",[183,1303,1304],{},[21,1305,435],{"href":434},[183,1307,1308],{},[21,1309,1311],{"href":1310},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fwrite-metadata-txt-qgis-plugin\u002F","Write metadata.txt for a QGIS Plugin",[183,1313,1314],{},[21,1315,992],{"href":991},[183,1317,1318],{},[21,1319,1321],{"href":1320},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fadd-plugin-options-page-to-qgis-settings\u002F","Add a Plugin Options Page to QGIS Settings",[1323,1324,1325],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 .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);}",{"title":219,"searchDepth":242,"depth":242,"links":1327},[1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339],{"id":177,"depth":242,"text":178},{"id":211,"depth":242,"text":212},{"id":439,"depth":242,"text":440},{"id":502,"depth":242,"text":503},{"id":652,"depth":242,"text":653},{"id":966,"depth":242,"text":967},{"id":1022,"depth":242,"text":1023},{"id":1072,"depth":242,"text":1073},{"id":1156,"depth":242,"text":1157},{"id":1217,"depth":242,"text":1218},{"id":1242,"depth":242,"text":1243},{"id":1293,"depth":242,"text":1294},"The complete translation workflow — marking strings with tr(), extracting them with pylupdate5, translating in Qt Linguist, compiling .qm files, loading a translator at startup, and keeping translations current across releases.","md",{"slug":1343,"type":1344,"breadcrumb":1345,"datePublished":1346,"dateModified":1346},"translate-qgis-plugin-with-qt-linguist","article","Translate a Plugin","2026-08-15","\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Ftranslate-qgis-plugin-with-qt-linguist",{"title":5,"description":1340},"qgis-plugin-development\u002Fplugin-settings-and-localization\u002Ftranslate-qgis-plugin-with-qt-linguist\u002Findex","2BttGhobKlqLmNxED1-_UnWxPbGSGs6Ao1dJZ6wfOoE",1786789584642]