Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a55afc0
refactor: move string translation logic to utils.py
alexandreparente Mar 2, 2026
48b4b38
i18n: wrap hardcoded strings with 'tr' function across modules
alexandreparente Mar 2, 2026
3097371
i18n: Implement retranslateUi for static UI elements and use tr() for…
alexandreparente Mar 2, 2026
cb7e749
i18n: add translation source files for en and pt_BR
alexandreparente Mar 2, 2026
462622d
i18n: unify translation contexts to 'Kart'
alexandreparente Mar 2, 2026
3d4af64
i18n: update translations pt_BR
alexandreparente Mar 2, 2026
5330ab6
i18n: define @default as context
alexandreparente Mar 2, 2026
139336b
i18n: update translations pt_BR
alexandreparente Mar 2, 2026
5244f4a
i18n: add translate option
alexandreparente Mar 2, 2026
b2955b3
i18n: fix TypeError in SettingsDialog.retranslateUi declaration
alexandreparente Mar 2, 2026
b6845a6
i18n: update translations pt_BR
alexandreparente Mar 2, 2026
6a7f8d8
i18n: update translations pt_BR
alexandreparente Mar 2, 2026
b662cfe
i18n: Wrap static strings in tr() function
alexandreparente Mar 5, 2026
d3cf2c6
i18n: wrap static strings using HTML templates
alexandreparente Mar 5, 2026
3a27f8d
i18n: relocate retranslateUi to end of block
alexandreparente Mar 5, 2026
b2be42e
i18n: update translations pt_BR
alexandreparente Mar 5, 2026
3426ff0
i18n: update translations en
alexandreparente Mar 5, 2026
9f69d47
fix: escape CSS braces in HTML template
alexandreparente Mar 5, 2026
85933b7
Merge main into translation-support
craigds Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,51 @@
import urllib.parse
import xmlrpc.client
import zipfile
import subprocess
import glob
import re
from configparser import ConfigParser
from io import StringIO


def translate():
"""Update translation sources (.ts), force 'Kart' context, and compile (.qm)"""
print("Updating translation files...")

# Define paths
src_dir = os.path.join(os.path.dirname(__file__), "kart")
i18n_dir = os.path.join(src_dir, "i18n")
ts_files = glob.glob(os.path.join(i18n_dir, "*.ts"))

if not ts_files:
print(f"Error: No .ts files found in {i18n_dir}")
return

# Directories to scan for translatable strings
search_dirs = ["", "gui", "processing", "core"]
source_files = []
for d in search_dirs:
source_files.extend(glob.glob(os.path.join(src_dir, d, "*.py")))

try:
# Update .ts files from source code
if source_files:
subprocess.run(["pylupdate5", "-noobsolete"] + source_files + ["-ts"] + ts_files, check=True)

# Compile .ts files into .qm files
subprocess.run(["lrelease"] + ts_files, check=True)
print(f"Success: {len(ts_files)} translation files updated.")

except subprocess.CalledProcessError as e:
print(f"Error during translation process (Qt Tools): {e}")
except Exception as e:
print(f"Unexpected error: {e}")


def package(version=None):
# Always update translations before packaging
translate()

if not version or version.startswith("dev-"):
# CI uses dev-{SHA}
archive = "kart.zip"
Expand All @@ -22,7 +62,8 @@ def package(version=None):
print(f"Creating {archive} ...")

with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zipFile:
excludes = {"test", "tests", "*.pyc", ".git", "metadata.txt"}
# Exclude development files: .ts are source, .qm are the compiled ones we keep
excludes = {"test", "tests", "*.pyc", ".git", "metadata.txt", "*.ts"}
src_dir = os.path.join(os.path.dirname(__file__), "kart")
exclude = lambda p: any([fnmatch.fnmatch(p, e) for e in excludes])

Expand Down Expand Up @@ -53,6 +94,8 @@ def filter_excludes(files):
zipFile.write(os.path.join(root, f), os.path.join(relpath, f))
filter_excludes(dirs)

print(f"Build complete: {archive}")


def install():
src = os.path.join(os.path.dirname(__file__), "kart")
Expand Down Expand Up @@ -108,6 +151,8 @@ def usage():
"Usage:\n"
f" {sys.argv[0]} package [VERSION] Build a QGIS plugin zip file\n"
f" {sys.argv[0]} install Install in your local QGIS (for development)\n"
f" {sys.argv[0]} translate Update and compile translation files (.ts -> .qm)\n"
f" {sys.argv[0]} publish [ARCHIVE] Upload to QGIS Python Plugins Repository\n"
),
file=sys.stderr,
)
Expand All @@ -118,7 +163,9 @@ def usage():
install()
elif len(sys.argv) in [2, 3] and sys.argv[1] == "package":
package(*sys.argv[2:])
elif len(sys.argv) == 2 and sys.argv[1] == "translate":
translate()
elif len(sys.argv) == 3 and sys.argv[1] == "publish":
publish(sys.argv[2])
else:
usage()
usage()
20 changes: 15 additions & 5 deletions kart/gui/clonedialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from qgis.gui import QgsMessageBar

