From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 433121FF13E for ; Fri, 06 Feb 2026 06:44:31 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 431E21EFC1; Fri, 6 Feb 2026 06:45:03 +0100 (CET) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Fri, 06 Feb 2026 13:44:50 +0800 Message-Id: Subject: Re: [PATCH proxmox-i18n v1] add pgettext() and npgettext() support for context-aware translations From: "Kefu Chai" To: "Maximiliano Sandoval" X-Mailer: aerc 0.20.0 References: <20260205101613.1067594-2-k.chai@proxmox.com> In-Reply-To: X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1770356618422 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.140 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_SHORT 0.001 Use of a URL Shortener for very short URL RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: F4TF37MO44OD6TCXVAMWYMMJOPQLYKNA X-Message-ID-Hash: F4TF37MO44OD6TCXVAMWYMMJOPQLYKNA X-MailFrom: k.chai@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: pve-devel@lists.proxmox.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 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=3Dpgettext:1c,2 # 1c =3D context, 2 =3D msgid >> --keyword=3Dnpgettext:1c,2,3 # 1c =3D context, 2 =3D singular, 3 = =3D 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') =E2=86=92 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") =3D 914449940 >> - hash("console menu" + "\x04" + "noVNC") =3D 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 =E2=86=92 POT =E2=86=92 PO =E2=86=92 JS catalo= g >> >> 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 >> --- >> 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=3D"$(shell cd $(2);git rev-parse HEAD)" \ >> --msgid-bugs-address=3D"" \ >> --copyright-holder=3D"Copyright (C) Proxmox Server Solutions GmbH= & the translation contributors." \ >> + --keyword=3Dpgettext: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=3Dpgettext: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=3Dnpgettext:1c,2,3 \ > > From meson's glib integration in its i18n module > (https://github.com/mesonbuild/meson/blob/61c5859b771eb938f20935cc3da54a9= 075d24c64/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=3D"$(1)".pot >> endef >> =20 >> 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; >> =20 >> -# current limits: >> -# - we do not support plural. forms >> -# - no message content support >> +# Note: This script now supports plural forms and message context (msgc= txt) > > I would just remove this note (or fill it with other limitations, if any > is known). Done! Removed the note comment from po2js.pl. > >> =20 >> my $options =3D {}; >> GetOptions($options, 't=3Ds', 'o=3Ds', 'v=3Ds') or die "unable to parse= options\n"; >> @@ -34,6 +32,15 @@ sub fnv31a { >> return $hval & 0x7fffffff; >> } >> ... >> ...