diff --git a/docs/markdown/i18n-module.md b/docs/markdown/i18n-module.md
index c0fba48de..ba3eeb986 100644
--- a/docs/markdown/i18n-module.md
+++ b/docs/markdown/i18n-module.md
@@ -56,3 +56,15 @@ for normal keywords. In addition it accepts these keywords:
* `args`: (*Added 0.51.0*) list of extra arguments to pass to `msgfmt`
*Added 0.37.0*
+
+### i18n.itstool_join()
+
+This joins translations into a XML file using `itstool`. See
+[[@custom_tgt]]
+for normal keywords. In addition it accepts these keywords:
+
+* `its_files`: filenames of ITS files that should be used explicitly
+ (XML translation rules are autodetected otherwise).
+* `mo_targets` *required*: mo file generation targets as returned by `i18n.gettext()`.
+
+*Added 0.61.0*
diff --git a/docs/markdown/snippets/i18n-itstool_join-added.md b/docs/markdown/snippets/i18n-itstool_join-added.md
new file mode 100644
index 000000000..53e8d61c8
--- /dev/null
+++ b/docs/markdown/snippets/i18n-itstool_join-added.md
@@ -0,0 +1,5 @@
+## Added support for XML translations using itstool
+
+XML files can now be translated easier by using `itstool` via
+`i18n.itstool_join()`. This ensures the XML is translated correctly
+based on the defined ITS rules for the specific XML layout.
diff --git a/mesonbuild/modules/i18n.py b/mesonbuild/modules/i18n.py
index 379b766b7..d92e33419 100644
--- a/mesonbuild/modules/i18n.py
+++ b/mesonbuild/modules/i18n.py
@@ -59,6 +59,20 @@ if T.TYPE_CHECKING:
languages: T.List[str]
preset: T.Optional[str]
+ class ItsJoinFile(TypedDict):
+
+ input: T.List[T.Union[
+ str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex,
+ build.ExtractedObjects, build.GeneratedList, ExternalProgram,
+ mesonlib.File]]
+ output: T.List[str]
+ build_by_default: bool
+ install: bool
+ install_dir: T.List[T.Union[str, bool]]
+ install_tag: T.List[str]
+ its_files: T.List[str]
+ mo_targets: T.List[T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]]
+
_ARGS: KwargInfo[T.List[str]] = KwargInfo(
'args',
@@ -115,12 +129,17 @@ class I18nModule(ExtensionModule):
self.methods.update({
'merge_file': self.merge_file,
'gettext': self.gettext,
+ 'itstool_join': self.itstool_join,
})
@staticmethod
def nogettext_warning() -> None:
mlog.warning('Gettext not found, all translation targets will be ignored.', once=True)
+ @staticmethod
+ def noitstool_error() -> T.NoReturn:
+ raise mesonlib.MesonException('Did not find itstool. Please install it to continue.')
+
@staticmethod
def _get_data_dirs(state: 'ModuleState', dirs: T.Iterable[str]) -> T.List[str]:
"""Returns source directories of relative paths"""
@@ -269,5 +288,65 @@ class I18nModule(ExtensionModule):
return ModuleReturnValue([gmotargets, pottarget, updatepotarget], targets)
+ @FeatureNew('i18n.itstool_join', '0.61.0')
+ @noPosargs
+ @typed_kwargs(
+ 'i18n.itstool_join',
+ CT_BUILD_BY_DEFAULT,
+ CT_INPUT_KW,
+ CT_INSTALL_DIR_KW,
+ CT_INSTALL_TAG_KW,
+ CT_OUTPUT_KW,
+ INSTALL_KW,
+ _ARGS.evolve(),
+ KwargInfo('its_files', ContainerTypeInfo(list, str)),
+ KwargInfo('mo_targets', ContainerTypeInfo(list, build.CustomTarget), required=True),
+ )
+ def itstool_join(self, state: 'ModuleState', args: T.List['TYPE_var'], kwargs: 'ItsJoinFile') -> ModuleReturnValue:
+ if not shutil.which('itstool'):
+ self.noitstool_error()
+ mo_targets = kwargs['mo_targets']
+ its_files = kwargs.get('its_files', [])
+
+ mo_fnames = []
+ for target in mo_targets:
+ mo_fnames.append(path.join(target.get_subdir(), target.get_outputs()[0]))
+
+ command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget,
+ build.CustomTargetIndex, 'ExternalProgram', mesonlib.File]] = []
+ command.extend(state.environment.get_build_command())
+ command.extend([
+ '--internal', 'itstool', 'join',
+ '-i', '@INPUT@',
+ '-o', '@OUTPUT@'
+ ])
+ if its_files:
+ for fname in its_files:
+ if not path.isabs(fname):
+ fname = path.join(state.environment.source_dir, state.subdir, fname)
+ command.extend(['--its', fname])
+ command.extend(mo_fnames)
+
+ build_by_default = kwargs['build_by_default']
+ if build_by_default is None:
+ build_by_default = kwargs['install']
+
+ real_kwargs = {
+ 'build_by_default': build_by_default,
+ 'command': command,
+ 'depends': mo_targets,
+ 'install': kwargs['install'],
+ 'install_dir': kwargs['install_dir'],
+ 'output': kwargs['output'],
+ 'input': kwargs['input'],
+ 'install_tag': kwargs['install_tag'],
+ }
+
+ ct = build.CustomTarget('', state.subdir, state.subproject,
+ T.cast(T.Dict[str, T.Any], real_kwargs))
+
+ return ModuleReturnValue(ct, [ct])
+
+
def initialize(interp: 'Interpreter') -> I18nModule:
return I18nModule(interp)
diff --git a/mesonbuild/scripts/itstool.py b/mesonbuild/scripts/itstool.py
new file mode 100644
index 000000000..fa3b0fa19
--- /dev/null
+++ b/mesonbuild/scripts/itstool.py
@@ -0,0 +1,82 @@
+# Copyright 2016 The Meson development team
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import argparse
+import subprocess
+import tempfile
+import shutil
+import typing as T
+
+parser = argparse.ArgumentParser()
+parser.add_argument('command')
+parser.add_argument('--build-dir', default='')
+parser.add_argument('-i', '--input', default='')
+parser.add_argument('-o', '--output', default='')
+parser.add_argument('--its', action='append', default=[])
+parser.add_argument('mo_files', nargs='+')
+
+
+def run_join(build_dir: str, its_files: T.List[str], mo_files: T.List[str], in_fname: str, out_fname: str) -> int:
+ if not mo_files:
+ print('No mo files specified to use for translation.')
+ return 1
+
+ with tempfile.TemporaryDirectory(prefix=os.path.basename(in_fname), dir=build_dir) as tmp_dir:
+ # copy mo files to have the right names so itstool can infer their locale
+ locale_mo_files = []
+ for mo_file in mo_files:
+ if not os.path.exists(mo_file):
+ print('Could not find mo file {}'.format(mo_file))
+ return 1
+ if not mo_file.endswith('.mo'):
+ print('File is not a mo file: {}'.format(mo_file))
+ return 1
+ # determine locale of this mo file
+ parts = mo_file.partition('LC_MESSAGES')
+ if parts[0].endswith((os.sep, '/')):
+ locale = os.path.basename(parts[0][:-1])
+ else:
+ locale = os.path.basename(parts[0])
+ tmp_mo_fname = os.path.join(tmp_dir, locale + '.mo')
+ shutil.copy(mo_file, tmp_mo_fname)
+ locale_mo_files.append(tmp_mo_fname)
+
+ cmd = ['itstool']
+ if its_files:
+ for fname in its_files:
+ cmd.extend(['-i', fname])
+ cmd.extend(['-j', in_fname,
+ '-o', out_fname])
+ cmd.extend(locale_mo_files)
+
+ return subprocess.call(cmd)
+
+
+def run(args: T.List[str]) -> int:
+ options = parser.parse_args(args)
+ command = options.command
+ build_dir = os.environ.get('MESON_BUILD_ROOT', os.getcwd())
+ if options.build_dir:
+ build_dir = options.build_dir
+
+ if command == 'join':
+ return run_join(build_dir,
+ options.its,
+ options.mo_files,
+ options.input,
+ options.output)
+ else:
+ print('Unknown subcommand.')
+ return 1
diff --git a/test cases/frameworks/6 gettext/data3/com.mesonbuild.test.intlprog.metainfo.xml b/test cases/frameworks/6 gettext/data3/com.mesonbuild.test.intlprog.metainfo.xml
new file mode 100644
index 000000000..7fb4d1fc9
--- /dev/null
+++ b/test cases/frameworks/6 gettext/data3/com.mesonbuild.test.intlprog.metainfo.xml
@@ -0,0 +1,33 @@
+
+
+ Test Application
+
+ International greeting.
+
+ This is text
with embedded XML tags. Nice!
+
Dies ist Text
mit eingebetteten XML Tags. Toll!
text
with embedded XML tags. Nice!"
+msgstr "Dies ist Text
mit eingebetteten XML Tags. Toll!"
diff --git a/test cases/frameworks/6 gettext/po/intltest.pot b/test cases/frameworks/6 gettext/po/intltest.pot
index 2d0a4ccf7..c34e7f9ef 100644
--- a/test cases/frameworks/6 gettext/po/intltest.pot
+++ b/test cases/frameworks/6 gettext/po/intltest.pot
@@ -32,3 +32,7 @@ msgstr ""
#: data/test.desktop.in:5
msgid "Test Application"
msgstr ""
+
+#: data/com.mesonbuild.test.dummy.metainfo.xml:19
+msgid "This is text
with embedded XML tags. Nice!"
+msgstr ""
diff --git a/test cases/frameworks/6 gettext/po/meson.build b/test cases/frameworks/6 gettext/po/meson.build
index 86e02f1c6..5510e424b 100644
--- a/test cases/frameworks/6 gettext/po/meson.build
+++ b/test cases/frameworks/6 gettext/po/meson.build
@@ -1,3 +1,4 @@
langs = ['fi', 'de', 'ru']
-i18n.gettext('intltest', languages : langs)
+gettext_targets = i18n.gettext('intltest', languages : langs)
+mo_targets = gettext_targets[0]
diff --git a/test cases/frameworks/6 gettext/test.json b/test cases/frameworks/6 gettext/test.json
index df9743095..910fc1c74 100644
--- a/test cases/frameworks/6 gettext/test.json
+++ b/test cases/frameworks/6 gettext/test.json
@@ -11,7 +11,8 @@
{"type": "file", "file": "usr/share/applications/test3.desktop"},
{"type": "file", "file": "usr/share/applications/test4.desktop"},
{"type": "file", "file": "usr/share/applications/test5.desktop"},
- {"type": "file", "file": "usr/share/applications/test6.desktop"}
+ {"type": "file", "file": "usr/share/applications/test6.desktop"},
+ {"type": "file", "file": "usr/share/metainfo/com.mesonbuild.test.intlprog.metainfo.xml"}
],
"matrix": {
"options": {