From 397ec0ad35eacce3f1781a0be77ba07f716d90d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:04:36 -0400 Subject: [PATCH 01/10] r.reclass.area: Translate complete messages instead of fragments The verbose and fatal messages were assembled from fragments such as ' larger than or equal to %f hectares...', which translators cannot reorder. Use one complete string for each combination of thresholds. --- scripts/r.reclass.area/r.reclass.area.py | 64 ++++++++++++++++-------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/scripts/r.reclass.area/r.reclass.area.py b/scripts/r.reclass.area/r.reclass.area.py index 2ffe46bd340..f8acbcb6611 100755 --- a/scripts/r.reclass.area/r.reclass.area.py +++ b/scripts/r.reclass.area/r.reclass.area.py @@ -171,11 +171,19 @@ def get_clumpfile( if minsize: minsize = ceil((minsize * 10000.0) / prod(resolution)) - msg = _("Generating a clumped raster file") - if clump_flags: - msg += _(" including diagonal neighbors") - if minsize: - msg += _(" with minimum %s pixels") % minsize + if clump_flags and minsize: + msg = _( + "Generating a clumped raster file including diagonal neighbors" + " with minimum {} pixels" + ).format(minsize) + elif clump_flags: + msg = _("Generating a clumped raster file including diagonal neighbors") + elif minsize: + msg = _("Generating a clumped raster file with minimum {} pixels").format( + minsize + ) + else: + msg = _("Generating a clumped raster file") gs.verbose(msg) tools.r_clump( flags=clump_flags, @@ -198,23 +206,40 @@ def reclass( tools = Tools(capture_output=True) stats_input = clump_map expected_fields_number = 4 - verbose_message = _("Generating a reclass map with area size") - range_filter_message = "" - # Define lower threshold - if lower: - range_filter_message += _(" larger than or equal to %f hectares...") % lower + # Each combination of thresholds gets complete messages, so that + # translators can reorder the words and numbers freely. + if lower and upper: + verbose_message = _( + "Generating a reclass map with area size larger than or equal to" + " {lower} hectares and smaller than or equal to {upper} hectares..." + ).format(lower=lower, upper=upper) + fatal_message = _( + "No areas of size larger than or equal to {lower} hectares" + " and smaller than or equal to {upper} hectares" + ).format(lower=lower, upper=upper) + elif lower: + verbose_message = _( + "Generating a reclass map with area size" + " larger than or equal to {lower} hectares..." + ).format(lower=lower) + fatal_message = _( + "No areas of size larger than or equal to {lower} hectares" + ).format(lower=lower) else: + verbose_message = _( + "Generating a reclass map with area size" + " smaller than or equal to {upper} hectares..." + ).format(upper=upper) + fatal_message = _( + "No areas of size smaller than or equal to {upper} hectares" + ).format(upper=upper) + gs.verbose(verbose_message) + + if not lower: lower = -inf - - # Define upper threshold - if upper: - if range_filter_message: - range_filter_message += " and" - range_filter_message += _(" smaller than or equal to %f hectares...") % upper - else: + if not upper: upper = inf - gs.verbose(verbose_message + range_filter_message) recfile = f"{TEMP_PREFIX}_recl" sflags = "aln" @@ -251,8 +276,7 @@ def reclass( quiet=True, ) else: - fatal_message = _("No areas of size ") - gs.fatal(fatal_message + range_filter_message) + gs.fatal(fatal_message) tools.r_mapcalc(expression=f"{output_map} = {recfile}") From 4067a2c96f405fe77039718f72f2b5207efa5cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:04:37 -0400 Subject: [PATCH 02/10] r.in.wms: Translate capabilities error as one message The reason was appended to a translated string ending in 'Reason: '. --- scripts/r.in.wms/wms_base.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scripts/r.in.wms/wms_base.py b/scripts/r.in.wms/wms_base.py index 217556bfb1a..151bcbdbbab 100644 --- a/scripts/r.in.wms/wms_base.py +++ b/scripts/r.in.wms/wms_base.py @@ -270,17 +270,15 @@ def _fetchCapabilities(self, options): % options["url"] ) else: - msg = _("Unable to fetch capabilities from <{0}>. Reason: ").format( - options["url"] + gs.fatal( + _( + "Unable to fetch capabilities from <{url}>. Reason: {reason}" + ).format( + url=options["url"], + reason=e.reason if hasattr(e, "reason") else e, + ) ) - if hasattr(e, "reason"): - msg += "{0}".format(e.reason) - else: - msg += "{0}".format(e) - - gs.fatal(msg) - gs.debug("Fetching capabilities OK") return gs.decode(cap.read()) From 5d5fbba8cd50295595ac666ec564536d268cb3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:04:38 -0400 Subject: [PATCH 03/10] wxGUI: Use complete translatable strings instead of fragments Cover the render status bar, scatterplot title and axis labels, RLi sample area title (previously a runtime string passed to gettext, so never translated), database table tab labels, GCP overwrite question, and two incomplete or trailing-space messages. --- gui/wxpython/core/render.py | 29 +++++++++++---------- gui/wxpython/dbmgr/base.py | 8 ++---- gui/wxpython/gcp/manager.py | 15 ++++++----- gui/wxpython/gui_core/preferences.py | 2 +- gui/wxpython/rlisetup/wizard.py | 6 ++++- gui/wxpython/tplot/frame.py | 4 ++- gui/wxpython/wxplot/scatter.py | 38 ++++++++++++++++------------ 7 files changed, 57 insertions(+), 45 deletions(-) diff --git a/gui/wxpython/core/render.py b/gui/wxpython/core/render.py index 491ff85e801..1997dcf7ae9 100644 --- a/gui/wxpython/core/render.py +++ b/gui/wxpython/core/render.py @@ -765,20 +765,21 @@ def ReportProgress(self, env, layer=None): self.progressInfo["downloading"].remove(layer) # for updating statusbar text - stText = "" - first = True - for layer in self.progressInfo["downloading"]: - if first: - stText += _("Downloading data ") - first = False - else: - stText += ", " - stText += "<%s>" % layer.GetName() - if stText: - stText += "..." - - if self.progressInfo["range"] != len(self.progressInfo["rendered"]): - stText = _("Rendering & ") + stText if stText else _("Rendering...") + downloading = ", ".join( + "<%s>" % downloading_layer.GetName() + for downloading_layer in self.progressInfo["downloading"] + ) + is_rendering = self.progressInfo["range"] != len(self.progressInfo["rendered"]) + if is_rendering and downloading: + stText = _("Rendering & downloading data {layers}...").format( + layers=downloading + ) + elif is_rendering: + stText = _("Rendering...") + elif downloading: + stText = _("Downloading data {layers}...").format(layers=downloading) + else: + stText = "" self.updateProgress.emit( range=self.progressInfo["range"], diff --git a/gui/wxpython/dbmgr/base.py b/gui/wxpython/dbmgr/base.py index 5acddb8a92e..c047a7e7535 100644 --- a/gui/wxpython/dbmgr/base.py +++ b/gui/wxpython/dbmgr/base.py @@ -1184,9 +1184,7 @@ def AddLayer(self, layer, pos=-1): self.layerPage[layer] = {"browsePage": panel.GetId()} - label = _("Table") - if not self.dbMgrData["editable"]: - label += _(" (read-only)") + label = _("Table") if self.dbMgrData["editable"] else _("Table (read-only)") if pos == -1: pos = self.GetPageCount() @@ -2329,9 +2327,7 @@ def AddLayer(self, layer, pos=-1): self.layerPage[layer] = {} panel = wx.Panel(parent=self, id=wx.ID_ANY) self.layerPage[layer]["tablePage"] = panel.GetId() - label = _("Table") - if not self.dbMgrData["editable"]: - label += _(" (read-only)") + label = _("Table") if self.dbMgrData["editable"] else _("Table (read-only)") if pos == -1: pos = self.GetPageCount() diff --git a/gui/wxpython/gcp/manager.py b/gui/wxpython/gcp/manager.py index 1fe1d0601df..2aa772267f1 100644 --- a/gui/wxpython/gcp/manager.py +++ b/gui/wxpython/gcp/manager.py @@ -1724,14 +1724,17 @@ def _getOverWriteDialog(self, maptype, overwrite): map_name = "<{}>".format(found["name"]) if found["name"] and not overwrite: + if maptype == "raster": + message = _( + "The raster map {map_name} exists. Do you want to overwrite?" + ).format(map_name=map_name) + else: + message = _( + "The vector map {map_name} exists. Do you want to overwrite?" + ).format(map_name=map_name) return wx.MessageDialog( self.GetParent(), - message=_( - "The {map_type} map {map_name} exists. Do you want to overwrite?" - ).format( - map_type=maptype, - map_name=map_name, - ), + message=message, caption=_("Overwrite?"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION, ) diff --git a/gui/wxpython/gui_core/preferences.py b/gui/wxpython/gui_core/preferences.py index b9b02be99ac..a778e996ce4 100644 --- a/gui/wxpython/gui_core/preferences.py +++ b/gui/wxpython/gui_core/preferences.py @@ -1559,7 +1559,7 @@ def _createLayersPage(self, notebook): randomColors = wx.CheckBox( parent=panel, id=wx.ID_ANY, - label=_("Random colors according to category number "), + label=_("Random colors according to category number"), ) randomColors.SetValue( self.settings.Get(group="vectorLayer", key="randomColors", subkey="enabled") diff --git a/gui/wxpython/rlisetup/wizard.py b/gui/wxpython/rlisetup/wizard.py index ce09d8c01fc..59bc21362de 100644 --- a/gui/wxpython/rlisetup/wizard.py +++ b/gui/wxpython/rlisetup/wizard.py @@ -1912,7 +1912,11 @@ def OnEnterPage(self, event: WizardEvent) -> None: GError(parent=self, message=_("The polygon seems to have 0 areas")) self.parent.wizard.ShowPage(self.parent.samplingareapage) return - self.title.SetLabel(_("Select sample area 1 of " + str(self.areanum))) + self.title.SetLabel( + _("Select sample area {areas_count} of {area_num}").format( + areas_count=1, area_num=self.areanum + ) + ) grass.use_temp_region() if self._raster != self.rast: self.map_ = self.mapPanel.GetMap() diff --git a/gui/wxpython/tplot/frame.py b/gui/wxpython/tplot/frame.py index 5788badd52e..9480d74fe9a 100755 --- a/gui/wxpython/tplot/frame.py +++ b/gui/wxpython/tplot/frame.py @@ -1017,7 +1017,9 @@ def OnRedraw(self, event=None): GError( parent=self, showTraceback=False, - message=_("Please change name of output CSV file or "), + message=_( + "Please change name of output CSV file or enable overwriting." + ), ) return dlg.Destroy() diff --git a/gui/wxpython/wxplot/scatter.py b/gui/wxpython/wxplot/scatter.py index 74aefa2cb77..b572381f77e 100644 --- a/gui/wxpython/wxplot/scatter.py +++ b/gui/wxpython/wxplot/scatter.py @@ -133,10 +133,7 @@ def OnSelectRaster(self, event): def SetupScatterplot(self): """Build data list for plotting each raster""" - # - # initialize title string - # - self.ptitle = _("Bivariate Scatterplot of ") + pair_labels = [] # # create a datalist for plotting for each raster pair @@ -148,26 +145,35 @@ def SetupScatterplot(self): self.raster[rpair]["datalist"] = self.CreateDatalist(rpair) # update title - self.ptitle += "%s vs %s, " % ( - rpair[0].split("@")[0], - rpair[1].split("@")[0], + pair_labels.append( + _("{first} vs {second}").format( + first=rpair[0].split("@")[0], + second=rpair[1].split("@")[0], + ) ) - self.ptitle = self.ptitle.strip(", ") + self.ptitle = _("Bivariate Scatterplot of {pairs}").format( + pairs=", ".join(pair_labels) + ) # # set xlabel & ylabel based on raster maps of first pair to be plotted # - self.xlabel = _("Raster <%s> cell values") % rpair[0].split("@")[0] - self.ylabel = _("Raster <%s> cell values") % rpair[1].split("@")[0] - - units = self.raster[self.rasterList[0]][0]["units"] - if units != "": - self.xlabel += _(": %s") % units + self.xlabel = self._cell_values_label( + rpair[0].split("@")[0], self.raster[self.rasterList[0]][0]["units"] + ) + self.ylabel = self._cell_values_label( + rpair[1].split("@")[0], self.raster[self.rasterList[0]][1]["units"] + ) - units = self.raster[self.rasterList[0]][1]["units"] + @staticmethod + def _cell_values_label(raster, units): + """Return the axis label for the cell values of a raster map""" if units != "": - self.ylabel += _(": %s") % units + return _("Raster <{raster}> cell values: {units}").format( + raster=raster, units=units + ) + return _("Raster <{raster}> cell values").format(raster=raster) def CreateDatalist(self, rpair): """Build a list of cell value, frequency pairs for histogram From 1f55ab1048ff22f354b6a41f3de367c52dae1332 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 21:37:30 +0000 Subject: [PATCH 04/10] Translation: Use complete source strings in more Python code Replace messages assembled from translated fragments or passed to gettext as already-built strings with complete format strings, in the temporal framework, wxGUI toolbars, selectors and logs, and several scripts. Also drop trailing spaces from a few messages. Co-Authored-By: Claude Sonnet 5.5 --- gui/wxpython/core/settings.py | 4 +++- gui/wxpython/gcp/toolbars.py | 7 ++----- gui/wxpython/gmodeler/model_items.py | 2 +- gui/wxpython/gui_core/forms.py | 2 +- gui/wxpython/gui_core/gselect.py | 4 ++-- gui/wxpython/iclass/toolbars.py | 7 ++----- gui/wxpython/image2target/ii2t_toolbars.py | 7 ++----- gui/wxpython/lmgr/layertree.py | 2 +- gui/wxpython/mapwin/analysis.py | 4 +++- gui/wxpython/photo2image/ip2i_toolbars.py | 7 ++----- gui/wxpython/psmap/dialogs.py | 2 +- gui/wxpython/psmap/frame.py | 4 +++- python/grass/temporal/core.py | 17 +++++++++++------ python/grass/temporal/temporal_algebra.py | 2 +- scripts/m.proj/m.proj.py | 2 +- scripts/r.in.srtm/r.in.srtm.py | 2 +- scripts/r.in.wms/wms_drv.py | 2 +- scripts/v.rast.stats/v.rast.stats.py | 6 ++++-- scripts/v.what.strds/v.what.strds.py | 5 +++-- 19 files changed, 45 insertions(+), 43 deletions(-) diff --git a/gui/wxpython/core/settings.py b/gui/wxpython/core/settings.py index 4861e278830..5b6454a160a 100644 --- a/gui/wxpython/core/settings.py +++ b/gui/wxpython/core/settings.py @@ -1110,7 +1110,9 @@ def Set(self, group, value, key=None, subkey=None, settings_type="user"): except KeyError as e: raise GException( - "%s '%s:%s:%s'" % (_("Unable to set "), group, key, subkey) + _("Unable to set '{group}:{key}:{subkey}'").format( + group=group, key=key, subkey=subkey + ) ) from e def Append(self, dict, group, key, subkey, value, overwrite=True): diff --git a/gui/wxpython/gcp/toolbars.py b/gui/wxpython/gcp/toolbars.py index f416232bbc4..259113a918e 100644 --- a/gui/wxpython/gcp/toolbars.py +++ b/gui/wxpython/gcp/toolbars.py @@ -108,11 +108,8 @@ def __init__(self, parent, toolSwitcher): self.SetToolShortHelp( self.togglemap.GetId(), - "%s %s %s" - % ( - _("Set map canvas for "), - BaseIcons["zoomBack"].GetLabel(), - _(" / Zoom to map"), + _("Set map canvas for {zoom_back} / Zoom to map").format( + zoom_back=BaseIcons["zoomBack"].GetLabel() ), ) diff --git a/gui/wxpython/gmodeler/model_items.py b/gui/wxpython/gmodeler/model_items.py index 1eb27bba5e7..0c38abdd3ba 100644 --- a/gui/wxpython/gmodeler/model_items.py +++ b/gui/wxpython/gmodeler/model_items.py @@ -880,7 +880,7 @@ def SetLabel(self, label=""): def GetLog(self): """Get log info""" if self.label: - return _("Condition: ") + self.label + return _("Condition: {label}").format(label=self.label) return _("Condition: not defined") def AddRelation(self, rel): diff --git a/gui/wxpython/gui_core/forms.py b/gui/wxpython/gui_core/forms.py index 2cdbcce334c..f04821faf70 100644 --- a/gui/wxpython/gui_core/forms.py +++ b/gui/wxpython/gui_core/forms.py @@ -557,7 +557,7 @@ def __init__( guisizer.Add(self.notebookpanel, proportion=1, flag=wx.EXPAND) # status bar - status_text = _("Enter parameters for '") + self.task.name + "'" + status_text = _("Enter parameters for '{name}'").format(name=self.task.name) try: self.task.get_cmd() self.updateValuesHook() diff --git a/gui/wxpython/gui_core/gselect.py b/gui/wxpython/gui_core/gselect.py index 21930239cc5..24e8430bb26 100644 --- a/gui/wxpython/gui_core/gselect.py +++ b/gui/wxpython/gui_core/gselect.py @@ -554,7 +554,7 @@ def _getElementList(self, element, mapsets=None, elements=None, exclude=False): first_mapset = None for mapset in mapsets: mapset_node = self.AddItem( - _("Mapset") + ": " + mapset, node=True, mapset=mapset + _("Mapset: {name}").format(name=mapset), node=True, mapset=mapset ) node = mapset_node if not first_mapset: @@ -567,7 +567,7 @@ def _getElementList(self, element, mapsets=None, elements=None, exclude=False): if isinstance(filesdict[mapset], dict): for elementType in filesdict[mapset].keys(): node = self.AddItem( - _("Type: ") + elementType, + _("Type: {name}").format(name=elementType), mapset=mapset, node=True, parent=mapset_node, diff --git a/gui/wxpython/iclass/toolbars.py b/gui/wxpython/iclass/toolbars.py index f29e7a00b95..d0fcf8383d3 100644 --- a/gui/wxpython/iclass/toolbars.py +++ b/gui/wxpython/iclass/toolbars.py @@ -64,11 +64,8 @@ def __init__(self, parent, toolSwitcher): self.SetToolShortHelp( self.togglemap.GetId(), - "%s %s %s" - % ( - _("Set map canvas for "), - BaseIcons["zoomBack"].GetLabel(), - _("/ Zoom to map"), + _("Set map canvas for {zoom_back} / Zoom to map").format( + zoom_back=BaseIcons["zoomBack"].GetLabel() ), ) diff --git a/gui/wxpython/image2target/ii2t_toolbars.py b/gui/wxpython/image2target/ii2t_toolbars.py index dae50eee817..f65b81f2e91 100644 --- a/gui/wxpython/image2target/ii2t_toolbars.py +++ b/gui/wxpython/image2target/ii2t_toolbars.py @@ -108,11 +108,8 @@ def __init__(self, parent, toolSwitcher): self.SetToolShortHelp( self.togglemap.GetId(), - "%s %s %s" - % ( - _("Set map canvas for "), - BaseIcons["zoomBack"].GetLabel(), - _(" / Zoom to map"), + _("Set map canvas for {zoom_back} / Zoom to map").format( + zoom_back=BaseIcons["zoomBack"].GetLabel() ), ) diff --git a/gui/wxpython/lmgr/layertree.py b/gui/wxpython/lmgr/layertree.py index 0b9c042c5c1..c69aab4c439 100644 --- a/gui/wxpython/lmgr/layertree.py +++ b/gui/wxpython/lmgr/layertree.py @@ -1556,7 +1556,7 @@ def AddLayer( elif ltype == "group": # group item ctrl = None - grouptext = _("Layer group:") + str(self.groupnode) + grouptext = _("Layer group: {number}").format(number=self.groupnode) self.groupnode += 1 else: btnbmp = LMIcons["layerOptions"].GetBitmap((16, 16)) diff --git a/gui/wxpython/mapwin/analysis.py b/gui/wxpython/mapwin/analysis.py index f56b08f0f65..7b6091037c1 100644 --- a/gui/wxpython/mapwin/analysis.py +++ b/gui/wxpython/mapwin/analysis.py @@ -248,7 +248,9 @@ def Start(self): ) if self._projInfo["proj"] != "xy": mapunits = self._projInfo["units"] - self._giface.WriteCmdLog(_("Measuring distance") + " (" + mapunits + "):") + self._giface.WriteCmdLog( + _("Measuring distance ({units}):").format(units=mapunits) + ) else: self._giface.WriteCmdLog(_("Measuring distance:")) diff --git a/gui/wxpython/photo2image/ip2i_toolbars.py b/gui/wxpython/photo2image/ip2i_toolbars.py index 67192efd377..01c8a95690d 100644 --- a/gui/wxpython/photo2image/ip2i_toolbars.py +++ b/gui/wxpython/photo2image/ip2i_toolbars.py @@ -108,11 +108,8 @@ def __init__(self, parent, toolSwitcher): self.SetToolShortHelp( self.togglemap.GetId(), - "%s %s %s" - % ( - _("Set map canvas for "), - BaseIcons["zoomBack"].GetLabel(), - _(" / Zoom to map"), + _("Set map canvas for {zoom_back} / Zoom to map").format( + zoom_back=BaseIcons["zoomBack"].GetLabel() ), ) diff --git a/gui/wxpython/psmap/dialogs.py b/gui/wxpython/psmap/dialogs.py index 2dc39c6d623..8379f3260e1 100644 --- a/gui/wxpython/psmap/dialogs.py +++ b/gui/wxpython/psmap/dialogs.py @@ -4987,7 +4987,7 @@ def _scalebarPanel(self): self.sbSegmentsCtrl = SpinCtrl(panel, id=wx.ID_ANY, min=1, max=30, initial=4) self.sbSegmentsCtrl.SetValue(self.scalebarDict["segment"]) - sbLabelsText1 = StaticText(panel, id=wx.ID_ANY, label=_("Label every ")) + sbLabelsText1 = StaticText(panel, id=wx.ID_ANY, label=_("Label every")) sbLabelsText2 = StaticText(panel, id=wx.ID_ANY, label=_("segments")) self.sbLabelsCtrl = SpinCtrl(panel, id=wx.ID_ANY, min=1, max=30, initial=1) self.sbLabelsCtrl.SetValue(self.scalebarDict["numbers"]) diff --git a/gui/wxpython/psmap/frame.py b/gui/wxpython/psmap/frame.py index 6345db1aad0..d28ff95cd72 100644 --- a/gui/wxpython/psmap/frame.py +++ b/gui/wxpython/psmap/frame.py @@ -2684,7 +2684,9 @@ def UpdateMapLabel(self): if not labelFiles: return labelFiles = [lFile.split("@")[0] for lFile in labelFiles] - self.itemLabels[mapId].append(_("labels: ") + ", ".join(labelFiles)) + self.itemLabels[mapId].append( + _("labels: {files}").format(files=", ".join(labelFiles)) + ) def UpdateLabel(self, itype, id): self.itemLabels[id] = [] diff --git a/python/grass/temporal/core.py b/python/grass/temporal/core.py index 1dbb8e3b558..4656e4cab4c 100644 --- a/python/grass/temporal/core.py +++ b/python/grass/temporal/core.py @@ -1242,7 +1242,9 @@ def check_table(self, table_name: str, mapset=None): mapset = decode(mapset) if mapset not in self.tgis_mapsets: self.msgr.fatal( - _("Unable to check table. " + self._create_mapset_error_message(mapset)) + _("Unable to check table. {reason}").format( + reason=self._create_mapset_error_message(mapset) + ) ) return self.connections[mapset].check_table(table_name) @@ -1260,9 +1262,8 @@ def execute(self, statement, args=None, mapset=None): mapset = decode(mapset) if mapset not in self.tgis_mapsets: self.msgr.fatal( - _( - "Unable to execute sql statement. " - + self._create_mapset_error_message(mapset) + _("Unable to execute sql statement. {reason}").format( + reason=self._create_mapset_error_message(mapset) ) ) @@ -1275,7 +1276,9 @@ def fetchone(self, mapset=None): mapset = decode(mapset) if mapset not in self.tgis_mapsets: self.msgr.fatal( - _("Unable to fetch one. " + self._create_mapset_error_message(mapset)) + _("Unable to fetch one. {reason}").format( + reason=self._create_mapset_error_message(mapset) + ) ) return self.connections[mapset].fetchone() @@ -1287,7 +1290,9 @@ def fetchall(self, mapset=None): mapset = decode(mapset) if mapset not in self.tgis_mapsets: self.msgr.fatal( - _("Unable to fetch all. " + self._create_mapset_error_message(mapset)) + _("Unable to fetch all. {reason}").format( + reason=self._create_mapset_error_message(mapset) + ) ) return self.connections[mapset].fetchall() diff --git a/python/grass/temporal/temporal_algebra.py b/python/grass/temporal/temporal_algebra.py index 1f30f5d22e6..a0b0cde9b8f 100644 --- a/python/grass/temporal/temporal_algebra.py +++ b/python/grass/temporal/temporal_algebra.py @@ -1296,7 +1296,7 @@ def check_stds( if ("condition_value" not in dir(map_i)) or clear: map_i.condition_value = [] else: - self.msgr.fatal(_("Wrong type of input " + str(input))) + self.msgr.fatal(_("Wrong type of input {input}").format(input=input)) # We generate a unique map id that will be used # in the topology analysis, since the maplist can diff --git a/scripts/m.proj/m.proj.py b/scripts/m.proj/m.proj.py index 861472f256c..b9a33238dcd 100755 --- a/scripts/m.proj/m.proj.py +++ b/scripts/m.proj/m.proj.py @@ -203,7 +203,7 @@ def main(): gcore.fatal(_("Invalid PROJ.4 output specification")) if not out_proj: - gcore.fatal(_("Missing output projection parameters ")) + gcore.fatal(_("Missing output projection parameters")) out_proj = out_proj.strip() gcore.verbose("Output parameters: '%s'" % out_proj) diff --git a/scripts/r.in.srtm/r.in.srtm.py b/scripts/r.in.srtm/r.in.srtm.py index 0eaf3a6589b..a245ded4741 100755 --- a/scripts/r.in.srtm/r.in.srtm.py +++ b/scripts/r.in.srtm/r.in.srtm.py @@ -263,7 +263,7 @@ def main(): # write cmd history: gs.raster_history(tileout) - gs.message(_("Done: generated map ") + tileout) + gs.message(_("Done: generated map {name}").format(name=tileout)) gs.message( _("(Note: Holes in the data can be closed with 'r.fillnulls' using splines)") ) diff --git a/scripts/r.in.wms/wms_drv.py b/scripts/r.in.wms/wms_drv.py index be171f97865..d46ba67d8c1 100644 --- a/scripts/r.in.wms/wms_drv.py +++ b/scripts/r.in.wms/wms_drv.py @@ -280,7 +280,7 @@ def _pct2rgb(self, src_filename, dst_filename): # open source file src_ds = gdal.Open(src_filename) if src_ds is None: - gs.fatal(_("Unable to open %s ") % src_filename) + gs.fatal(_("Unable to open {name}").format(name=src_filename)) src_band = src_ds.GetRasterBand(band_number) diff --git a/scripts/v.rast.stats/v.rast.stats.py b/scripts/v.rast.stats/v.rast.stats.py index a70f1b2aac1..467d299cfa2 100644 --- a/scripts/v.rast.stats/v.rast.stats.py +++ b/scripts/v.rast.stats/v.rast.stats.py @@ -411,8 +411,10 @@ def set_up_columns(vector, layer, percentile, colprefix, basecols, dbfdriver, c) if currcolumn in gs.vector_columns(vector, layer).keys(): if not c: gs.fatal( - (_("Cannot create column <%s> (already present). ") % currcolumn) - + _("Use -c flag to update values in this column.") + _( + "Cannot create column <{column}> (already present)." + " Use -c flag to update values in this column." + ).format(column=currcolumn) ) else: coltype = "INTEGER" if i == "n" else "DOUBLE PRECISION" diff --git a/scripts/v.what.strds/v.what.strds.py b/scripts/v.what.strds/v.what.strds.py index 6c99222893a..4ee50a82802 100644 --- a/scripts/v.what.strds/v.what.strds.py +++ b/scripts/v.what.strds/v.what.strds.py @@ -260,8 +260,9 @@ def main(): except CalledModuleError: dbif.close() gs.fatal( - _("Unable to add column %s to vector map <%s> ") - % (column_string, output) + _("Unable to add column {column} to vector map <{vector}>").format( + column=column_string, vector=output + ) ) try: gs.run_command( From 9705e3bd9ca52c72cc9244b239df8f925334ce53 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 14:36:48 +0000 Subject: [PATCH 05/10] i18n: Use descriptive placeholder names and share the t.vect.what.strds column message --- gui/wxpython/gui_core/forms.py | 2 +- gui/wxpython/gui_core/gselect.py | 4 ++-- scripts/r.in.srtm/r.in.srtm.py | 2 +- scripts/r.in.wms/wms_drv.py | 2 +- temporal/t.vect.what.strds/t.vect.what.strds.py | 5 +++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/gui/wxpython/gui_core/forms.py b/gui/wxpython/gui_core/forms.py index f04821faf70..abcc3dc2abd 100644 --- a/gui/wxpython/gui_core/forms.py +++ b/gui/wxpython/gui_core/forms.py @@ -557,7 +557,7 @@ def __init__( guisizer.Add(self.notebookpanel, proportion=1, flag=wx.EXPAND) # status bar - status_text = _("Enter parameters for '{name}'").format(name=self.task.name) + status_text = _("Enter parameters for '{tool}'").format(tool=self.task.name) try: self.task.get_cmd() self.updateValuesHook() diff --git a/gui/wxpython/gui_core/gselect.py b/gui/wxpython/gui_core/gselect.py index 24e8430bb26..077bac2b178 100644 --- a/gui/wxpython/gui_core/gselect.py +++ b/gui/wxpython/gui_core/gselect.py @@ -554,7 +554,7 @@ def _getElementList(self, element, mapsets=None, elements=None, exclude=False): first_mapset = None for mapset in mapsets: mapset_node = self.AddItem( - _("Mapset: {name}").format(name=mapset), node=True, mapset=mapset + _("Mapset: {mapset}").format(mapset=mapset), node=True, mapset=mapset ) node = mapset_node if not first_mapset: @@ -567,7 +567,7 @@ def _getElementList(self, element, mapsets=None, elements=None, exclude=False): if isinstance(filesdict[mapset], dict): for elementType in filesdict[mapset].keys(): node = self.AddItem( - _("Type: {name}").format(name=elementType), + _("Type: {element_type}").format(element_type=elementType), mapset=mapset, node=True, parent=mapset_node, diff --git a/scripts/r.in.srtm/r.in.srtm.py b/scripts/r.in.srtm/r.in.srtm.py index a245ded4741..b5e8c0f4220 100755 --- a/scripts/r.in.srtm/r.in.srtm.py +++ b/scripts/r.in.srtm/r.in.srtm.py @@ -263,7 +263,7 @@ def main(): # write cmd history: gs.raster_history(tileout) - gs.message(_("Done: generated map {name}").format(name=tileout)) + gs.message(_("Done: generated map {map_name}").format(map_name=tileout)) gs.message( _("(Note: Holes in the data can be closed with 'r.fillnulls' using splines)") ) diff --git a/scripts/r.in.wms/wms_drv.py b/scripts/r.in.wms/wms_drv.py index d46ba67d8c1..51b43c73fbd 100644 --- a/scripts/r.in.wms/wms_drv.py +++ b/scripts/r.in.wms/wms_drv.py @@ -280,7 +280,7 @@ def _pct2rgb(self, src_filename, dst_filename): # open source file src_ds = gdal.Open(src_filename) if src_ds is None: - gs.fatal(_("Unable to open {name}").format(name=src_filename)) + gs.fatal(_("Unable to open {filename}").format(filename=src_filename)) src_band = src_ds.GetRasterBand(band_number) diff --git a/temporal/t.vect.what.strds/t.vect.what.strds.py b/temporal/t.vect.what.strds/t.vect.what.strds.py index 4dc01954f84..38fcf260b72 100755 --- a/temporal/t.vect.what.strds/t.vect.what.strds.py +++ b/temporal/t.vect.what.strds/t.vect.what.strds.py @@ -178,8 +178,9 @@ def main(): except CalledModuleError: dbif.close() gs.fatal( - _("Unable to add column %s to vector map <%s>") - % (col_name, vectmap) + _( + "Unable to add column {column} to vector map <{vector}>" + ).format(column=col_name, vector=vectmap) ) # Call v.what.rast From e24da30c4798edaa5ecc704e163cf8d59cbf2418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:29:35 -0400 Subject: [PATCH 06/10] Add {reason} placeholders for two more methods in SQLDatabaseInterfaceConnection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Edouard Choinière <27212526+echoix@users.noreply.github.com> --- python/grass/temporal/core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/grass/temporal/core.py b/python/grass/temporal/core.py index 4656e4cab4c..d472e602713 100644 --- a/python/grass/temporal/core.py +++ b/python/grass/temporal/core.py @@ -1219,6 +1219,10 @@ def mogrify_sql_statement(self, content, mapset=None): "Unable to mogrify sql statement. " + self._create_mapset_error_message(mapset) ) + self.msgr.fatal( + _("Unable to mogrify sql statement. {reason}").format( + reason=self._create_mapset_error_message(mapset) + ) ) return self.connections[mapset].mogrify_sql_statement(content) @@ -1315,6 +1319,10 @@ def execute_transaction(self, statement, mapset=None): "Unable to execute transaction. " + self._create_mapset_error_message(mapset) ) + self.msgr.fatal( + _("Unable to execute transaction. {reason}").format( + reason=self._create_mapset_error_message(mapset) + ) ) return self.connections[mapset].execute_transaction(statement) From d265a243efb1a307a093ab89e0af1e4b708d59c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:33:27 +0000 Subject: [PATCH 07/10] grass.temporal: Remove leftover calls from applied review suggestions Applying the review suggestions for the mogrify and transaction mapset errors kept the old fatal() calls unclosed before the new ones, which made core.py fail to compile. --- python/grass/temporal/core.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/python/grass/temporal/core.py b/python/grass/temporal/core.py index d472e602713..78fc4524c1e 100644 --- a/python/grass/temporal/core.py +++ b/python/grass/temporal/core.py @@ -1214,11 +1214,6 @@ def mogrify_sql_statement(self, content, mapset=None): mapset = decode(mapset) if mapset not in self.tgis_mapsets: - self.msgr.fatal( - _( - "Unable to mogrify sql statement. " - + self._create_mapset_error_message(mapset) - ) self.msgr.fatal( _("Unable to mogrify sql statement. {reason}").format( reason=self._create_mapset_error_message(mapset) @@ -1314,11 +1309,6 @@ def execute_transaction(self, statement, mapset=None): mapset = decode(mapset) if mapset not in self.tgis_mapsets: - self.msgr.fatal( - _( - "Unable to execute transaction. " - + self._create_mapset_error_message(mapset) - ) self.msgr.fatal( _("Unable to execute transaction. {reason}").format( reason=self._create_mapset_error_message(mapset) From 5e1fdee1cc455bcebc6bda986841fd965f0c7e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:40:27 -0400 Subject: [PATCH 08/10] Make `_create_mapset_error_message` messages translatable --- python/grass/temporal/core.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/python/grass/temporal/core.py b/python/grass/temporal/core.py index 78fc4524c1e..fc365bed0dc 100644 --- a/python/grass/temporal/core.py +++ b/python/grass/temporal/core.py @@ -1318,13 +1318,11 @@ def execute_transaction(self, statement, mapset=None): return self.connections[mapset].execute_transaction(statement) def _create_mapset_error_message(self, mapset) -> str: - return ( - "You have no permission to " - "access mapset <%(mapset)s>, or " - "mapset <%(mapset)s> has no temporal database. " - "Accessible mapsets are: <%(mapsets)s>" - % {"mapset": decode(mapset), "mapsets": ",".join(self.tgis_mapsets.keys())} - ) + return _( + "You have no permission to access mapset <{mapset}>, or mapset" + " <{mapset}> has no temporal database." + " Accessible mapsets are: <{mapsets}>" + ).format(mapset=decode(mapset), mapsets=",".join(self.tgis_mapsets.keys())) ############################################################################### From 53a4b7ce8cccd5efc8902702cc10f8c7380d92db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:42:41 -0400 Subject: [PATCH 09/10] Apply suggestion to avoid printing "None" --- scripts/r.reclass.area/r.reclass.area.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/r.reclass.area/r.reclass.area.py b/scripts/r.reclass.area/r.reclass.area.py index f8acbcb6611..b86377ce318 100755 --- a/scripts/r.reclass.area/r.reclass.area.py +++ b/scripts/r.reclass.area/r.reclass.area.py @@ -226,7 +226,7 @@ def reclass( fatal_message = _( "No areas of size larger than or equal to {lower} hectares" ).format(lower=lower) - else: + elif upper: verbose_message = _( "Generating a reclass map with area size" " smaller than or equal to {upper} hectares..." @@ -234,6 +234,10 @@ def reclass( fatal_message = _( "No areas of size smaller than or equal to {upper} hectares" ).format(upper=upper) + else: + # A zero lower limit and no upper limit filter nothing. + verbose_message = _("Generating a reclass map without area size limits...") + fatal_message = _("No areas found") gs.verbose(verbose_message) if not lower: From f16b0101b68c4b4d3910d0e55a95d45f3b0d773d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sat, 10 Oct 2026 09:45:18 -0400 Subject: [PATCH 10/10] Name the variable in r.reclass.area's changed string --- scripts/r.reclass.area/r.reclass.area.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/r.reclass.area/r.reclass.area.py b/scripts/r.reclass.area/r.reclass.area.py index b86377ce318..5ba65d34572 100755 --- a/scripts/r.reclass.area/r.reclass.area.py +++ b/scripts/r.reclass.area/r.reclass.area.py @@ -174,14 +174,14 @@ def get_clumpfile( if clump_flags and minsize: msg = _( "Generating a clumped raster file including diagonal neighbors" - " with minimum {} pixels" - ).format(minsize) + " with minimum {pixel_count} pixels" + ).format(pixel_count=minsize) elif clump_flags: msg = _("Generating a clumped raster file including diagonal neighbors") elif minsize: - msg = _("Generating a clumped raster file with minimum {} pixels").format( - minsize - ) + msg = _( + "Generating a clumped raster file with minimum {pixel_count} pixels" + ).format(pixel_count=minsize) else: msg = _("Generating a clumped raster file") gs.verbose(msg)