from qgis.PyQt import uic
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtCore import Qt, QCoreApplication
from qgis.PyQt.QtWidgets import QDialog, QSizePolicy, QFileDialog

from kart.gui.extentselectionpanel import ExtentSelectionPanel
Expand All @@ -14,6 +14,8 @@
InvalidLocationException,
)

from kart.utils import tr

WIDGET, BASE = uic.loadUiType(os.path.join(os.path.dirname(__file__), "clonedialog.ui"))


Expand All @@ -23,6 +25,8 @@ def __init__(self, parent=None):
super(QDialog, self).__init__(parent)
self.setupUi(self)

self.retranslateUi()

self.bar = QgsMessageBar()
self.bar.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.layout().addWidget(self.bar)
Expand All @@ -48,7 +52,7 @@ def setSrc(self, src):

def browse(self, textbox):
folder = QFileDialog.getExistingDirectory(
iface.mainWindow(), "Select Folder", ""
iface.mainWindow(), tr("Select Folder"), ""
)
if folder:
textbox.setText(folder)
Expand All @@ -63,7 +67,7 @@ def okClicked(self):
self.location = self.locationPanel.location()
except InvalidLocationException:
self.bar.pushMessage(
"Invalid location definition", Qgis.MessageLevel.Warning, duration=5
tr("Invalid location definition"), Qgis.MessageLevel.Warning, duration=5
)
return
self.src = self.txtSrc.text()
Expand All @@ -72,7 +76,7 @@ def okClicked(self):
self.extent = self.extentPanel.getExtent()
if self.extent is None:
self.bar.pushMessage(
"Invalid extent value", Qgis.MessageLevel.Warning, duration=5
tr("Invalid extent value"), Qgis.MessageLevel.Warning, duration=5
)
return
else:
Expand All @@ -85,5 +89,11 @@ def okClicked(self):
self.accept()
else:
self.bar.pushMessage(
"Text fields must not be empty", Qgis.MessageLevel.Warning, duration=5
tr("Text fields must not be empty"),
Qgis.MessageLevel.Warning,
duration=5,
)

def retranslateUi(self, *args):
"""Update translations for UI elements from the .ui file"""
self.setWindowTitle(tr("Clone"))
35 changes: 24 additions & 11 deletions kart/gui/conflictsdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from qgis.gui import QgsMessageBar

from qgis.PyQt import uic
from qgis.PyQt.QtCore import QSize, Qt
from qgis.PyQt.QtCore import QSize, Qt, QCoreApplication
from qgis.PyQt.QtGui import QFont
from qgis.PyQt.QtWidgets import (
QDialog,
Expand All @@ -18,6 +18,7 @@
)

from kart.gui import icons
from kart.utils import tr


