* [PATCH proxmox-i18n v1] add pgettext() and npgettext() support for context-aware translations
@ 2026-02-05 10:16 Kefu Chai
2026-02-05 13:15 ` Maximiliano Sandoval
2026-02-06 5:47 ` [PATCH proxmox-i18n v2] " Kefu Chai
0 siblings, 2 replies; 7+ messages in thread
From: Kefu Chai @ 2026-02-05 10:16 UTC (permalink / raw)
To: pve-devel
This commit adds message context (msgctxt) support to the JavaScript
i18n tooling, enabling pgettext() and npgettext() functions. This
allows different translations for the same English word based on usage
context (e.g., "Save" in a menu vs "Save" for disk operations).
Changes:
- po2js.pl: Add fnv31a_ctxt() to hash context+msgid combinations
- po2js.pl: Change from load_file_ashash to load_file_asarray to
properly handle multiple msgid entries with different contexts
- po2js.pl: Add pgettext() and npgettext() JavaScript functions
- Makefile: Add --keyword flags for xgettext to extract context
Implementation details:
- Uses composite hash: hash(msgctxt + "\x04" + msgid)
- \x04 is the standard gettext EOT separator
- Backward compatible: messages without context use hash(msgid)
- Same flat catalog structure, no nesting required
JavaScript API:
pgettext(context, msgid)
- Translates message with context
- Example: pgettext("menu", "Save") vs pgettext("disk", "Save")
- Returns msgid if no translation found
npgettext(context, singular, plural, n)
- Translates message with context and plural support
- Example: npgettext("file", "1 file", "{n} files", count)
- Returns appropriate singular/plural form based on n
PO file format:
msgctxt "menu"
msgid "Save"
msgstr "Guardar"
msgctxt "disk operation"
msgid "Save"
msgstr "Almacenar"
# Without context (traditional)
msgid "Save"
msgstr "Salvar"
Extraction:
xgettext now recognizes:
--keyword=pgettext:1c,2 # 1c = context, 2 = msgid
--keyword=npgettext:1c,2,3 # 1c = context, 2 = singular, 3 = plural
Backward compatibility:
- Existing PO files without msgctxt work unchanged
- Existing gettext()/ngettext() calls work unchanged
- Hash values for non-context messages are identical
- Same catalog file format
Tested with following steps:
Modified pve-manager submodule:
File: pve-manager/www/manager6/button/ConsoleButton.js
- Changed gettext('Console') → pgettext('button', 'Console')
- Added pgettext('console menu', 'noVNC/SPICE/xterm.js')
- Added npgettext('console', '{0} console', '{0} consoles', count)
Ran 'make update_pot' to verify xgettext extraction:
Successfully extracted msgctxt entries to pve-manager.pot:
msgctxt "button"
msgid "Console"
msgctxt "console menu"
msgid "noVNC"
msgctxt "console"
msgid "{0} console"
msgid_plural "{0} consoles"
Created test PO file with context translations and verified po2js.pl
generates correct context-aware hashes:
- hash("button" + "\x04" + "Console") = 914449940
- hash("console menu" + "\x04" + "noVNC") = 418124897
- Different contexts produce unique hashes for same msgid
All tests passed:
- xgettext extracts msgctxt from JavaScript
- po2js.pl processes msgctxt from PO files
- Context-aware hashing produces unique digests
- Generated JS includes pgettext/npgettext functions
- Complete workflow: JS → POT → PO → JS catalog
This matches the existing Rust pgettext/npgettext implementation
in proxmox-yew-widget-toolkit, enabling consistent context-aware
translations across the Proxmox ecosystem.
Signed-off-by: Kefu Chai <k.chai@proxmox.com>
---
Makefile | 2 ++
po2js.pl | 69 +++++++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 63 insertions(+), 8 deletions(-)
diff --git a/Makefile b/Makefile
index 86bd723..424918e 100644
--- a/Makefile
+++ b/Makefile
@@ -155,6 +155,8 @@ define potupdate
--package-version="$(shell cd $(2);git rev-parse HEAD)" \
--msgid-bugs-address="<support@proxmox.com>" \
--copyright-holder="Copyright (C) Proxmox Server Solutions GmbH <support@proxmox.com> & the translation contributors." \
+ --keyword=pgettext:1c,2 \
+ --keyword=npgettext:1c,2,3 \
--output="$(1)".pot
endef
diff --git a/po2js.pl b/po2js.pl
index 316c0bd..11939f8 100755
--- a/po2js.pl
+++ b/po2js.pl
@@ -8,9 +8,7 @@ use Getopt::Long;
use JSON;
use Locale::PO;
-# current limits:
-# - we do not support plural. forms
-# - no message content support
+# Note: This script now supports plural forms and message context (msgctxt)
my $options = {};
GetOptions($options, 't=s', 'o=s', 'v=s') or die "unable to parse options\n";
@@ -34,6 +32,15 @@ sub fnv31a {
return $hval & 0x7fffffff;
}
+# Hash function for messages with context
+sub fnv31a_ctxt {
+ my ($msgctxt, $msgid) = @_;
+ return fnv31a($msgid) if !length($msgctxt); # Empty context = no context
+ # Use EOT character (0x04) as separator, standard in gettext
+ my $combined = $msgctxt . "\x04" . $msgid;
+ return fnv31a($combined);
+}
+
my $catalog = {};
my $plurals_catalog = {};
@@ -41,11 +48,20 @@ my $nplurals = 2;
my $plural_forms = "n!=1";
foreach my $filename (@ARGV) {
- my $href = Locale::PO->load_file_ashash($filename)
+ my $aref = Locale::PO->load_file_asarray($filename)
|| die "unable to load '$filename'\n";
my $charset;
- my $hpo = $href->{'""'} || die "no header";
+ # Find header entry (msgid "")
+ my $hpo;
+ foreach my $po (@$aref) {
+ if ($po->msgid eq '""') {
+ $hpo = $po;
+ last;
+ }
+ }
+ die "no header" if !$hpo;
+
my $header = $hpo->dequote($hpo->msgstr);
if ($header =~ m|^Content-Type:\s+text/plain;\s+charset=(\S+)$|im) {
$charset = $1;
@@ -58,8 +74,7 @@ foreach my $filename (@ARGV) {
$plural_forms = $2;
}
- foreach my $k (keys %$href) {
- my $po = $href->{$k};
+ foreach my $po (@$aref) {
next if $po->fuzzy(); # skip fuzzy entries
my $ref = $po->reference();
@@ -76,9 +91,18 @@ foreach my $filename (@ARGV) {
my $qmsgid_plural = decode($charset, $po->msgid_plural);
my $msgid_plural = $po->dequote($qmsgid_plural);
+ # Extract message context if present
+ my $msgctxt = '';
+ if (defined($po->msgctxt)) {
+ my $qmsgctxt = decode($charset, $po->msgctxt);
+ $msgctxt = $po->dequote($qmsgctxt);
+ }
+
next if !length($msgid) && !length($msgid_plural); # skip header
- my $digest = fnv31a($msgid);
+ my $digest = length($msgctxt) > 0
+ ? fnv31a_ctxt($msgctxt, $msgid)
+ : fnv31a($msgid);
die "duplicate digest" if $catalog->{$digest};
@@ -150,6 +174,35 @@ function ngettext(singular, plural, n) {
}
return translation[msg_idx];
}
+
+function fnv31a_ctxt(context, text) {
+ // Use EOT character (0x04) as separator
+ var combined = context + "\\x04" + text;
+ return fnv31a(combined);
+}
+
+function pgettext(context, msgid) {
+ var digest = fnv31a_ctxt(context, msgid);
+ var data = __proxmox_i18n_msgcat__[digest];
+ if (!data) {
+ return msgid; // Return msgid (not context) as fallback
+ }
+ return data[0] || msgid;
+}
+
+function npgettext(context, singular, plural, n) {
+ const msg_idx = Number($plural_forms);
+ const digest = fnv31a_ctxt(context, singular);
+ const translation = __proxmox_i18n_plurals_msgcat__[digest];
+ if (!translation || msg_idx >= translation.length) {
+ if (n === 1) {
+ return singular;
+ } else {
+ return plural;
+ }
+ }
+ return translation[msg_idx];
+}
__EOD
if ($outfile) {
--
2.47.3
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH proxmox-i18n v1] add pgettext() and npgettext() support for context-aware translations 2026-02-05 10:16 [PATCH proxmox-i18n v1] add pgettext() and npgettext() support for context-aware translations Kefu Chai @ 2026-02-05 13:15 ` Maximiliano Sandoval 2026-02-06 5:44 ` Kefu Chai 2026-02-06 5:47 ` [PATCH proxmox-i18n v2] " Kefu Chai 1 sibling, 1 reply; 7+ messages in thread From: Maximiliano Sandoval @ 2026-02-05 13:15 UTC (permalink / raw) To: Kefu Chai; +Cc: pve-devel Kefu Chai <k.chai@proxmox.com> writes: Thanks for this! I will need more time to test and review this, but some small comments bellow. > This commit adds message context (msgctxt) support to the JavaScript > i18n tooling, enabling pgettext() and npgettext() functions. This > allows different translations for the same English word based on usage > context (e.g., "Save" in a menu vs "Save" for disk operations). While this is an interesting possibility, it is more interesting to use it when, for example, one use is a verb and another is for a noun, or when there are two different nouns altogether, as for example "Monitor" which is used indiscriminately for "Ceph Monitors" or "QEMU Monitors" in the web UI. > Changes: > - po2js.pl: Add fnv31a_ctxt() to hash context+msgid combinations > - po2js.pl: Change from load_file_ashash to load_file_asarray to > properly handle multiple msgid entries with different contexts > - po2js.pl: Add pgettext() and npgettext() JavaScript functions > - Makefile: Add --keyword flags for xgettext to extract context > > Implementation details: > - Uses composite hash: hash(msgctxt + "\x04" + msgid) > - \x04 is the standard gettext EOT separator > - Backward compatible: messages without context use hash(msgid) > - Same flat catalog structure, no nesting required > > JavaScript API: > pgettext(context, msgid) > - Translates message with context > - Example: pgettext("menu", "Save") vs pgettext("disk", "Save") > - Returns msgid if no translation found > > npgettext(context, singular, plural, n) > - Translates message with context and plural support > - Example: npgettext("file", "1 file", "{n} files", count) > - Returns appropriate singular/plural form based on n > > PO file format: > msgctxt "menu" > msgid "Save" > msgstr "Guardar" > > msgctxt "disk operation" > msgid "Save" > msgstr "Almacenar" > > # Without context (traditional) > msgid "Save" > msgstr "Salvar" > > Extraction: > xgettext now recognizes: > --keyword=pgettext:1c,2 # 1c = context, 2 = msgid > --keyword=npgettext:1c,2,3 # 1c = context, 2 = singular, 3 = plural > > Backward compatibility: > - Existing PO files without msgctxt work unchanged > - Existing gettext()/ngettext() calls work unchanged > - Hash values for non-context messages are identical > - Same catalog file format > > Tested with following steps: > Modified pve-manager submodule: > File: pve-manager/www/manager6/button/ConsoleButton.js > - Changed gettext('Console') → pgettext('button', 'Console') > - Added pgettext('console menu', 'noVNC/SPICE/xterm.js') > - Added npgettext('console', '{0} console', '{0} consoles', count) > > Ran 'make update_pot' to verify xgettext extraction: > Successfully extracted msgctxt entries to pve-manager.pot: > msgctxt "button" > msgid "Console" > > msgctxt "console menu" > msgid "noVNC" > > msgctxt "console" > msgid "{0} console" > msgid_plural "{0} consoles" > > Created test PO file with context translations and verified po2js.pl > generates correct context-aware hashes: > - hash("button" + "\x04" + "Console") = 914449940 > - hash("console menu" + "\x04" + "noVNC") = 418124897 > - Different contexts produce unique hashes for same msgid > > All tests passed: > - xgettext extracts msgctxt from JavaScript > - po2js.pl processes msgctxt from PO files > - Context-aware hashing produces unique digests > - Generated JS includes pgettext/npgettext functions > - Complete workflow: JS → POT → PO → JS catalog > > This matches the existing Rust pgettext/npgettext implementation > in proxmox-yew-widget-toolkit, enabling consistent context-aware > translations across the Proxmox ecosystem. > > Signed-off-by: Kefu Chai <k.chai@proxmox.com> > --- > Makefile | 2 ++ > po2js.pl | 69 +++++++++++++++++++++++++++++++++++++++++++++++++------- > 2 files changed, 63 insertions(+), 8 deletions(-) > > diff --git a/Makefile b/Makefile > index 86bd723..424918e 100644 > --- a/Makefile > +++ b/Makefile > @@ -155,6 +155,8 @@ define potupdate > --package-version="$(shell cd $(2);git rev-parse HEAD)" \ > --msgid-bugs-address="<support@proxmox.com>" \ > --copyright-holder="Copyright (C) Proxmox Server Solutions GmbH <support@proxmox.com> & the translation contributors." \ > + --keyword=pgettext:1c,2 \ This one is correct, however I think that xgettext by default recognizes all {p,np,n,}gettext as keywords. This would be useful, if you want to define custom fns for translations, e.g. tr(), i18n(), _() or C_() the later two used as an example by glib, e.g. https://docs.gtk.org/glib/i18n.html. > + --keyword=npgettext:1c,2,3 \ >From meson's glib integration in its i18n module (https://github.com/mesonbuild/meson/blob/61c5859b771eb938f20935cc3da54a9075d24c64/mesonbuild/modules/i18n.py#L105C1-L105C30) I think this should be "1c,2" instead, however as I said it would be preferable to not redefine the default keywords. > --output="$(1)".pot > endef > > diff --git a/po2js.pl b/po2js.pl > index 316c0bd..11939f8 100755 > --- a/po2js.pl > +++ b/po2js.pl > @@ -8,9 +8,7 @@ use Getopt::Long; > use JSON; > use Locale::PO; > > -# current limits: > -# - we do not support plural. forms > -# - no message content support > +# Note: This script now supports plural forms and message context (msgctxt) I would just remove this note (or fill it with other limitations, if any is known). > > my $options = {}; > GetOptions($options, 't=s', 'o=s', 'v=s') or die "unable to parse options\n"; > @@ -34,6 +32,15 @@ sub fnv31a { > return $hval & 0x7fffffff; > } > ... > ... -- Maximiliano ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH proxmox-i18n v1] add pgettext() and npgettext() support for context-aware translations 2026-02-05 13:15 ` Maximiliano Sandoval @ 2026-02-06 5:44 ` Kefu Chai 0 siblings, 0 replies; 7+ messages in thread From: Kefu Chai @ 2026-02-06 5:44 UTC (permalink / raw) To: Maximiliano Sandoval; +Cc: pve-devel Hi Maximilano, Thanks for the detailed review! I've addressed your comments: On Thu Feb 5, 2026 at 9:15 PM CST, Maximiliano Sandoval wrote: > Kefu Chai <k.chai@proxmox.com> writes: > > Thanks for this! I will need more time to test and review this, but some > small comments bellow. > >> This commit adds message context (msgctxt) support to the JavaScript >> i18n tooling, enabling pgettext() and npgettext() functions. This >> allows different translations for the same English word based on usage >> context (e.g., "Save" in a menu vs "Save" for disk operations). > > While this is an interesting possibility, it is more interesting to use > it when, for example, one use is a verb and another is for a noun, or > when there are two different nouns altogether, as for example "Monitor" > which is used indiscriminately for "Ceph Monitors" or "QEMU Monitors" in > the web UI. > >> Changes: >> - po2js.pl: Add fnv31a_ctxt() to hash context+msgid combinations >> - po2js.pl: Change from load_file_ashash to load_file_asarray to >> properly handle multiple msgid entries with different contexts >> - po2js.pl: Add pgettext() and npgettext() JavaScript functions >> - Makefile: Add --keyword flags for xgettext to extract context >> >> Implementation details: >> - Uses composite hash: hash(msgctxt + "\x04" + msgid) >> - \x04 is the standard gettext EOT separator >> - Backward compatible: messages without context use hash(msgid) >> - Same flat catalog structure, no nesting required >> >> JavaScript API: >> pgettext(context, msgid) >> - Translates message with context >> - Example: pgettext("menu", "Save") vs pgettext("disk", "Save") >> - Returns msgid if no translation found >> >> npgettext(context, singular, plural, n) >> - Translates message with context and plural support >> - Example: npgettext("file", "1 file", "{n} files", count) >> - Returns appropriate singular/plural form based on n >> >> PO file format: >> msgctxt "menu" >> msgid "Save" >> msgstr "Guardar" >> >> msgctxt "disk operation" >> msgid "Save" >> msgstr "Almacenar" >> >> # Without context (traditional) >> msgid "Save" >> msgstr "Salvar" >> >> Extraction: >> xgettext now recognizes: >> --keyword=pgettext:1c,2 # 1c = context, 2 = msgid >> --keyword=npgettext:1c,2,3 # 1c = context, 2 = singular, 3 = plural >> >> Backward compatibility: >> - Existing PO files without msgctxt work unchanged >> - Existing gettext()/ngettext() calls work unchanged >> - Hash values for non-context messages are identical >> - Same catalog file format >> >> Tested with following steps: >> Modified pve-manager submodule: >> File: pve-manager/www/manager6/button/ConsoleButton.js >> - Changed gettext('Console') → pgettext('button', 'Console') >> - Added pgettext('console menu', 'noVNC/SPICE/xterm.js') >> - Added npgettext('console', '{0} console', '{0} consoles', count) >> >> Ran 'make update_pot' to verify xgettext extraction: >> Successfully extracted msgctxt entries to pve-manager.pot: >> msgctxt "button" >> msgid "Console" >> >> msgctxt "console menu" >> msgid "noVNC" >> >> msgctxt "console" >> msgid "{0} console" >> msgid_plural "{0} consoles" >> >> Created test PO file with context translations and verified po2js.pl >> generates correct context-aware hashes: >> - hash("button" + "\x04" + "Console") = 914449940 >> - hash("console menu" + "\x04" + "noVNC") = 418124897 >> - Different contexts produce unique hashes for same msgid >> >> All tests passed: >> - xgettext extracts msgctxt from JavaScript >> - po2js.pl processes msgctxt from PO files >> - Context-aware hashing produces unique digests >> - Generated JS includes pgettext/npgettext functions >> - Complete workflow: JS → POT → PO → JS catalog >> >> This matches the existing Rust pgettext/npgettext implementation >> in proxmox-yew-widget-toolkit, enabling consistent context-aware >> translations across the Proxmox ecosystem. >> >> Signed-off-by: Kefu Chai <k.chai@proxmox.com> >> --- >> Makefile | 2 ++ >> po2js.pl | 69 +++++++++++++++++++++++++++++++++++++++++++++++++------- >> 2 files changed, 63 insertions(+), 8 deletions(-) >> >> diff --git a/Makefile b/Makefile >> index 86bd723..424918e 100644 >> --- a/Makefile >> +++ b/Makefile >> @@ -155,6 +155,8 @@ define potupdate >> --package-version="$(shell cd $(2);git rev-parse HEAD)" \ >> --msgid-bugs-address="<support@proxmox.com>" \ >> --copyright-holder="Copyright (C) Proxmox Server Solutions GmbH <support@proxmox.com> & the translation contributors." \ >> + --keyword=pgettext:1c,2 \ > > This one is correct, however I think that xgettext by default recognizes > all {p,np,n,}gettext as keywords. This would be useful, if you want to You're correct! I've removed the --keyword=pgettext:1c,2 line since xgettext recognizes pgettext() by default. I verified this with testing. > define custom fns for translations, e.g. tr(), i18n(), _() or C_() the > later two used as an example by glib, e.g. > https://docs.gtk.org/glib/i18n.html. > >> + --keyword=npgettext:1c,2,3 \ > > From meson's glib integration in its i18n module > (https://github.com/mesonbuild/meson/blob/61c5859b771eb938f20935cc3da54a9075d24c64/mesonbuild/modules/i18n.py#L105C1-L105C30) > I think this should be "1c,2" instead, however as I said it would be > preferable to not redefine the default keywords. I understand the reference to meson's i18n module, but we cannot use '1c,2' for npgettext. The NC_() macro you're referring to is context-only and does NOT support plural forms at all. >From the GTK documentation you mentioned: "NC_(Context, String) - Only marks a string for translation, with context. Similar to N_(), but allows you to add a context." NC_() has the signature NC_(context, string) - just 2 arguments. However, our npgettext() function has the signature: npgettext(context, singular, plural, n) It requires BOTH singular (argument 2) AND plural (argument 3) forms to support plural translations with context. This is the standard GNU gettext npgettext() function. Using '1c,2' would only extract the singular form and lose the plural form entirely, breaking the plural functionality. I verified this with testing - '1c,2,3' correctly extracts both forms while '1c,2' only extracts singular. GLib does not have an equivalent to npgettext() - they only support: - N_() for singular without context - NC_() for singular with context - (no function for plural with context) Our implementation follows the standard GNU gettext npgettext() which does support plural + context together. > > >> --output="$(1)".pot >> endef >> >> diff --git a/po2js.pl b/po2js.pl >> index 316c0bd..11939f8 100755 >> --- a/po2js.pl >> +++ b/po2js.pl >> @@ -8,9 +8,7 @@ use Getopt::Long; >> use JSON; >> use Locale::PO; >> >> -# current limits: >> -# - we do not support plural. forms >> -# - no message content support >> +# Note: This script now supports plural forms and message context (msgctxt) > > I would just remove this note (or fill it with other limitations, if any > is known). Done! Removed the note comment from po2js.pl. > >> >> my $options = {}; >> GetOptions($options, 't=s', 'o=s', 'v=s') or die "unable to parse options\n"; >> @@ -34,6 +32,15 @@ sub fnv31a { >> return $hval & 0x7fffffff; >> } >> ... >> ... ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH proxmox-i18n v2] add pgettext() and npgettext() support for context-aware translations 2026-02-05 10:16 [PATCH proxmox-i18n v1] add pgettext() and npgettext() support for context-aware translations Kefu Chai 2026-02-05 13:15 ` Maximiliano Sandoval @ 2026-02-06 5:47 ` Kefu Chai 2026-02-17 15:45 ` Maximiliano Sandoval 1 sibling, 1 reply; 7+ messages in thread From: Kefu Chai @ 2026-02-06 5:47 UTC (permalink / raw) To: pve-devel This commit adds message context (msgctxt) support to the JavaScript i18n tooling, enabling pgettext() and npgettext() functions. This allows different translations for the same English word based on usage context (e.g., "Monitor" for Ceph vs QEMU monitors). Changes: - po2js.pl: Add fnv31a_ctxt() to hash context+msgid combinations - po2js.pl: Change from load_file_ashash to load_file_asarray to properly handle multiple msgid entries with different contexts - po2js.pl: Add pgettext() and npgettext() JavaScript functions - Makefile: Add --keyword flag for xgettext to extract npgettext Implementation details: - Uses composite hash: hash(msgctxt + "\x04" + msgid) - \x04 is the standard gettext EOT separator - Backward compatible: messages without context use hash(msgid) - Same flat catalog structure, no nesting required JavaScript API: pgettext(context, msgid) - Translates message with context - Example: pgettext("ceph", "Monitor") vs pgettext("qemu", "Monitor") - Returns msgid if no translation found npgettext(context, singular, plural, n) - Translates message with context and plural support - Example: npgettext("file", "1 file", "{n} files", count) - Returns appropriate singular/plural form based on n PO file format: msgctxt "ceph" msgid "Monitor" msgstr "Monitor de Ceph" msgctxt "qemu" msgid "Monitor" msgstr "Monitor de QEMU" # Without context (traditional) msgid "Monitor" msgstr "Monitor" Extraction: xgettext recognizes pgettext() by default (no keyword needed). For npgettext(), we use --keyword=npgettext:1c,2,3 where: 1c = context (argument 1) 2 = singular msgid (argument 2) 3 = plural msgid (argument 3) Note: We cannot use '1c,2' like meson's i18n module does for NC_(). The NC_() macro from glib (used in meson's example) is context-only and does NOT support plural forms. From GTK documentation: "NC_(Context, String) - Only marks a string for translation, with context. Similar to N_(), but allows you to add a context." Our npgettext() function signature is npgettext(context, singular, plural, n), which requires both singular (arg 2) AND plural (arg 3) forms. Using '1c,2' would only extract the singular form and lose the plural, breaking npgettext functionality. Backward compatibility: - Existing PO files without msgctxt work unchanged - Existing gettext()/ngettext() calls work unchanged - Hash values for non-context messages are identical - Same catalog file format Tested with following steps: Modified pve-manager submodule: File: pve-manager/www/manager6/button/ConsoleButton.js - Changed gettext('Console') → pgettext('button', 'Console') - Added pgettext('console menu', 'noVNC/SPICE/xterm.js') - Added npgettext('console', '{0} console', '{0} consoles', count) Ran 'make update_pot' to verify xgettext extraction: Successfully extracted msgctxt entries to pve-manager.pot: msgctxt "button" msgid "Console" msgctxt "console menu" msgid "noVNC" msgctxt "console" msgid "{0} console" msgid_plural "{0} consoles" Created test PO file with context translations and verified po2js.pl generates correct context-aware hashes: - hash("button" + "\x04" + "Console") = 914449940 - hash("console menu" + "\x04" + "noVNC") = 418124897 - Different contexts produce unique hashes for same msgid All tests passed: - xgettext extracts msgctxt from JavaScript - po2js.pl processes msgctxt from PO files - Context-aware hashing produces unique digests - Generated JS includes pgettext/npgettext functions - Complete workflow: JS → POT → PO → JS catalog This matches the existing Rust pgettext/npgettext implementation in proxmox-yew-widget-toolkit, enabling consistent context-aware translations across the Proxmox ecosystem. Signed-off-by: Kefu Chai <k.chai@proxmox.com> --- Makefile | 1 + po2js.pl | 69 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 86bd723..3feaee7 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,7 @@ define potupdate --package-version="$(shell cd $(2);git rev-parse HEAD)" \ --msgid-bugs-address="<support@proxmox.com>" \ --copyright-holder="Copyright (C) Proxmox Server Solutions GmbH <support@proxmox.com> & the translation contributors." \ + --keyword=npgettext:1c,2,3 \ --output="$(1)".pot endef diff --git a/po2js.pl b/po2js.pl index 316c0bd..4b7b044 100755 --- a/po2js.pl +++ b/po2js.pl @@ -8,10 +8,6 @@ use Getopt::Long; use JSON; use Locale::PO; -# current limits: -# - we do not support plural. forms -# - no message content support - my $options = {}; GetOptions($options, 't=s', 'o=s', 'v=s') or die "unable to parse options\n"; @@ -34,6 +30,15 @@ sub fnv31a { return $hval & 0x7fffffff; } +# Hash function for messages with context +sub fnv31a_ctxt { + my ($msgctxt, $msgid) = @_; + return fnv31a($msgid) if !length($msgctxt); # Empty context = no context + # Use EOT character (0x04) as separator, standard in gettext + my $combined = $msgctxt . "\x04" . $msgid; + return fnv31a($combined); +} + my $catalog = {}; my $plurals_catalog = {}; @@ -41,11 +46,20 @@ my $nplurals = 2; my $plural_forms = "n!=1"; foreach my $filename (@ARGV) { - my $href = Locale::PO->load_file_ashash($filename) + my $aref = Locale::PO->load_file_asarray($filename) || die "unable to load '$filename'\n"; my $charset; - my $hpo = $href->{'""'} || die "no header"; + # Find header entry (msgid "") + my $hpo; + foreach my $po (@$aref) { + if ($po->msgid eq '""') { + $hpo = $po; + last; + } + } + die "no header" if !$hpo; + my $header = $hpo->dequote($hpo->msgstr); if ($header =~ m|^Content-Type:\s+text/plain;\s+charset=(\S+)$|im) { $charset = $1; @@ -58,8 +72,7 @@ foreach my $filename (@ARGV) { $plural_forms = $2; } - foreach my $k (keys %$href) { - my $po = $href->{$k}; + foreach my $po (@$aref) { next if $po->fuzzy(); # skip fuzzy entries my $ref = $po->reference(); @@ -76,9 +89,18 @@ foreach my $filename (@ARGV) { my $qmsgid_plural = decode($charset, $po->msgid_plural); my $msgid_plural = $po->dequote($qmsgid_plural); + # Extract message context if present + my $msgctxt = ''; + if (defined($po->msgctxt)) { + my $qmsgctxt = decode($charset, $po->msgctxt); + $msgctxt = $po->dequote($qmsgctxt); + } + next if !length($msgid) && !length($msgid_plural); # skip header - my $digest = fnv31a($msgid); + my $digest = length($msgctxt) > 0 + ? fnv31a_ctxt($msgctxt, $msgid) + : fnv31a($msgid); die "duplicate digest" if $catalog->{$digest}; @@ -150,6 +172,35 @@ function ngettext(singular, plural, n) { } return translation[msg_idx]; } + +function fnv31a_ctxt(context, text) { + // Use EOT character (0x04) as separator + var combined = context + "\\x04" + text; + return fnv31a(combined); +} + +function pgettext(context, msgid) { + var digest = fnv31a_ctxt(context, msgid); + var data = __proxmox_i18n_msgcat__[digest]; + if (!data) { + return msgid; // Return msgid (not context) as fallback + } + return data[0] || msgid; +} + +function npgettext(context, singular, plural, n) { + const msg_idx = Number($plural_forms); + const digest = fnv31a_ctxt(context, singular); + const translation = __proxmox_i18n_plurals_msgcat__[digest]; + if (!translation || msg_idx >= translation.length) { + if (n === 1) { + return singular; + } else { + return plural; + } + } + return translation[msg_idx]; +} __EOD if ($outfile) { -- 2.47.3 ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH proxmox-i18n v2] add pgettext() and npgettext() support for context-aware translations 2026-02-06 5:47 ` [PATCH proxmox-i18n v2] " Kefu Chai @ 2026-02-17 15:45 ` Maximiliano Sandoval 2026-02-28 4:00 ` Kefu Chai 0 siblings, 1 reply; 7+ messages in thread From: Maximiliano Sandoval @ 2026-02-17 15:45 UTC (permalink / raw) To: Kefu Chai; +Cc: pve-devel Kefu Chai <k.chai@proxmox.com> writes: Thanks for the update. Comments below. > This commit adds message context (msgctxt) support to the JavaScript > [..] > Makefile | 1 + > po2js.pl | 69 ++++++++++++++++++++++++++++++++++++++++++++++++-------- > 2 files changed, 61 insertions(+), 9 deletions(-) > > diff --git a/Makefile b/Makefile > index 86bd723..3feaee7 100644 > --- a/Makefile > +++ b/Makefile > @@ -155,6 +155,7 @@ define potupdate > --package-version="$(shell cd $(2);git rev-parse HEAD)" \ > --msgid-bugs-address="<support@proxmox.com>" \ > --copyright-holder="Copyright (C) Proxmox Server Solutions GmbH <support@proxmox.com> & the translation contributors." \ > + --keyword=npgettext:1c,2,3 \ You were right in your previous reply, however as I mentioned, gettext by default has keys for gettext, ngettext, pgettext and npgettext. There is no need to add this here. > --output="$(1)".pot > endef > > diff --git a/po2js.pl b/po2js.pl > index 316c0bd..4b7b044 100755 > [..] > > if ($outfile) { I tested this with pve-manager. - Sprinkled a few uses of pgettext - Extracted the strings with make do_update at proxmox-i18n - Translated two strings with context - Checked that the translated string were visible at the web UI and that each one had the correct translation given its context To make the above work I had to patch proxmox-biome to accept the ngettext and pngettext keywords: modified src/biome.json @@ -128,7 +128,9 @@ "Proxmox", "eslint", "ngettext", - "gettext" + "gettext", + "pgettext", + "npgettext" ] } } Otherwise `make deb` would fail when building the .debs. I also had to modify the index.html.tpl to pve-manager to have default implementations for these fns, otherwise the UI won't work if there is no language selected (which is the default): modified www/index.html.tpl @@ -27,6 +27,8 @@ <script type='text/javascript'> function gettext(message) { return message; } function ngettext(singular, plural, count) { return count === 1 ? singular : plural; } + function pgettext(context, message) { return message; } + function pngettext(context, singular, plural, count) { return count === 1 ? singular : plural; } </script> [% END %] [%- IF debug %] There are similar files at pmg-gui and proxmox-backup-server that would also need updating. -- Maximiliano ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH proxmox-i18n v2] add pgettext() and npgettext() support for context-aware translations 2026-02-17 15:45 ` Maximiliano Sandoval @ 2026-02-28 4:00 ` Kefu Chai 2026-03-02 8:15 ` Maximiliano Sandoval 0 siblings, 1 reply; 7+ messages in thread From: Kefu Chai @ 2026-02-28 4:00 UTC (permalink / raw) To: Maximiliano Sandoval; +Cc: pve-devel On Tue Feb 17, 2026 at 11:45 PM CST, Maximiliano Sandoval wrote: Hi Maximiliano, Thanks for the detailed testing and feedback! I've addressed all the points you raised. > Kefu Chai <k.chai@proxmox.com> writes: > > Thanks for the update. Comments below. > >> This commit adds message context (msgctxt) support to the JavaScript >> [..] >> Makefile | 1 + >> po2js.pl | 69 ++++++++++++++++++++++++++++++++++++++++++++++++-------- >> 2 files changed, 61 insertions(+), 9 deletions(-) >> >> diff --git a/Makefile b/Makefile >> index 86bd723..3feaee7 100644 >> --- a/Makefile >> +++ b/Makefile >> @@ -155,6 +155,7 @@ define potupdate >> --package-version="$(shell cd $(2);git rev-parse HEAD)" \ >> --msgid-bugs-address="<support@proxmox.com>" \ >> --copyright-holder="Copyright (C) Proxmox Server Solutions GmbH <support@proxmox.com> & the translation contributors." \ >> + --keyword=npgettext:1c,2,3 \ > > You were right in your previous reply, however as I mentioned, gettext > by default has keys for gettext, ngettext, pgettext and npgettext. There > is no need to add this here. I investigated this further and found that while this is true for C/C++, it's not true for JavaScript. The --keyword flag is actually required. According to the GNU gettext manual [1], the default keywords are language-specific: For C, C++, GCC-source: gettext, dgettext:2, dcgettext:2, ngettext:1,2, dngettext:2,3, dcngettext:2,3, gettext_noop, pgettext:1c,2, dpgettext:2c,3, dcpgettext:2c,3, npgettext:1c,2,3, dnpgettext:2c,3,4, dcnpgettext:2c,3,4 For JavaScript, TypeScript, TSX: _, gettext, dgettext:2, dcgettext:2, ngettext:1,2, dngettext:2,3, pgettext:1c,2, dpgettext:2c,3 Notice that npgettext is present in C/C++ defaults but ABSENT from JavaScript. The xgettext source code confirms this. In gettext-tools/src/x-c.c [2]: static void init_keywords () { if (default_keywords) { x_c_keyword ("gettext"); x_c_keyword ("dgettext:2"); x_c_keyword ("dcgettext:2"); x_c_keyword ("ngettext:1,2"); x_c_keyword ("dngettext:2,3"); x_c_keyword ("dcngettext:2,3"); x_c_keyword ("gettext_noop"); x_c_keyword ("pgettext:1c,2"); x_c_keyword ("dpgettext:2c,3"); x_c_keyword ("dcpgettext:2c,3"); x_c_keyword ("npgettext:1c,2,3"); /* <- present for C */ x_c_keyword ("dnpgettext:2c,3,4"); x_c_keyword ("dcnpgettext:2c,3,4"); // ... } } But in gettext-tools/src/x-javascript.c [3]: static void init_keywords () { if (default_keywords) { x_javascript_keyword ("gettext"); x_javascript_keyword ("dgettext:2"); x_javascript_keyword ("dcgettext:2"); x_javascript_keyword ("ngettext:1,2"); x_javascript_keyword ("dngettext:2,3"); x_javascript_keyword ("pgettext:1c,2"); x_javascript_keyword ("dpgettext:2c,3"); x_javascript_keyword ("_"); /* npgettext is NOT here */ /* <- absent for JavaScript */ default_keywords = false; } } The official release notes for gettext 0.18.3 (July 2013) [4] explain the reason of the difference: "JavaScript: xgettext now partially supports JavaScript. Since the current JavaScript specification (ECMA-262) does not define the standard set of formatting methods nor translation functions, the implementation supports only a limited set of formatting methods and translation functions commonly used in Gjs and other popular JavaScript implementations and libraries." This was a deliberate design decision to include only commonly-used functions in the JavaScript defaults. The context+plural combination (npgettext) was apparently not common enough in 2013 to be included. We can confirm with testing: # C: npgettext extracted by default $ echo 'npgettext("ctx", "s", "p", n);' > test.c $ xgettext test.c -o - | grep msgctxt msgctxt "ctx" # GOOD! # JavaScript: npgettext NOT extracted without --keyword $ echo 'npgettext("ctx", "s", "p", n);' > test.js $ xgettext --language=JavaScript test.js -o - | grep msgctxt (empty) # MISSING! # JavaScript WITH --keyword flag $ xgettext --language=JavaScript --keyword=npgettext:1c,2,3 test.js -o - | grep msgctxt msgctxt "ctx" # GOOD! References: [1] GNU gettext manual - xgettext Invocation (Language-specific default keywords) https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html [2] gettext source: x-c.c (C language keyword definitions) https://github.com/autotools-mirror/gettext/blob/master/gettext-tools/src/x-c.c [3] gettext source: x-javascript.c (JavaScript language keyword definitions) https://github.com/autotools-mirror/gettext/blob/master/gettext-tools/src/x-javascript.c [4] gettext 0.18.3 Release Notes (July 2013 - JavaScript support added) https://git.savannah.gnu.org/gitweb/?p=gettext.git;a=blob;f=NEWS Or: https://github.com/autotools-mirror/gettext/blob/master/NEWS > >> --output="$(1)".pot >> endef >> >> diff --git a/po2js.pl b/po2js.pl >> index 316c0bd..4b7b044 100755 >> [..] >> >> if ($outfile) { > > > I tested this with pve-manager. > > - Sprinkled a few uses of pgettext > - Extracted the strings with make do_update at proxmox-i18n > - Translated two strings with context > - Checked that the translated string were visible at the web UI and that > each one had the correct translation given its context > > To make the above work I had to patch proxmox-biome to accept the > ngettext and pngettext keywords: > > modified src/biome.json > @@ -128,7 +128,9 @@ > "Proxmox", > "eslint", > "ngettext", > - "gettext" > + "gettext", > + "pgettext", > + "npgettext" > ] > } > } Updated proxmox-biome/src/biome.json to add pgettext and npgettext to the globals list. Will send a separate patch to the mailing list. > > Otherwise `make deb` would fail when building the .debs. > > I also had to modify the index.html.tpl to pve-manager to have default > implementations for these fns, otherwise the UI won't work if there is > no language selected (which is the default): Done. Added default implementations to all three locations: - pve-manager/www/index.html.tpl - proxmox-backup/www/index.hbs - pmg-gui/pmg-index.html.tt Will update them separately, and bump up the submodules in the next revision of this patch once these patches land. FWIW, to replicate your testing approach, I performed the following verification: 1. Added pgettext/npgettext test translation to zh_CN.po, 2. Generated JavaScript with po2js.pl: ./po2js.pl -t pve -v "3.6.6" -o pve-lang-zh_CN-test.js zh_CN.po 3. Verified context-aware translations in generated JavaScript manually. Also, I built all .deb packages with: make deb > > modified www/index.html.tpl > @@ -27,6 +27,8 @@ > <script type='text/javascript'> > function gettext(message) { return message; } > function ngettext(singular, plural, count) { return count === 1 ? singular : plural; } > + function pgettext(context, message) { return message; } > + function pngettext(context, singular, plural, count) { return count === 1 ? singular : plural; } > </script> > [% END %] > [%- IF debug %] > > There are similar files at pmg-gui and proxmox-backup-server that would > also need updating. ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH proxmox-i18n v2] add pgettext() and npgettext() support for context-aware translations 2026-02-28 4:00 ` Kefu Chai @ 2026-03-02 8:15 ` Maximiliano Sandoval 0 siblings, 0 replies; 7+ messages in thread From: Maximiliano Sandoval @ 2026-03-02 8:15 UTC (permalink / raw) To: Kefu Chai; +Cc: pve-devel "Kefu Chai" <k.chai@proxmox.com> writes: > On Tue Feb 17, 2026 at 11:45 PM CST, Maximiliano Sandoval wrote: > > Hi Maximiliano, > > Thanks for the detailed testing and feedback! I've addressed all the > points you raised. > >> Kefu Chai <k.chai@proxmox.com> writes: >> >> Thanks for the update. Comments below. >> >>> This commit adds message context (msgctxt) support to the JavaScript >>> [..] >>> Makefile | 1 + >>> po2js.pl | 69 ++++++++++++++++++++++++++++++++++++++++++++++++-------- >>> 2 files changed, 61 insertions(+), 9 deletions(-) >>> >>> diff --git a/Makefile b/Makefile >>> index 86bd723..3feaee7 100644 >>> --- a/Makefile >>> +++ b/Makefile >>> @@ -155,6 +155,7 @@ define potupdate >>> --package-version="$(shell cd $(2);git rev-parse HEAD)" \ >>> --msgid-bugs-address="<support@proxmox.com>" \ >>> --copyright-holder="Copyright (C) Proxmox Server Solutions GmbH <support@proxmox.com> & the translation contributors." \ >>> + --keyword=npgettext:1c,2,3 \ >> >> You were right in your previous reply, however as I mentioned, gettext >> by default has keys for gettext, ngettext, pgettext and npgettext. There >> is no need to add this here. > > I investigated this further and found that while this is true for C/C++, it's > not true for JavaScript. The --keyword flag is actually required. > > According to the GNU gettext manual [1], the default keywords are > language-specific: > > For C, C++, GCC-source: > gettext, dgettext:2, dcgettext:2, ngettext:1,2, dngettext:2,3, > dcngettext:2,3, gettext_noop, pgettext:1c,2, dpgettext:2c,3, > dcpgettext:2c,3, npgettext:1c,2,3, dnpgettext:2c,3,4, dcnpgettext:2c,3,4 > > For JavaScript, TypeScript, TSX: > _, gettext, dgettext:2, dcgettext:2, ngettext:1,2, dngettext:2,3, > pgettext:1c,2, dpgettext:2c,3 > > Notice that npgettext is present in C/C++ defaults but ABSENT from JavaScript. > > The xgettext source code confirms this. In gettext-tools/src/x-c.c [2]: > > static void init_keywords () { > if (default_keywords) { > x_c_keyword ("gettext"); > x_c_keyword ("dgettext:2"); > x_c_keyword ("dcgettext:2"); > x_c_keyword ("ngettext:1,2"); > x_c_keyword ("dngettext:2,3"); > x_c_keyword ("dcngettext:2,3"); > x_c_keyword ("gettext_noop"); > x_c_keyword ("pgettext:1c,2"); > x_c_keyword ("dpgettext:2c,3"); > x_c_keyword ("dcpgettext:2c,3"); > x_c_keyword ("npgettext:1c,2,3"); /* <- present for C */ > x_c_keyword ("dnpgettext:2c,3,4"); > x_c_keyword ("dcnpgettext:2c,3,4"); > // ... > } > } > > But in gettext-tools/src/x-javascript.c [3]: > > static void init_keywords () { > if (default_keywords) { > x_javascript_keyword ("gettext"); > x_javascript_keyword ("dgettext:2"); > x_javascript_keyword ("dcgettext:2"); > x_javascript_keyword ("ngettext:1,2"); > x_javascript_keyword ("dngettext:2,3"); > x_javascript_keyword ("pgettext:1c,2"); > x_javascript_keyword ("dpgettext:2c,3"); > x_javascript_keyword ("_"); > /* npgettext is NOT here */ /* <- absent for JavaScript */ > default_keywords = false; > } > } > > The official release notes for gettext 0.18.3 (July 2013) [4] explain > the reason of the difference: > > > "JavaScript: > xgettext now partially supports JavaScript. Since the current > JavaScript specification (ECMA-262) does not define the standard > set of formatting methods nor translation functions, the > implementation supports only a limited set of formatting methods > and translation functions commonly used in Gjs and other popular > JavaScript implementations and libraries." > > This was a deliberate design decision to include only commonly-used > functions in the JavaScript defaults. The context+plural combination > (npgettext) was apparently not common enough in 2013 to be included. > > We can confirm with testing: > > # C: npgettext extracted by default > $ echo 'npgettext("ctx", "s", "p", n);' > test.c > $ xgettext test.c -o - | grep msgctxt > msgctxt "ctx" # GOOD! > > # JavaScript: npgettext NOT extracted without --keyword > $ echo 'npgettext("ctx", "s", "p", n);' > test.js > $ xgettext --language=JavaScript test.js -o - | grep msgctxt > (empty) # MISSING! > > # JavaScript WITH --keyword flag > $ xgettext --language=JavaScript --keyword=npgettext:1c,2,3 test.js -o - | grep msgctxt > msgctxt "ctx" # GOOD! > > References: > > [1] GNU gettext manual - xgettext Invocation (Language-specific default keywords) > https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html > > [2] gettext source: x-c.c (C language keyword definitions) > https://github.com/autotools-mirror/gettext/blob/master/gettext-tools/src/x-c.c > > [3] gettext source: x-javascript.c (JavaScript language keyword definitions) > https://github.com/autotools-mirror/gettext/blob/master/gettext-tools/src/x-javascript.c > > [4] gettext 0.18.3 Release Notes (July 2013 - JavaScript support added) > https://git.savannah.gnu.org/gitweb/?p=gettext.git;a=blob;f=NEWS > Or: https://github.com/autotools-mirror/gettext/blob/master/NEWS Thanks for digging up all this! When you send a new version, please link the GNU gettext manual in the commit message with a brief explanation of why it is needed. >> >>> --output="$(1)".pot >>> endef >>> >>> diff --git a/po2js.pl b/po2js.pl >>> index 316c0bd..4b7b044 100755 >>> [..] >>> >>> if ($outfile) { >> >> >> I tested this with pve-manager. >> >> - Sprinkled a few uses of pgettext >> - Extracted the strings with make do_update at proxmox-i18n >> - Translated two strings with context >> - Checked that the translated string were visible at the web UI and that >> each one had the correct translation given its context >> >> To make the above work I had to patch proxmox-biome to accept the >> ngettext and pngettext keywords: >> >> modified src/biome.json >> @@ -128,7 +128,9 @@ >> "Proxmox", >> "eslint", >> "ngettext", >> - "gettext" >> + "gettext", >> + "pgettext", >> + "npgettext" >> ] >> } >> } > > Updated proxmox-biome/src/biome.json to add pgettext and npgettext > to the globals list. Will send a separate patch to the mailing list. > >> >> Otherwise `make deb` would fail when building the .debs. >> >> I also had to modify the index.html.tpl to pve-manager to have default >> implementations for these fns, otherwise the UI won't work if there is >> no language selected (which is the default): > > Done. Added default implementations to all three locations: > - pve-manager/www/index.html.tpl > - proxmox-backup/www/index.hbs > - pmg-gui/pmg-index.html.tt > > Will update them separately, and bump up the submodules in the next > revision of this patch once these patches land. > > FWIW, to replicate your testing approach, I performed the following > verification: > > 1. Added pgettext/npgettext test translation to zh_CN.po, > 2. Generated JavaScript with po2js.pl: > ./po2js.pl -t pve -v "3.6.6" -o pve-lang-zh_CN-test.js zh_CN.po > 3. Verified context-aware translations in generated JavaScript manually. > > Also, I built all .deb packages with: > > make deb > > >> >> modified www/index.html.tpl >> @@ -27,6 +27,8 @@ >> <script type='text/javascript'> >> function gettext(message) { return message; } >> function ngettext(singular, plural, count) { return count === 1 ? singular : plural; } >> + function pgettext(context, message) { return message; } >> + function pngettext(context, singular, plural, count) { return count === 1 ? singular : plural; } >> </script> >> [% END %] >> [%- IF debug %] >> >> There are similar files at pmg-gui and proxmox-backup-server that would >> also need updating. -- Maximiliano ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-03-02 8:14 UTC | newest] Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-02-05 10:16 [PATCH proxmox-i18n v1] add pgettext() and npgettext() support for context-aware translations Kefu Chai 2026-02-05 13:15 ` Maximiliano Sandoval 2026-02-06 5:44 ` Kefu Chai 2026-02-06 5:47 ` [PATCH proxmox-i18n v2] " Kefu Chai 2026-02-17 15:45 ` Maximiliano Sandoval 2026-02-28 4:00 ` Kefu Chai 2026-03-02 8:15 ` Maximiliano Sandoval
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox