Bugzilla – Attachment 180642 Details for
Bug 39564
Enable runtime translations for plugins (Koha::I18N)
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 39564: Enable runtime translations for plugins (Koha::I18N)
0001-Bug-39564-Enable-runtime-translations-for-plugins-Ko.patch (text/plain), 15.20 KB, created by
Julian Maurice
on 2025-04-04 13:24:47 UTC
(
hide
)
Description:
Bug 39564: Enable runtime translations for plugins (Koha::I18N)
Filename:
MIME Type:
Creator:
Julian Maurice
Created:
2025-04-04 13:24:47 UTC
Size:
15.20 KB
patch
obsolete
>From c5f8196dc4c7b8dd764e648ab73e2aecf3bea66d Mon Sep 17 00:00:00 2001 >From: Julian Maurice <julian.maurice@biblibre.com> >Date: Fri, 4 Apr 2025 13:44:27 +0200 >Subject: [PATCH] Bug 39564: Enable runtime translations for plugins > (Koha::I18N) > >This solution uses gettext's textdomain feature to allow plugins to use >Koha::I18N for their own translations. > >Basically we have one textdomain per plugin and we bind the directory >where a plugin store its translations (.mo files) to its textdomain. > >Plugins have to specify the textdomain like this: > > __d('MyPlugin', 'string to translate') > >or, if within a template: > > [% I18N.td('MyPlugin', 'string to translate') %] > >This patches adds several functions to Koha::I18N: >__d, __dn, __dp, __dnp, __dx, __dnx, __dpx, and __dnpx > >and their template equivalents in Koha::Template::Plugin::I18N > >It also moves some code for string extraction from gulpfile.js to >misc/translator/xgettext-perl and misc/translator/xgettext-js for easier >use by plugins authors. > >(Note that, even if there is a misc/translator/xgettext-js script, this >patch does not add support for JS translations in plugins) > >By default a plugin named Koha::Plugin::Foo will have a textdomain >'Koha-Plugin-Foo'. It can be changed by overriding method >locale_textdomain. > >By default the directory bound to the textdomain will be >Koha/Plugin/Foo/locale. It can be changed by overriding method >locale_dir. >This directory should contain one subdirectory for each language, and >each language subdirectory should contain an LC_MESSAGES subdirectory >which should contain a <textdomain>.mo file. >It should look like this: > >Koha/ > Plugin/ > Foo/ > locale/ > fr_FR/ > LC_MESSAGES/ > Koha-Plugin-Foo.mo > de_DE/ > LC_MESSAGES/ > Koha-Plugin-Foo.mo > ... >--- > Koha/I18N.pm | 122 ++++++++++++++++++++++++++++++++++ > Koha/Plugins/Base.pm | 29 ++++++++ > Koha/Template/Plugin/I18N.pm | 115 +++++++++++++++++++++++++++++++- > gulpfile.js | 12 +--- > misc/translator/xgettext-js | 19 ++++++ > misc/translator/xgettext-perl | 17 +++++ > misc/translator/xgettext-tt2 | 17 +++++ > 7 files changed, 320 insertions(+), 11 deletions(-) > create mode 100755 misc/translator/xgettext-js > create mode 100755 misc/translator/xgettext-perl > >diff --git a/Koha/I18N.pm b/Koha/I18N.pm >index c957e9df40..4c775a675b 100644 >--- a/Koha/I18N.pm >+++ b/Koha/I18N.pm >@@ -26,6 +26,10 @@ use Encode; > use List::Util qw( first ); > use Locale::Messages qw( > bindtextdomain >+ dgettext >+ dngettext >+ dnpgettext >+ dpgettext > gettext > LC_MESSAGES > ngettext >@@ -34,7 +38,9 @@ use Locale::Messages qw( > textdomain > ); > use POSIX qw(); >+use Try::Tiny; > use Koha::Cache::Memory::Lite; >+use Koha::Plugins; > > use parent 'Exporter'; > our @EXPORT = qw( >@@ -47,10 +53,25 @@ our @EXPORT = qw( > __px > __np > __npx >+ >+ __d >+ __dn >+ __dp >+ __dnp >+ __dx >+ __dnx >+ __dpx >+ __dnpx >+ > N__ > N__n > N__p > N__np >+ >+ N__d >+ N__dn >+ N__dp >+ N__dnp > ); > > our $textdomain = 'Koha'; >@@ -84,6 +105,18 @@ sub init { > my $directory = _base_directory(); > textdomain($textdomain); > bindtextdomain( $textdomain, $directory ); >+ >+ my @plugins = Koha::Plugins->get_enabled_plugins(); >+ foreach my $plugin (@plugins) { >+ try { >+ my $locale_dir = $plugin->locale_dir; >+ if ( $locale_dir && -d $locale_dir && -r $locale_dir ) { >+ bindtextdomain($plugin->locale_textdomain, $locale_dir); >+ } >+ } catch { >+ warn sprintf('Failed to initialize translations for plugin %s: %s', ref $plugin, $_); >+ } >+ } > } else { > warn "No locale installed. Localization cannot work and is therefore disabled"; > } >@@ -168,6 +201,79 @@ sub __npx { > return _gettext( \&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count ], %vars ); > } > >+sub __d { >+ my ( $textdomain, $msgid ) = @_; >+ >+ $msgid = Encode::encode_utf8($msgid); >+ >+ return _gettext( \&dgettext, [ $textdomain, $msgid ] ); >+} >+ >+sub __dn { >+ my ( $textdomain, $msgid, $msgid_plural, $count ) = @_; >+ >+ $msgid = Encode::encode_utf8($msgid); >+ $msgid_plural = Encode::encode_utf8($msgid_plural); >+ >+ return _gettext( \&dngettext, [ $textdomain, $msgid, $msgid_plural, $count ] ); >+} >+ >+sub __dp { >+ my ( $textdomain, $msgctxt, $msgid ) = @_; >+ >+ $msgctxt = Encode::encode_utf8($msgctxt); >+ $msgid = Encode::encode_utf8($msgid); >+ >+ return _gettext( \&dpgettext, [ $textdomain, $msgctxt, $msgid ] ); >+} >+ >+sub __dnp { >+ my ( $textdomain, $msgctxt, $msgid, $msgid_plural, $count ) = @_; >+ >+ $msgctxt = Encode::encode_utf8($msgctxt); >+ $msgid = Encode::encode_utf8($msgid); >+ $msgid_plural = Encode::encode_utf8($msgid_plural); >+ >+ return _gettext( \&dnpgettext, [ $textdomain, $msgctxt, $msgid, $msgid_plural, $count ] ); >+} >+ >+sub __dx { >+ my ( $textdomain, $msgid, %vars ) = @_; >+ >+ $msgid = Encode::encode_utf8($msgid); >+ >+ return _gettext( \&dgettext, [ $textdomain, $msgid ], %vars ); >+} >+ >+sub __dnx { >+ my ( $textdomain, $msgid, $msgid_plural, $count, %vars ) = @_; >+ >+ $msgid = Encode::encode_utf8($msgid); >+ $msgid_plural = Encode::encode_utf8($msgid_plural); >+ >+ return _gettext( \&dngettext, [ $textdomain, $msgid, $msgid_plural, $count ], %vars ); >+} >+ >+sub __dpx { >+ my ( $textdomain, $msgctxt, $msgid, %vars ) = @_; >+ >+ $msgctxt = Encode::encode_utf8($msgctxt); >+ $msgid = Encode::encode_utf8($msgid); >+ >+ return _gettext( \&dpgettext, [ $textdomain, $msgctxt, $msgid ], %vars ); >+} >+ >+sub __dnpx { >+ my ( $textdomain, $msgctxt, $msgid, $msgid_plural, $count, %vars ) = @_; >+ >+ $msgctxt = Encode::encode_utf8($msgctxt); >+ $msgid = Encode::encode_utf8($msgid); >+ $msgid_plural = Encode::encode_utf8($msgid_plural); >+ >+ return _gettext( \&dnpgettext, [ $textdomain, $msgctxt, $msgid, $msgid_plural, $count ], %vars ); >+} >+ >+ > sub N__ { > return $_[0]; > } >@@ -184,6 +290,22 @@ sub N__np { > return $_[1]; > } > >+sub N__d { >+ return $_[1]; >+} >+ >+sub N__dn { >+ return $_[1]; >+} >+ >+sub N__dp { >+ return $_[2]; >+} >+ >+sub N__dnp { >+ return $_[2]; >+} >+ > sub _base_directory { > > # Directory structure is not the same for dev and standard installs >diff --git a/Koha/Plugins/Base.pm b/Koha/Plugins/Base.pm >index 75c628e53d..585ce96f8e 100644 >--- a/Koha/Plugins/Base.pm >+++ b/Koha/Plugins/Base.pm >@@ -273,6 +273,35 @@ sub bundle_path { > return $self->{_bundle_path}; > } > >+=head2 locale_dir >+ >+Returns the directory where locale files (.mo) are stored >+ >+=cut >+ >+sub locale_dir { >+ my ($self) = @_; >+ >+ return $self->mbf_exists('locale') ? $self->mbf_path('locale') : undef; >+} >+ >+=head2 locale_dir >+ >+Returns the locale textdomain for this plugin. The textdomain needs to be >+passed to functions __d, __dn, __dp, __dnp, __dx, __dnx, __dpx, __dnpx. >+It is also the name of the .mo file that should be present in locale_dir >+ >+By default it's the plugin name with '::' replaced by '-', so for a plugin >+named Koha::Plugin::Foo, the text domain will be Koha-Plugin-Foo >+ >+=cut >+ >+sub locale_textdomain { >+ my ($self) = @_; >+ >+ return ( ref $self ) =~ s/::/-/gr; >+} >+ > =head2 output > > $self->output( $data, $content_type[, $status[, $extra_options]]); >diff --git a/Koha/Template/Plugin/I18N.pm b/Koha/Template/Plugin/I18N.pm >index 9ba622ebcf..16c5f86807 100644 >--- a/Koha/Template/Plugin/I18N.pm >+++ b/Koha/Template/Plugin/I18N.pm >@@ -22,7 +22,7 @@ use Modern::Perl; > use base qw( Template::Plugin ); > > use C4::Context; >-use Koha::I18N qw( __ __n __np __npx __nx __p __px __x __xn ); >+use Koha::I18N qw( __ __n __np __npx __nx __p __px __x __xn __d __dn __dp __dnp __dx __dnx __dpx __dnpx); > > =head1 NAME > >@@ -42,6 +42,15 @@ Koha::Template::Plugin::I18N - Translate strings in templates > [% I18N.tnp('bibliographic material', "item", "items", count) %] > [% I18N.tnpx('bibliographic material', "{count} item", "{count} items", count, { count = count }) %] > >+ [%# For plugins %] >+ [% I18N.td("MyPlugin", "Hello!") %] >+ [% I18N.tdx("MyPlugin", "Hello {name}", { name = name }) %] >+ [% I18N.tdn("MyPlugin", "Hello friend", "Hello friends", count) %] >+ [% I18N.tdnx("MyPlugin", "Hello my {count} friend", "Hello my {count} friends", count, { count = count }) %] >+ [% I18N.tdp("MyPlugin", 'verb', 'Item') # to order %] >+ [% I18N.tdnp("MyPlugin", 'bibliographic material', "item", "items", count) %] >+ [% I18N.tdnpx("MyPlugin", 'bibliographic material', "{count} item", "{count} items", count, { count = count }) %] >+ > Do not use this plugin directly. Add the following directive > > [% PROCESS 'i18n.inc' %] >@@ -183,4 +192,108 @@ sub tnpx { > return __npx( $msgctxt, $msgid, $msgid_plural, $count, %$vars ); > } > >+=head2 td >+ >+ [% I18N.td("TextDomain", "hello") %] >+ >+Same as t, but with textdomain as first argument. Useful for plugins >+ >+=cut >+ >+sub td { >+ my ($self, $textdomain, $msgid) = @_; >+ return __d($textdomain, $msgid); >+} >+ >+=head2 tdn >+ >+ [% I18N.tdn("TextDomain", "item", "items", count) %] >+ >+Same as tn, but with textdomain as first argument. Useful for plugins >+ >+=cut >+ >+sub tdn { >+ my ( $self, $textdomain, $msgid, $msgid_plural, $count ) = @_; >+ return __dn( $textdomain, $msgid, $msgid_plural, $count ); >+} >+ >+=head2 tdp >+ >+ [% I18N.tdp("TextDomain", "context", "hello") %] >+ >+Same as tp, but with textdomain as first argument. Useful for plugins >+ >+=cut >+ >+sub tdp { >+ my ( $self, $textdomain, $msgctxt, $msgid ) = @_; >+ return __p( $textdomain, $msgctxt, $msgid ); >+} >+ >+=head2 tdnp >+ >+ [% I18N.tdnp("TextDomain", "context", "item", "items", count) %] >+ >+Same as tnp, but with textdomain as first argument. Useful for plugins >+ >+=cut >+ >+sub tdnp { >+ my ( $self, $textdomain, $msgctxt, $msgid, $msgid_plural, $count ) = @_; >+ return __np( $textdomain, $msgctxt, $msgid, $msgid_plural, $count ); >+} >+ >+=head2 tdx >+ >+ [% I18N.tdx("TextDomain", "hello {name}", { name = name }) %] >+ >+Same as tx, but with textdomain as first argument. Useful for plugins >+ >+=cut >+ >+sub tdx { >+ my ($self, $textdomain, $msgid, $vars) = @_; >+ return __dx($textdomain, $msgid, %$vars); >+} >+ >+=head2 tdnx >+ >+ [% I18N.tdnx("TextDomain", "{count} item", "{count} items", count, { count = count }) %] >+ >+Same as tnx, but with textdomain as first argument. Useful for plugins >+ >+=cut >+ >+sub tdnx { >+ my ( $self, $textdomain, $msgid, $msgid_plural, $count, $vars ) = @_; >+ return __dnx( $textdomain, $msgid, $msgid_plural, $count, %$vars ); >+} >+ >+=head2 tdpx >+ >+ [% I18N.tdpx("TextDomain", "context", "hello {name}", { name = name }) %] >+ >+Same as tpx, but with textdomain as first argument. Useful for plugins >+ >+=cut >+ >+sub tdpx { >+ my ( $self, $textdomain, $msgctxt, $msgid, $vars ) = @_; >+ return __p( $textdomain, $msgctxt, $msgid, %$vars ); >+} >+ >+=head2 tdnpx >+ >+ [% I18N.tdnpx("TextDomain", "context", "{count} item", "{count} items", count, { count = count }) %] >+ >+Same as tnpx, but with textdomain as first argument. Useful for plugins >+ >+=cut >+ >+sub tdnpx { >+ my ( $self, $textdomain, $msgctxt, $msgid, $msgid_plural, $count, $vars ) = @_; >+ return __npx( $textdomain, $msgctxt, $msgid, $msgid_plural, $count, %$vars ); >+} >+ > 1; >diff --git a/gulpfile.js b/gulpfile.js >index 46375e5838..b56ed7a8b4 100644 >--- a/gulpfile.js >+++ b/gulpfile.js >@@ -235,14 +235,6 @@ function po_extract_opac() { > .pipe(dest("misc/translator")); > } > >-const xgettext_options = >- "--from-code=UTF-8 --package-name Koha " + >- "--package-version= -k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 " + >- "-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ " + >- "-kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3 " + >- "-k -k$__ -k$__x -k$__n:1,2 -k$__nx:1,2 -k$__xn:1,2 " + >- "--force-po"; >- > function po_extract_messages_js() { > const globs = [ > "koha-tmpl/intranet-tmpl/prog/js/vue/**/*.vue", >@@ -253,7 +245,7 @@ function po_extract_messages_js() { > return src(globs, { read: false, nocase: true }) > .pipe( > xgettext( >- `xgettext -L JavaScript ${xgettext_options}`, >+ `misc/translator/xgettext-js`, > "Koha-messages-js.pot" > ) > ) >@@ -264,7 +256,7 @@ function po_extract_messages() { > const perlStream = src(["**/*.pl", "**/*.pm"], { > read: false, > nocase: true, >- }).pipe(xgettext(`xgettext -L Perl ${xgettext_options}`, "Koha-perl.pot")); >+ }).pipe(xgettext('misc/translator/xgettext-perl', "Koha-perl.pot")); > > const ttStream = src( > [ >diff --git a/misc/translator/xgettext-js b/misc/translator/xgettext-js >new file mode 100755 >index 0000000000..32e00cc3fe >--- /dev/null >+++ b/misc/translator/xgettext-js >@@ -0,0 +1,19 @@ >+#!/bin/sh >+ >+exec xgettext \ >+ -L JavaScript \ >+ --from-code=UTF-8 \ >+ --package-name Koha \ >+ --package-version= \ >+ --force-po \ >+ -k \ >+ -k'__' -k'__n:1,2' -k'__p:1c,2' -k'__np:1c,2,3' \ >+ -k'__x' -k'__nx:1,2' -k'__px:1c,2' -k'__npx:1c,2,3' \ >+ -k'__xn:1,2' \ >+ -k'__d:2' -k'__dn:2,3' -k'__dp:2c,3' -k'__dnp:2c,3,4' \ >+ -k'__dx:2' -k'__dnx:2,3' -k'__dpx:2c,3' -k'__dnpx:2c,3,4' \ >+ -k'$__' -k'$__n:1,2' -k'$__p:1c,2' -k'$__np:1c,2,3' \ >+ -k'$__d:2' -k'$__dn:2,3' -k'$__dp:2c,3' -k'$__dnp:2c,3,4' \ >+ -k'N__' -k'N__n:1,2' -k'N__p:1c,2' -k'N__np:1c,2,3' \ >+ -k'N__d:2' -k'N__dn:2,3' -k'N__dp:2c,3' -k'N__dnp:2c,3,4' \ >+ "$@" >diff --git a/misc/translator/xgettext-perl b/misc/translator/xgettext-perl >new file mode 100755 >index 0000000000..1b23af1cfc >--- /dev/null >+++ b/misc/translator/xgettext-perl >@@ -0,0 +1,17 @@ >+#!/bin/sh >+ >+exec xgettext \ >+ -L Perl \ >+ --from-code=UTF-8 \ >+ --package-name Koha \ >+ --package-version= \ >+ --force-po \ >+ -k \ >+ -k'__' -k'__n:1,2' -k'__p:1c,2' -k'__np:1c,2,3' \ >+ -k'__x' -k'__nx:1,2' -k'__px:1c,2' -k'__npx:1c,2,3' \ >+ -k'__xn:1,2' \ >+ -k'__d:2' -k'__dn:2,3' -k'__dp:2c,3' -k'__dnp:2c,3,4' \ >+ -k'__dx:2' -k'__dnx:2,3' -k'__dpx:2c,3' -k'__dnpx:2c,3,4' \ >+ -k'N__' -k'N__n:1,2' -k'N__p:1c,2' -k'N__np:1c,2,3' \ >+ -k'N__d:2' -k'N__dn:2,3' -k'N__dp:2c,3' -k'N__dnp:2c,3,4' \ >+ "$@" >diff --git a/misc/translator/xgettext-tt2 b/misc/translator/xgettext-tt2 >index 02bb61f4a1..4b73d24015 100755 >--- a/misc/translator/xgettext-tt2 >+++ b/misc/translator/xgettext-tt2 >@@ -37,6 +37,15 @@ sub defaultKeywords { > 'tpx:1c,2', > 'tnp:1c,2,3', > 'tnpx:1c,2,3', >+ 'dt:2', >+ 'dtx:2', >+ 'dtn:2,3', >+ 'dtnx:2,3', >+ 'dtxn:2,3', >+ 'dtp:2c,3', >+ 'dtpx:2c,3', >+ 'dtnp:2c,3,4', >+ 'dtnpx:2c,3,4', > ]; > } > >@@ -50,6 +59,14 @@ sub defaultFlags { > 'tpx:2:perl-brace-format', > 'tnpx:2:perl-brace-format', > 'tnpx:3:perl-brace-format', >+ 'dtx:2:perl-brace-format', >+ 'dtnx:2:perl-brace-format', >+ 'dtnx:3:perl-brace-format', >+ 'dtxn:2:perl-brace-format', >+ 'dtxn:3:perl-brace-format', >+ 'dtpx:3:perl-brace-format', >+ 'dtnpx:3:perl-brace-format', >+ 'dtnpx:4:perl-brace-format', > ], > } > >-- >2.39.5 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 39564
:
180642
|
180643