* [PATCH v4 i18n 0/1] add pgettext() and npgettext() support for context-aware translations @ 2026-06-10 7:59 Kefu Chai 2026-06-10 7:59 ` [PATCH v4 i18n 1/1] " Kefu Chai 0 siblings, 1 reply; 3+ messages in thread From: Kefu Chai @ 2026-06-10 7:59 UTC (permalink / raw) To: pve-devel Changes since v3: - Drop the submodule bumps. The pgettext/npgettext stubs they would have pulled in are already present in the current proxmox-i18n submodule pointers, picked up by routine bumps since v3 was sent. Kefu Chai (1): add pgettext() and npgettext() support for context-aware translations Makefile | 1 + po2js.pl | 69 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 9 deletions(-) -- 2.47.3 ^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v4 i18n 1/1] add pgettext() and npgettext() support for context-aware translations 2026-06-10 7:59 [PATCH v4 i18n 0/1] add pgettext() and npgettext() support for context-aware translations Kefu Chai @ 2026-06-10 7:59 ` Kefu Chai 2026-07-29 13:50 ` Maximiliano Sandoval 0 siblings, 1 reply; 3+ messages in thread From: Kefu Chai @ 2026-06-10 7:59 UTC (permalink / raw) To: pve-devel Context-aware translations (msgctxt in gettext) distinguish identical msgids that mean different things in different parts of the UI, for example "Monitor" for Ceph versus QEMU. To keep msgid+msgctxt pairs distinct in the generated JS catalog, po2js.pl now hashes the two together with the standard gettext EOT separator ("\x04") between them. Messages without a context fall through to the existing fnv31a(msgid). Locale::PO::load_file_ashash() conflates rows that share the same msgid, so switch to load_file_asarray() to preserve the distinct entries. The generated catalog gains JS pgettext() and npgettext() helpers that perform the same lookup at runtime. The Makefile change adds --keyword=npgettext:1c,2,3 to xgettext. pgettext is in xgettext's default JavaScript keyword list, but npgettext is not: it was omitted from the defaults in gettext 0.18.3 (2013) when JavaScript support first landed, and never added since. Without the explicit flag, xgettext silently drops npgettext() calls during 'make update_pot'. See "xgettext Invocation" in the GNU gettext manual: https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html 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 16aab32..e1d9633 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 related [flat|nested] 3+ messages in thread
* Re: [PATCH v4 i18n 1/1] add pgettext() and npgettext() support for context-aware translations 2026-06-10 7:59 ` [PATCH v4 i18n 1/1] " Kefu Chai @ 2026-07-29 13:50 ` Maximiliano Sandoval 0 siblings, 0 replies; 3+ messages in thread From: Maximiliano Sandoval @ 2026-07-29 13:50 UTC (permalink / raw) To: Kefu Chai; +Cc: pve-devel Kefu Chai <k.chai@proxmox.com> writes: > Context-aware translations (msgctxt in gettext) distinguish identical > msgids that mean different things in different parts of the UI, for > example "Monitor" for Ceph versus QEMU. > > To keep msgid+msgctxt pairs distinct in the generated JS catalog, > po2js.pl now hashes the two together with the standard gettext EOT > separator ("\x04") between them. Messages without a context fall > through to the existing fnv31a(msgid). Locale::PO::load_file_ashash() > conflates rows that share the same msgid, so switch to > load_file_asarray() to preserve the distinct entries. The generated > catalog gains JS pgettext() and npgettext() helpers that perform the > same lookup at runtime. > > The Makefile change adds --keyword=npgettext:1c,2,3 to xgettext. > pgettext is in xgettext's default JavaScript keyword list, but > npgettext is not: it was omitted from the defaults in gettext 0.18.3 > (2013) when JavaScript support first landed, and never added since. > Without the explicit flag, xgettext silently drops npgettext() calls > during 'make update_pot'. > > See "xgettext Invocation" in the GNU gettext manual: > https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html > > 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 16aab32..e1d9633 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) { Tested that strings using pgettext are correctly extracted: #: pve-manager/www/manager6/ceph/Monitor.js:36 #: pve-manager/www/manager6/node/Config.js:373 msgctxt "ceph" msgid "Monitor" msgstr "" #: pve-manager/www/manager6/qemu/Config.js:350 msgctxt "qemu-guest-agent" msgid "Monitor" msgstr "" and that they are displayed correctly on the web UI after translation. This would be a good addition for a couple of strings where we have two versions of the same string which might require different translations. lgtm. Tested-by: Maximiliano Sandoval <m.sandoval@proxmox.com> Reviewed-by: Maximiliano Sandoval <m.sandoval@proxmox.com> -- Maximiliano ^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-07-29 13:51 UTC | newest] Thread overview: 3+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-06-10 7:59 [PATCH v4 i18n 0/1] add pgettext() and npgettext() support for context-aware translations Kefu Chai 2026-06-10 7:59 ` [PATCH v4 i18n 1/1] " Kefu Chai 2026-07-29 13:50 ` Maximiliano Sandoval
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox