From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!.POSTED.blaine.gmane.org!not-for-mail From: Juri Linkov Newsgroups: gmane.emacs.devel Subject: Re: Emacs i18n Date: Sun, 17 Mar 2019 23:23:03 +0200 Organization: LINKOV.NET Message-ID: <87va0htcss.fsf@mail.linkov.net> References: <87o97aq6gz.fsf@jidanni.org> <83o96wk2mi.fsf@gnu.org> <87k1hjfvjd.fsf@mail.linkov.net> <871s3p0zdz.fsf@mail.linkov.net> <83h8ckezyt.fsf@gnu.org> <83o96qegv1.fsf@gnu.org> <32b1ab1b-bef4-629a-8830-b1dcc6915087@cs.ucla.edu> <83a7iae9va.fsf@gnu.org> <05ed2dec-2a84-f7dc-1af5-c9d923992785@cs.ucla.edu> <87bm2p56gu.fsf@mail.linkov.net> <838sxrdgco.fsf@gnu.org> <831s3jdbrk.fsf@gnu.org> <507BBC71-4E90-476A-9F1D-E0D7F0EB56C9@gmail.com> <87k1h5w08o.fsf@mail.linkov.net> <1b23d0ff-3998-b795-9895-7320e7028f70@cs.ucla.edu> <87d0mvu6wo.fsf@mail.linkov.net> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: blaine.gmane.org; posting-host="blaine.gmane.org:195.159.176.226"; logging-data="180141"; mail-complaints-to="usenet@blaine.gmane.org" User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/27.0.50 (x86_64-pc-linux-gnu) To: emacs-devel Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Sun Mar 17 22:44:08 2019 Return-path: Envelope-to: ged-emacs-devel@m.gmane.org Original-Received: from lists.gnu.org ([209.51.188.17]) by blaine.gmane.org with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:256) (Exim 4.89) (envelope-from ) id 1h5dZf-000kgC-CQ for ged-emacs-devel@m.gmane.org; Sun, 17 Mar 2019 22:44:08 +0100 Original-Received: from localhost ([127.0.0.1]:60512 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1h5dZe-0006YM-Ab for ged-emacs-devel@m.gmane.org; Sun, 17 Mar 2019 17:44:06 -0400 Original-Received: from eggs.gnu.org ([209.51.188.92]:47535) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1h5dWl-0005GC-Us for emacs-devel@gnu.org; Sun, 17 Mar 2019 17:41:09 -0400 Original-Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1h5dU5-0005CO-8M for emacs-devel@gnu.org; Sun, 17 Mar 2019 17:38:22 -0400 Original-Received: from palegreen.birch.relay.mailchannels.net ([23.83.209.140]:9529) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1h5dU4-0005Aw-3a for emacs-devel@gnu.org; Sun, 17 Mar 2019 17:38:21 -0400 X-Sender-Id: dreamhost|x-authsender|jurta@jurta.org Original-Received: from relay.mailchannels.net (localhost [127.0.0.1]) by relay.mailchannels.net (Postfix) with ESMTP id 30BFA141829 for ; Sun, 17 Mar 2019 21:38:17 +0000 (UTC) Original-Received: from pdx1-sub0-mail-a47.g.dreamhost.com (100-96-7-25.trex.outbound.svc.cluster.local [100.96.7.25]) (Authenticated sender: dreamhost) by relay.mailchannels.net (Postfix) with ESMTPA id A73821414C0 for ; Sun, 17 Mar 2019 21:38:16 +0000 (UTC) X-Sender-Id: dreamhost|x-authsender|jurta@jurta.org Original-Received: from pdx1-sub0-mail-a47.g.dreamhost.com ([TEMPUNAVAIL]. [64.90.62.162]) (using TLSv1.2 with cipher DHE-RSA-AES256-GCM-SHA384) by 0.0.0.0:2500 (trex/5.16.3); Sun, 17 Mar 2019 21:38:17 +0000 X-MC-Relay: Neutral X-MailChannels-SenderId: dreamhost|x-authsender|jurta@jurta.org X-MailChannels-Auth-Id: dreamhost X-Duck-Share: 22f09b7408f87f5d_1552858696932_1972754818 X-MC-Loop-Signature: 1552858696932:3397661951 X-MC-Ingress-Time: 1552858696932 Original-Received: from pdx1-sub0-mail-a47.g.dreamhost.com (localhost [127.0.0.1]) by pdx1-sub0-mail-a47.g.dreamhost.com (Postfix) with ESMTP id 6CDFF8042D for ; Sun, 17 Mar 2019 14:38:11 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=linkov.net; h=from:to :subject:references:date:in-reply-to:message-id:mime-version :content-type; s=linkov.net; bh=JV/W6pwbLoiLd9m7DLHYKrC/UXM=; b= 2ibGulYZNWg8xgjvZqduBrVnKD4ZPoX7waEt5mxdFn/f56/n0KKmxVOSWvQxVlCt BFa/M7Ydtb1osYo5VbgYOjPyRf5XwApNahkfbEV7LlnSeIIEHkj/WjbsNtfuBRCJ lXGlyel61tRTOSmVgFDWjf26db5wqoZ1NQLpDOvfNp0= Original-Received: from mail.jurta.org (m91-129-106-13.cust.tele2.ee [91.129.106.13]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) (Authenticated sender: jurta@jurta.org) by pdx1-sub0-mail-a47.g.dreamhost.com (Postfix) with ESMTPSA id 22E088042C for ; Sun, 17 Mar 2019 14:38:09 -0700 (PDT) X-DH-BACKEND: pdx1-sub0-mail-a47 In-Reply-To: <87d0mvu6wo.fsf@mail.linkov.net> (Juri Linkov's message of "Tue, 12 Mar 2019 23:45:07 +0200") X-VR-OUT-STATUS: OK X-VR-OUT-SCORE: 0 X-VR-OUT-SPAMCAUSE: gggruggvucftvghtrhhoucdtuddrgedutddrheelgdduheegucetufdoteggodetrfdotffvucfrrhhofhhilhgvmecuggftfghnshhusghstghrihgsvgdpffftgfetoffjqffuvfenuceurghilhhouhhtmecufedttdenucenucfjughrpefhvffuohhfffgjkfgfgggtsehmtderredtreejnecuhfhrohhmpefluhhrihcunfhinhhkohhvuceojhhurhhisehlihhnkhhovhdrnhgvtheqnecuffhomhgrihhnpehsohhurhgtvghfohhrghgvrdhiohenucfkphepledurdduvdelrddutdeirddufeenucfrrghrrghmpehmohguvgepshhmthhppdhhvghlohepmhgrihhlrdhjuhhrthgrrdhorhhgpdhinhgvthepledurdduvdelrddutdeirddufedprhgvthhurhhnqdhprghthheplfhurhhiucfnihhnkhhovhcuoehjuhhriheslhhinhhkohhvrdhnvghtqedpmhgrihhlfhhrohhmpehjuhhriheslhhinhhkohhvrdhnvghtpdhnrhgtphhtthhopegvmhgrtghsqdguvghvvghlsehgnhhurdhorhhgnecuvehluhhsthgvrhfuihiivgeptd X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] X-Received-From: 23.83.209.140 X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Original-Sender: "Emacs-devel" Xref: news.gmane.org gmane.emacs.devel:234305 Archived-At: --=-=-= Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable >>> Please note that you have to handle not only format-strings of >>> =E2=80=98message=E2=80=99, but also =E2=80=98error=E2=80=99 and even = more low-level =E2=80=98format=E2=80=99, i.e. all >>> these (error STRING &rest ARGS) (message FORMAT-STRING &rest ARGS) >>> (format-message STRING &rest OBJECTS) (format STRING &rest OBJECTS) >>> >> I expect that 'format' won't translate its first argument, whereas >> 'error', 'message', and 'format-message' will. This will be for the sa= me >> reason that 'format' does not translate quotes. > > Then it should be sufficient to add a gettext call to 'format-message' = only, > because all other related functions 'message', 'error', 'tramp-message'= , > 'tramp-error', etc. all they use 'format-message' directly or indirectl= y. Maybe I'm too stupid to comprehend the complexity of this task in its ent= irety, but I tried to install gettext infrastructure in Emacs with gettextize, and then tried to run xgettext on source code, and see no technical probl= ems. What I tried is to run this command, and it extracts all messages: xgettext --from-code=3DUTF-8 -kformat-message -kmessage -kerror -ktramp= -message -ktramp-error *.el then this command extracts all Gnus messages into a separate file: xgettext --from-code=3DUTF-8 -kformat-message -kmessage -kerror gnus/*.= el -o gnus_messages.po this command extracts all menu items: xgettext --from-code=3DUTF-8 -kmenu-item *.el **/*.el -o menus.po and this extracts all docstrings: xgettext --from-code=3DUTF-8 -kdefcustom:3 -kdefvar:3 -kdefun:3 *.el **= /*.el -o docstrings.po The size of docstrings.po is about 9MB, so perhaps it should reside in a separate catalog defined by e.g. (defdomain emacs-docstrings with semantics similar to defgroup, but I have no opinion about this. I think this project urgently needs a coordinator: to negotiate with package authors and translation teams about how to better split translations to message catalogs. So there are not so much technical problems, but mostly organizational ones. > IIUC, using standard gettext functions this would rather correspond to > > (message (ngettext "Replaced %1$d occurrence%s" > "Replaced %1$d occurrences%s" > replace-count) It seems better to start with this standard function and add more optimizations like =E2=80=98nmessage=E2=80=99 later. Other Lisp implementations use =E2=80=98ngettext=E2=80=99 as well, e.g.: https://clisp.sourceforge.io/impnotes.html#ggettext So I'm going to start with more obvious parts of the task by fixing the current bugs of incorrect English syntax in a forward-compatible way: --=-=-= Content-Type: text/x-diff Content-Disposition: inline; filename=i18n-ngettext.patch diff --git a/lisp/subr.el b/lisp/subr.el index 6c0ad00afa..1f000f77ad 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -342,6 +342,13 @@ define-error (delete-dups (copy-sequence (cons name conditions)))) (when message (put name 'error-message message)))) +(defun ngettext (msgid msgid_plural n &optional _domain _category) + "Return the plural form of the translation for of MSGID and N. +In the given DOMAIN, depending on the given CATEGORY. MSGID and +MSGID_PLURAL should be ASCII strings, and are normally the English singular +and English plural variant of the message, respectively." + (if (/= n 1) msgid_plural msgid)) + ;; We put this here instead of in frame.el so that it's defined even on ;; systems where frame.el isn't loaded. (defun frame-configuration-p (object) diff --git a/lisp/progmodes/grep.el b/lisp/progmodes/grep.el index a5427dd8b7..c0f47159c9 100644 --- a/lisp/progmodes/grep.el +++ b/lisp/progmodes/grep.el @@ -459,7 +459,7 @@ grep-mode-font-lock-keywords ;; remove match from grep-regexp-alist before fontifying ("^Grep[/a-zA-Z]* started.*" (0 '(face nil compilation-message nil help-echo nil mouse-face nil) t)) - ("^Grep[/a-zA-Z]* finished with \\(?:\\(\\(?:[0-9]+ \\)?matches found\\)\\|\\(no matches found\\)\\).*" + ("^Grep[/a-zA-Z]* finished with \\(?:\\(\\(?:[0-9]+ \\)?match\\(?:es\\)? found\\)\\|\\(no matches found\\)\\).*" (0 '(face nil compilation-message nil help-echo nil mouse-face nil) t) (1 compilation-info-face nil t) (2 compilation-warning-face nil t)) @@ -552,7 +552,10 @@ grep-exit-message ;; so the buffer is still unmodified if there is no output. (cond ((and (zerop code) (buffer-modified-p)) (if (> grep-num-matches-found 0) - (cons (format "finished with %d matches found\n" grep-num-matches-found) + (cons (format (ngettext "finished with %d match found\n" + "finished with %d matches found\n" + grep-num-matches-found) + grep-num-matches-found) "matched") '("finished with matches found\n" . "matched"))) ((not (buffer-modified-p)) diff --git a/lisp/replace.el b/lisp/replace.el index 59ad1a375b..318a9fb025 100644 --- a/lisp/replace.el +++ b/lisp/replace.el @@ -983,7 +983,10 @@ flush-lines (progn (forward-line 1) (point))) (setq count (1+ count)))) (set-marker rend nil) - (when interactive (message "Deleted %d matching lines" count)) + (when interactive (message (ngettext "Deleted %d matching line" + "Deleted %d matching lines" + count) + count)) count)) (defun how-many (regexp &optional rstart rend interactive) @@ -1032,9 +1035,10 @@ how-many (if (= opoint (point)) (forward-char 1) (setq count (1+ count)))) - (when interactive (message "%d occurrence%s" - count - (if (= count 1) "" "s"))) + (when interactive (message (ngettext "%d occurrence" + "%d occurrences" + count) + count)) count))) @@ -1617,11 +1621,12 @@ occur-1 (not (eq occur-excluded-properties t)))))) (let* ((bufcount (length active-bufs)) (diff (- (length bufs) bufcount))) - (message "Searched %d buffer%s%s; %s match%s%s" - bufcount (if (= bufcount 1) "" "s") + (message "Searched %d %s%s; %s %s%s" + bufcount + (ngettext "buffer" "buffers" bufcount) (if (zerop diff) "" (format " (%d killed)" diff)) (if (zerop count) "no" (format "%d" count)) - (if (= count 1) "" "es") + (ngettext "match" "matches" count) ;; Don't display regexp if with remaining text ;; it is longer than window-width. (if (> (+ (length (or (get-text-property 0 'isearch-string regexp) @@ -1856,14 +1861,15 @@ occur-engine (let ((beg (point)) end) (insert (propertize - (format "%d match%s%s%s in buffer: %s%s\n" - matches (if (= matches 1) "" "es") + (format "%d %s%s%s in buffer: %s%s\n" + matches + (ngettext "match" "matches" matches) ;; Don't display the same number of lines ;; and matches in case of 1 match per line. (if (= lines matches) - "" (format " in %d line%s" + "" (format " in %d %s" lines - (if (= lines 1) "" "s"))) + (ngettext "line" "lines" lines))) ;; Don't display regexp for multi-buffer. (if (> (length buffers) 1) "" (occur-regexp-descr regexp)) @@ -1889,13 +1895,15 @@ occur-engine (goto-char (point-min)) (let ((beg (point)) end) - (insert (format "%d match%s%s total%s:\n" - global-matches (if (= global-matches 1) "" "es") + (insert (format "%d %s%s total%s:\n" + global-matches + (ngettext "match" "matches" global-matches) ;; Don't display the same number of lines ;; and matches in case of 1 match per line. (if (= global-lines global-matches) - "" (format " in %d line%s" - global-lines (if (= global-lines 1) "" "s"))) + "" (format " in %d %s" + global-lines + (ngettext "line" "lines" global-lines))) (occur-regexp-descr regexp))) (setq end (point)) (when title-face @@ -2730,10 +2738,10 @@ perform-replace (1+ num-replacements)))))) (when (and (eq def 'undo-all) (null (zerop num-replacements))) - (message "Undid %d %s" num-replacements - (if (= num-replacements 1) - "replacement" - "replacements")) + (message (ngettext "Undid %d replacement" + "Undid %d replacements" + num-replacements) + num-replacements) (ding 'no-terminate) (sit-for 1))) (setq replaced nil last-was-undo t last-was-act-and-show nil))) @@ -2859,9 +2867,10 @@ perform-replace last-was-act-and-show nil)))))) (replace-dehighlight)) (or unread-command-events - (message "Replaced %d occurrence%s%s" + (message (ngettext "Replaced %d occurrence%s" + "Replaced %d occurrences%s" + replace-count) replace-count - (if (= replace-count 1) "" "s") (if (> (+ skip-read-only-count skip-filtered-count skip-invisible-count) --=-=-=--