diff options
author | Matthias Klumpp <matthias@tenstral.net> | 2021-12-18 04:26:46 +0100 |
---|---|---|
committer | Matthias Klumpp <matthias@tenstral.net> | 2022-01-21 22:26:17 +0100 |
commit | 02fb0c3f8bb60d88998c8a8c7d090ecc864ed04c (patch) | |
tree | d94965290f00c5d1a4173c274ca9575328a43624 | |
parent | e60d358e0482edc56b6441aaa3021d83dd14b527 (diff) | |
download | meson-02fb0c3f8bb60d88998c8a8c7d090ecc864ed04c.zip meson-02fb0c3f8bb60d88998c8a8c7d090ecc864ed04c.tar.gz meson-02fb0c3f8bb60d88998c8a8c7d090ecc864ed04c.tar.bz2 |
i18n: Add support for joining XML localization via itstool
-rw-r--r-- | docs/markdown/i18n-module.md | 12 | ||||
-rw-r--r-- | docs/markdown/snippets/i18n-itstool_join-added.md | 5 | ||||
-rw-r--r-- | mesonbuild/modules/i18n.py | 79 | ||||
-rw-r--r-- | mesonbuild/scripts/itstool.py | 82 | ||||
-rw-r--r-- | test cases/frameworks/6 gettext/data3/com.mesonbuild.test.intlprog.metainfo.xml | 33 | ||||
-rw-r--r-- | test cases/frameworks/6 gettext/data3/meson.build | 33 | ||||
-rw-r--r-- | test cases/frameworks/6 gettext/data3/metainfo.its | 33 | ||||
-rwxr-xr-x | test cases/frameworks/6 gettext/data3/verify.py | 13 | ||||
-rw-r--r-- | test cases/frameworks/6 gettext/meson.build | 3 | ||||
-rw-r--r-- | test cases/frameworks/6 gettext/po/de.po | 4 | ||||
-rw-r--r-- | test cases/frameworks/6 gettext/po/intltest.pot | 4 | ||||
-rw-r--r-- | test cases/frameworks/6 gettext/po/meson.build | 3 | ||||
-rw-r--r-- | test cases/frameworks/6 gettext/test.json | 3 |
13 files changed, 305 insertions, 2 deletions
diff --git a/docs/markdown/i18n-module.md b/docs/markdown/i18n-module.md index c0fba48..ba3eeb9 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 0000000..53e8d61 --- /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 379b766..d92e334 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,6 +129,7 @@ class I18nModule(ExtensionModule): self.methods.update({ 'merge_file': self.merge_file, 'gettext': self.gettext, + 'itstool_join': self.itstool_join, }) @staticmethod @@ -122,6 +137,10 @@ class I18nModule(ExtensionModule): 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""" src_dir = path.join(state.environment.get_source_dir(), state.subdir) @@ -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 0000000..fa3b0fa --- /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 0000000..7fb4d1f --- /dev/null +++ b/test cases/frameworks/6 gettext/data3/com.mesonbuild.test.intlprog.metainfo.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<component type="console-application"> + <id>com.mesonbuild.test.intlprog</id> + + <name>Test</name> + <summary>Application</summary> + + <metadata_license>FSFAP</metadata_license> + <project_license>FSFAP</project_license> + + <description> + <p> + Test Application + </p> + <p> + International greeting. + </p> + <p> + This is <code>text</code> with <em>embedded XML tags</em>. Nice! + </p> + </description> + + <icon type="stock">meson-unittest-intlprog</icon> + + <categories> + <category>Development</category> + <category>Building</category> + </categories> + + <provides> + <binary>intlprog</binary> + </provides> +</component> diff --git a/test cases/frameworks/6 gettext/data3/meson.build b/test cases/frameworks/6 gettext/data3/meson.build new file mode 100644 index 0000000..2be2e89 --- /dev/null +++ b/test cases/frameworks/6 gettext/data3/meson.build @@ -0,0 +1,33 @@ + +if itstool.found() + + mi_translated = i18n.itstool_join( + input: 'com.mesonbuild.test.intlprog.metainfo.xml', + output: 'com.mesonbuild.test.intlprog.metainfo.xml', + mo_targets: mo_targets, + its_files: ['metainfo.its'], + install: true, + install_dir: get_option('datadir') / 'metainfo', + ) + + # older versions of itstool have a bug where ITS rules specified on the command-line + # are not read when joining files. Since we don't install appstream in the Meson CI + # environment, the to-be-tested entry will be untranslated and the test would fail, so + # we just skip verification if the installed itstool is too old. + r = run_command(itstool, '-v', check: true) + itstool_v = r.stdout().strip().split() + if itstool_v[1].version_compare('>=2.0.6') + verify_exe = find_program('verify.py') + test('test xml translation', + verify_exe, + args: [mi_translated, + '<p xml:lang="de">Dies ist <code>Text</code> mit <em>eingebetteten XML Tags</em>. Toll!</p>'] + ) + else + message('Skipping translation verification: Itstool too old.') + endif + +else + install_data('com.mesonbuild.test.intlprog.metainfo.xml', + install_dir: get_option('datadir') / 'metainfo') +endif diff --git a/test cases/frameworks/6 gettext/data3/metainfo.its b/test cases/frameworks/6 gettext/data3/metainfo.its new file mode 100644 index 0000000..0852a0f --- /dev/null +++ b/test cases/frameworks/6 gettext/data3/metainfo.its @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<!-- + Copyright (C) 2015-2021 Matthias Klumpp <matthias@tenstral.net> + Copyright (C) 2019 Takao Fujiwara <takao.fujiwara1@gmail.com> + SPDX-License-Identifier: FSFAP +--> +<its:rules xmlns:its="http://www.w3.org/2005/11/its" + version="2.0"> + + <its:withinTextRule withinText="yes" selector="/component//description//em | + /component//description//code"/> + + <its:translateRule selector="/component" translate="no"/> + <its:translateRule selector="/component/name | + /component/summary | + /component/description | + /component/developer_name | + /component/name_variant_suffix | + /component/screenshots/screenshot/caption | + /component/releases/release/description | + /component/agreement/agreement_section/name | + /component/agreement/agreement_section/description" + translate="yes"/> + + <its:translateRule selector="/component/name[@translatable = 'no']" + translate="no"/> + <its:translateRule selector="/component/developer_name[@translatable = 'no']" + translate="no"/> + <its:translateRule selector="/component/name_variant_suffix[@translatable = 'no']" + translate="no"/> + <its:translateRule selector="/component/releases/release/description[@translatable = 'no']" + translate="no"/> +</its:rules> diff --git a/test cases/frameworks/6 gettext/data3/verify.py b/test cases/frameworks/6 gettext/data3/verify.py new file mode 100755 index 0000000..aff2f2e --- /dev/null +++ b/test cases/frameworks/6 gettext/data3/verify.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import os +import sys + +assert len(sys.argv) == 3 + +fname = sys.argv[1] +check_str = sys.argv[2] + +assert os.path.isfile(fname) +with open(fname, 'r', encoding='utf-8') as f: + assert check_str in f.read() diff --git a/test cases/frameworks/6 gettext/meson.build b/test cases/frameworks/6 gettext/meson.build index 2640ab2..ce99242 100644 --- a/test cases/frameworks/6 gettext/meson.build +++ b/test cases/frameworks/6 gettext/meson.build @@ -15,10 +15,13 @@ if not intl.found() error('MESON_SKIP_TEST libintl/gettext functions not found.') endif +itstool = find_program('itstool', required: false) + i18n = import('i18n') subdir('po') subdir('src') subdir('data') subdir('data2') +subdir('data3') subdir('generated') diff --git a/test cases/frameworks/6 gettext/po/de.po b/test cases/frameworks/6 gettext/po/de.po index 8d9d6e7..1e4693b 100644 --- a/test cases/frameworks/6 gettext/po/de.po +++ b/test cases/frameworks/6 gettext/po/de.po @@ -20,3 +20,7 @@ msgstr "" #: src/intlmain.c:15 msgid "International greeting." msgstr "Internationale Gruss." + +#: data/com.mesonbuild.test.dummy.metainfo.xml:19 +msgid "This is <code>text</code> with <em>embedded XML tags</em>. Nice!" +msgstr "Dies ist <code>Text</code> mit <em>eingebetteten XML Tags</em>. Toll!" diff --git a/test cases/frameworks/6 gettext/po/intltest.pot b/test cases/frameworks/6 gettext/po/intltest.pot index 2d0a4cc..c34e7f9 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 <code>text</code> with <em>embedded XML tags</em>. Nice!" +msgstr "" diff --git a/test cases/frameworks/6 gettext/po/meson.build b/test cases/frameworks/6 gettext/po/meson.build index 86e02f1..5510e42 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 df97430..910fc1c 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": { |