WIDGET, BASE = uic.loadUiType(
Expand All @@ -32,6 +33,8 @@ def __init__(self, conflicts):
self.conflicts = conflicts
self.setupUi(self)

self.retranslateUi()

self.bar = QgsMessageBar()
self.bar.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.layout().addWidget(self.bar)
Expand Down Expand Up @@ -118,8 +121,10 @@ def updateFromCurrentSelectedItem(self):
def solveAllOurs(self):
ret = QMessageBox.warning(
self,
"Solve conflicts",
"Are you sure you want to solve all conflicts using the 'ours' version?",
tr("Solve conflicts"),
tr(
"Are you sure you want to solve all conflicts using the 'ours' version?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes,
)
Expand All @@ -129,8 +134,10 @@ def solveAllOurs(self):
def solveAllTheirs(self):
ret = QMessageBox.warning(
self,
"Solve conflicts",
"Are you sure you want to solve all conflicts using the 'theirs' version?",
tr("Solve conflicts"),
tr(
"Are you sure you want to solve all conflicts using the 'theirs' version?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes,
)
Expand All @@ -153,7 +160,7 @@ def solveFeature(self):
else:
self.bar.pushMessage(
"",
"There are still conflicts in the current feature",
tr("There are still conflicts in the current feature"),
Qgis.MessageLevel.Warning,
)
return
Expand All @@ -164,7 +171,7 @@ def solveFeature(self):
else:
self.bar.pushMessage(
"",
"There are still conflicts in the current feature",
tr("There are still conflicts in the current feature"),
Qgis.MessageLevel.Warning,
)
return
Expand All @@ -183,8 +190,10 @@ def updateAfterSolvingCurrentItem(self):
if not self.treeConflicts.topLevelItemCount():
QMessageBox.warning(
self,
"Solve conflicts",
"All conflicts are solved. The merge operation will now be closed",
tr("Solve conflicts"),
tr(
"All conflicts are solved. The merge operation will now be closed"
),
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Ok,
)
Expand Down Expand Up @@ -295,8 +304,8 @@ def closeEvent(self, evnt):
if not self.okToMerge:
ret = QMessageBox.warning(
self,
"Conflict resolution",
"Do you really want to exit without resolving conflicts?",
tr("Conflict resolution"),
tr("Do you really want to exit without resolving conflicts?"),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if ret == QMessageBox.StandardButton.No:
Expand Down Expand Up @@ -354,3 +363,7 @@ def __init__(self, path, fid, conflict):
self.conflict = conflict
self.fid = fid
self.path = path

def retranslateUi(self, *args):
"""Update translations for UI elements from the .ui file"""
self.setWindowTitle(tr("Merge Conflicts"))
Comment on lines +366 to +369
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConflictItem inherits QTreeWidgetItem, which doesn't have setWindowTitle(). This retranslateUi() implementation will raise an AttributeError if called and looks like it was meant for the dialog/widget class instead. Consider removing it or updating it to set item text/labels rather than a window title.

Suggested change
def retranslateUi(self, *args):
"""Update translations for UI elements from the .ui file"""
self.setWindowTitle(tr("Merge Conflicts"))

Copilot uses AI. Check for mistakes.
19 changes: 13 additions & 6 deletions kart/gui/dbconnectiondialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from qgis.utils import iface

from qgis.PyQt import uic
from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtWidgets import QDialog, QSizePolicy
from qgis.core import (
Qgis,
Expand All @@ -13,7 +14,7 @@
from qgis.gui import QgsAuthSettingsWidget, QgsMessageBar

from kart.kartapi import Repository
from kart.utils import waitcursor
from kart.utils import waitcursor, tr

WIDGET, BASE = uic.loadUiType(
os.path.join(os.path.dirname(__file__), "dbconnectiondialog.ui")
Expand All @@ -26,12 +27,14 @@ def __init__(self, parent=None):
super(QDialog, self).__init__(parent)
self.setupUi(self)

self.retranslateUi()

self.bar = QgsMessageBar()
self.bar.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.layout().addWidget(self.bar, 10, 0, 1, 3)

self.authWidget = QgsAuthSettingsWidget()
self.authWidget.setWarningText("These credentials are not saved")
self.authWidget.setWarningText(tr("These credentials are not saved"))
self.layout().addWidget(self.authWidget, 7, 1, 1, 2)

self.btnLoadTables.clicked.connect(self.loadTables)
Expand All @@ -51,7 +54,7 @@ def __init__(self, parent=None):

def resetTables(self):
self.comboTable.clear()
self.comboTable.addItem("All tables", None)
self.comboTable.addItem(tr("All tables"), None)

def loadTables(self):
self.resetTables()
Expand All @@ -60,7 +63,7 @@ def loadTables(self):
tables = self._getTables(url)
except Exception:
self.bar.pushMessage(
"Cannot connect to the provided database table(s)",
tr("Cannot connect to the provided database table(s)"),
Qgis.MessageLevel.Warning,
duration=5,
)
Expand All @@ -69,7 +72,7 @@ def loadTables(self):
table = table.replace(".", "/")
self.comboTable.addItem(table, table)
self.bar.pushMessage(
"Tables correctly loaded into tables list",
tr("Tables correctly loaded into tables list"),
Qgis.MessageLevel.Success,
duration=5,
)
Expand All @@ -80,7 +83,7 @@ def okClicked(self):
self._getTables(url)
except Exception:
self.bar.pushMessage(
"Cannot connect to the provided database table(s)",
tr("Cannot connect to the provided database table(s)"),
Qgis.MessageLevel.Warning,
duration=5,
)
Expand Down Expand Up @@ -123,3 +126,7 @@ def _getUrl(self):
else:
credentials = ""
return f"{dbtype}{credentials}{host}:{port}/{database}{schema}"

def retranslateUi(self, *args):
"""Update translations for UI elements from the .ui file"""
self.setWindowTitle(tr("Import from Database"))
Loading
Loading