diff --git a/docs/locale/es/config.md.po b/docs/locale/es/config.md.po index 7d4bcdb..5a78d62 100644 --- a/docs/locale/es/config.md.po +++ b/docs/locale/es/config.md.po @@ -224,3 +224,19 @@ msgstr "" "Especifica como cadena terminada con `%` como `55%` para porcentaje de " "mensajes totales o como entero como `76` para determinar el número mínimo de" " mensajes traducidos requeridos para incluir un idioma." + +msgid "" +"Exclude certain files from being translated, still creating copies of " +"original ones in target languages. Accepts relative paths to files from " +"`docs_dir` (documentation directory)." +msgstr "" +"Excluye ciertos archivos de ser traducidos, aún creando copias de los " +"archivos originales en los idiomas objetivo. Acepta rutas relativas a los " +"archivos desde el directorio `docs_dir` (directorio de documentación)." + +msgid "" +"This setting is useful if you want, for example, to exclude a changelog file" +" from being translated." +msgstr "" +"Esta configuración es útil si quieres, por ejemplo, excluir un archivo de " +"historial de cambios o changelog de ser traducido." diff --git a/docs/src/config.md b/docs/src/config.md index c969919..e546dc2 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -215,13 +215,23 @@ total messages or as an integer like `76` to determine the minimum number of translated messages required to include a language. -### **`ignore_extensions`** (*list*) +### **`exclude`** (*list[str]*) + +Exclude certain files from being translated, still creating copies of +original ones in target languages. Accepts relative paths to files from +`docs_dir` (documentation directory). + +This setting is useful if you want, for example, to exclude a changelog +file from being translated. + + +### **`ignore_extensions`** (*list[str]*) File extensions that are ignored from being added to site directory, defaults to `['.po', '.pot', '.mo']`. -### **`ignore_msgids`** (*list*) +### **`ignore_msgids`** (*list[str]*) You can ignore certain messages from being dumped into PO files adding them to this list. diff --git a/docs/src/useful-recipes.md b/docs/src/useful-recipes.md index db43f6a..03cebde 100644 --- a/docs/src/useful-recipes.md +++ b/docs/src/useful-recipes.md @@ -55,11 +55,11 @@ use them directly [as a command line interface][mdpo-cli] or through is ```yaml - repo: https://github.com/mondeja/mdpo - rev: v0.3.84 + rev: v0.3.85 hooks: - id: md2po2md files: ^README\.md - args: ['-l', 'es', '-l', 'fr', '-o', 'locale/{lang}'] + args: ['-l', 'es', 'fr', '-o', 'locale/{lang}'] ``` === "Directories tree" diff --git a/examples/exclude/docs/locale/es/do-not-translate.md.po b/examples/exclude/docs/locale/es/do-not-translate.md.po new file mode 100644 index 0000000..3e3b5de --- /dev/null +++ b/examples/exclude/docs/locale/es/do-not-translate.md.po @@ -0,0 +1,6 @@ +# +msgid "" +msgstr "" + +msgid "Not translated" +msgstr "No traducida" diff --git a/examples/exclude/docs/locale/es/index.md.po b/examples/exclude/docs/locale/es/index.md.po new file mode 100644 index 0000000..84f051b --- /dev/null +++ b/examples/exclude/docs/locale/es/index.md.po @@ -0,0 +1,12 @@ +# +msgid "" +msgstr "" + +msgid "Home" +msgstr "Inicio" + +msgid "Welcome to MkDocs" +msgstr "Bienvenido a Mkdocs" + +msgid "Some content" +msgstr "Algo de contenido" diff --git a/examples/exclude/docs/locale/fr/do-not-translate.md.po b/examples/exclude/docs/locale/fr/do-not-translate.md.po new file mode 100644 index 0000000..17105ac --- /dev/null +++ b/examples/exclude/docs/locale/fr/do-not-translate.md.po @@ -0,0 +1,6 @@ +# +msgid "" +msgstr "" + +msgid "Not translated" +msgstr "Non traduit" diff --git a/examples/exclude/docs/locale/fr/index.md.po b/examples/exclude/docs/locale/fr/index.md.po new file mode 100644 index 0000000..b31bec7 --- /dev/null +++ b/examples/exclude/docs/locale/fr/index.md.po @@ -0,0 +1,12 @@ +# +msgid "" +msgstr "" + +msgid "Home" +msgstr "Accueil" + +msgid "Welcome to MkDocs" +msgstr "Bienvenue sur mkdocs" + +msgid "Some content" +msgstr "Du contenu" diff --git a/examples/exclude/docs/src/changelog.md b/examples/exclude/docs/src/changelog.md new file mode 100644 index 0000000..2784c2a --- /dev/null +++ b/examples/exclude/docs/src/changelog.md @@ -0,0 +1,7 @@ +# Changelog + +## version - date + +- Feature A +- Feature B +- Bugfix A diff --git a/examples/exclude/docs/src/do-not-translate.md b/examples/exclude/docs/src/do-not-translate.md new file mode 100644 index 0000000..e6d478c --- /dev/null +++ b/examples/exclude/docs/src/do-not-translate.md @@ -0,0 +1,3 @@ + + +Some content that shouldn't be translated. diff --git a/examples/exclude/docs/src/index.md b/examples/exclude/docs/src/index.md new file mode 100644 index 0000000..6bc8680 --- /dev/null +++ b/examples/exclude/docs/src/index.md @@ -0,0 +1,3 @@ +# Welcome to MkDocs + +Some content diff --git a/examples/exclude/mkdocs.yml b/examples/exclude/mkdocs.yml new file mode 100644 index 0000000..0b4be4e --- /dev/null +++ b/examples/exclude/mkdocs.yml @@ -0,0 +1,20 @@ +site_name: mkdocs-mdpo-plugin Mkdocs theme example +site_url: https://mkdocs-mdpo.ga +docs_dir: docs/src + +nav: + - Home: index.md + - Changelog: changelog.md + - Not translated: do-not-translate.md + +plugins: + - search + - mdpo: + languages: + - en + - es + - fr + cross_language_search: false + locale_dir: ../locale + exclude: + - changelog.md diff --git a/mkdocs_mdpo_plugin/config.py b/mkdocs_mdpo_plugin/config.py index e3eca8f..f8e84da 100644 --- a/mkdocs_mdpo_plugin/config.py +++ b/mkdocs_mdpo_plugin/config.py @@ -21,6 +21,7 @@ ('ignore_msgids', Type(list, default=[])), ('cross_language_search', Type(bool, default=True)), ('min_translated_messages', Type((str, int), default=None)), + ('exclude', Type(list, default=[])), ) @@ -191,6 +192,23 @@ def _languages_required(): else: plugin.config['min_translated_messages'] = min_translated + # check that 'exclude' contains a valid list + exclude = plugin.config.get('exclude') or [] + if not isinstance(exclude, list): + raise ValidationError( + 'Expected mdpo\'s "exclude" setting to be a list, but found' + f' the value {str(exclude)} of type {type(exclude).__name__}', + ) + else: + for i, path in enumerate(exclude): + if not isinstance(path, str): + raise ValidationError( + f'Expected mdpo\'s setting "exclude[{i}]" value to' + f' be a string, but found the value {str(path)} of' + f' type {type(path).__name__}', + ) + plugin.config['exclude'] = exclude + # store reference in plugin to markdown_extensions for later usage plugin.extensions.markdown = markdown_extensions diff --git a/mkdocs_mdpo_plugin/plugin.py b/mkdocs_mdpo_plugin/plugin.py index e8d7425..5ef162f 100644 --- a/mkdocs_mdpo_plugin/plugin.py +++ b/mkdocs_mdpo_plugin/plugin.py @@ -207,9 +207,23 @@ def on_page_markdown(self, markdown, page, config, files): if hasattr(page.file, '_mdpo_language'): return - # navigation pages titles translations and new pages urls are stored - # in dictionaries by language, so we can translate the titles in their - # own PO files and then change the URLs (see `on_page_context` event) + # get minimum translation requirements + min_translated = self.config['min_translated_messages'] + + # check if the file is excluded to be translated + # + # the implementation here opts for create the file but + # not creating the PO for translations + # + # other option would be to skip the languages loop entirely, but + # this would not create the file for a language and the navigation + # will do cross language linking, which worsens the user experience + excluded_page = page.file.src_path in self.config['exclude'] + + # navigation pages titles translations and new pages urls are + # stored in dictionaries by language, so we can translate the + # titles in their own PO files and then change the URLs + # (see `on_page_context` event) if page.title not in self.translations.nav: # lang: [title, url] self.translations.nav[page.title] = {} @@ -229,102 +243,123 @@ def on_page_markdown(self, markdown, page, config, files): _mdpo_languages = {} # {lang: file} for language in self._non_default_languages(): - lang_docs_dir = self._language_dir(config['docs_dir'], language) + if not excluded_page: + # if the page has been excluded from being translated + lang_docs_dir = self._language_dir( + config['docs_dir'], + language, + ) - compendium_filepath = os.path.join( - lang_docs_dir, - '_compendium.po', - ) + compendium_filepath = os.path.join( + lang_docs_dir, + '_compendium.po', + ) - # create compendium if doesn't exists, load to memory - if language not in self.translations.compendium_files: - if not os.path.isfile(compendium_filepath): - compendium_pofile = polib.POFile() - compendium_pofile.save(compendium_filepath) - self.translations.compendium_files[language] = \ - compendium_filepath + # create compendium if doesn't exists, load to memory + if language not in self.translations.compendium_files: + if not os.path.isfile(compendium_filepath): + compendium_pofile = polib.POFile() + compendium_pofile.save(compendium_filepath) + self.translations.compendium_files[language] = \ + compendium_filepath - # intialize compendium messages cache - self.translations.compendium_msgstrs_tr[language] = [] - self.translations.compendium_msgids[language] = [] + # intialize compendium messages cache + self.translations.compendium_msgstrs_tr[language] = [] + self.translations.compendium_msgids[language] = [] - compendium_pofile = polib.pofile(compendium_filepath) + compendium_pofile = polib.pofile(compendium_filepath) - # create pofile of the page for each language - po_filepath = os.path.join( - lang_docs_dir, - f'{page.file.src_path}.po', - ) - os.makedirs( - os.path.abspath(os.path.dirname(po_filepath)), - exist_ok=True, - ) - if not os.path.isfile(po_filepath): - po = polib.POFile() - else: - po = polib.pofile(po_filepath) - - for entry in original_po: - if entry not in po: - po.append(entry) - - _translated_entries_msgids = [] - _translated_entries_msgstrs = [] - - # translate title - translated_page_title, _title_in_pofile = (None, False) - for entry in po: - if entry.msgid == page.title: - # matching title found - entry.obsolete = False - translated_page_title = entry.msgstr - _title_in_pofile = True + # create pofile of the page for each language + po_filepath = os.path.join( + lang_docs_dir, + f'{page.file.src_path}.po', + ) + os.makedirs( + os.path.abspath(os.path.dirname(po_filepath)), + exist_ok=True, + ) + if not os.path.isfile(po_filepath): + po = polib.POFile() + else: + po = polib.pofile(po_filepath) + + for entry in original_po: + if entry not in po: + po.append(entry) + + _translated_entries_msgids = [] + _translated_entries_msgstrs = [] + + # translate title + translated_page_title, _title_in_pofile = (None, False) + for entry in po: + if entry.msgid == page.title: + # matching title found + entry.obsolete = False + translated_page_title = entry.msgstr + _title_in_pofile = True + _translated_entries_msgids.append(page.title) + if entry.msgstr: + _translated_entries_msgstrs.append(page.title) + if not _title_in_pofile: + po.insert( + 0, + polib.POEntry( + msgid=page.title, + msgstr='', + ), + ) _translated_entries_msgids.append(page.title) - if entry.msgstr: - _translated_entries_msgstrs.append(page.title) - if not _title_in_pofile: - po.insert( - 0, - polib.POEntry( - msgid=page.title, - msgstr='', - ), + + # add temporally compendium entries to language pofiles + for entry in compendium_pofile: + if entry not in po and entry.msgstr: + po.append(entry) + po.save(po_filepath) + + # if a minimum number of translations are required to include + # the file, compute number of untranslated messages + if min_translated: + n_translated, n_total = po_messages_stats(str(po)) + if language not in self.translations.stats: + self.translations.stats[language] = { + 'total': n_total, + 'translated': n_translated, + } + else: + self.translations.stats[language][ + 'total' + ] += n_total + self.translations.stats[language][ + 'translated' + ] += n_translated + + # translate part of the markdown producing a translated file + # content (the rest of the translations are handled by + # extensions, see `extension` module) + po2md = Po2Md( + [po_filepath, compendium_filepath], + events=po2md_events, + wrapwidth=math.inf, # ignore line wrapping ) - _translated_entries_msgids.append(page.title) + content = po2md.translate(markdown) - # add temporally compendium entries to language pofiles - for entry in compendium_pofile: - if entry not in po and entry.msgstr: - po.append(entry) - po.save(po_filepath) + _disabled_msgids = [ + entry.msgid for entry in po2md.disabled_entries + ] + _disabled_msgids.extend(self.config['ignore_msgids']) - # if a minimum number of translations are required to include - # the file, compute number of untranslated messages - min_translated = self.config['min_translated_messages'] - if min_translated: - n_translated, n_total = po_messages_stats(str(po)) - if language not in self.translations.stats: - self.translations.stats[language] = { - 'total': n_total, - 'translated': n_translated, - } - else: - self.translations.stats[language][ - 'total' - ] += n_total - self.translations.stats[language][ - 'translated' - ] += n_translated - - # translate part of the markdown producing a translated file - # content (the rest of the translations are handled by extensions, - # see `extension` module) - po2md = Po2Md( - [po_filepath, compendium_filepath], - events=po2md_events, - wrapwidth=math.inf, # generated file so ignore line wrapping - ) - content = po2md.translate(markdown) + for entry in po2md.translated_entries: + _translated_entries_msgstrs.append(entry.msgstr) + _translated_entries_msgids.append(entry.msgid) + else: + # mock variables if the file is excluded from being translated + content = markdown + translated_page_title = None + _disabled_msgids = [] + _translated_entries_msgstrs = [] + _translated_entries_msgids = [] + po, po_filepath = [], None temp_abs_path = self.translations.files[ page.file.src_path @@ -345,23 +380,16 @@ def on_page_markdown(self, markdown, page, config, files): self.translations.tempdir.name, ) new_file._mdpo_language = language + + new_page_title = translated_page_title or page.title new_page = mkdocs.structure.pages.Page( - translated_page_title, + new_page_title, new_file, config, ) files.append(new_file) _mdpo_languages[language] = new_file - _disabled_msgids = [ - entry.msgid for entry in po2md.disabled_entries - ] - _disabled_msgids.extend(self.config['ignore_msgids']) - - for entry in po2md.translated_entries: - _translated_entries_msgstrs.append(entry.msgstr) - _translated_entries_msgids.append(entry.msgid) - # create translation object translation = Translation( language, @@ -380,8 +408,11 @@ def on_page_markdown(self, markdown, page, config, files): url = removesuffix(url, 'index.html') new_page.file.url = url + # the title of the page will be 'page.title' (the original) + # if the file is being excluded from translations using the + # 'exclude' plugin's config setting self.translations.nav[page.title][language] = [ - translated_page_title, new_page.file.url, + new_page_title, new_page.file.url, ] mkdocs.commands.build._populate_page( @@ -500,10 +531,13 @@ def on_post_build(self, config): ) search_patcher.patch_site_dir() - # save PO files + # save PO files for not excluded pages for translations in self.translations.all.values(): for translation in translations: - translation.po.save(translation.po_filepath) + # po_filepath is None if the file has been excluded from + # translations using 'exclude' config setting + if translation.po_filepath is not None: + translation.po.save(translation.po_filepath) # dump repeated msgids from language files to compendium and # remove them from language files @@ -555,11 +589,14 @@ def on_post_build(self, config): # mark not found msgstrs as obsolete for translation in translations: - po = polib.pofile(translation.po_filepath) - for entry in po: - if entry.msgid not in translation.translated_msgids: - entry.obsolete = True - po.save(translation.po_filepath) + # po_filepath is None if the file has been excluded from + # translations using 'exclude' config setting + if translation.po_filepath is not None: + po = polib.pofile(translation.po_filepath) + for entry in po: + if entry.msgid not in translation.translated_msgids: + entry.obsolete = True + po.save(translation.po_filepath) # remove empty compendium files for compendium_filepath in self.translations.compendium_files.values(): diff --git a/setup.cfg b/setup.cfg index 323688b..26fcdb3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ classifiers = [options] packages = mkdocs_mdpo_plugin install_requires = - mdpo==0.3.84 + mdpo==0.3.85 python_requires = >=3.6 include_package_data = True diff --git a/tests/test_exclude.py b/tests/test_exclude.py new file mode 100644 index 0000000..5dd7d80 --- /dev/null +++ b/tests/test_exclude.py @@ -0,0 +1,134 @@ +"""Tests for "exclude" configuration setting.""" + +import os + +import pytest + + +TESTS = ( + pytest.param( + { + 'index.md': ( + 'Hello\n\nBye' + ), + 'changelog.md': ( + 'Some changes\n\nIn the changelog' + ), + }, + { + 'es/index.md.po': { + 'Hello': 'Hola', + 'Bye': 'Adiós', + }, + }, + { + 'languages': ['en', 'es'], + 'exclude': ['changelog.md'], + }, + {}, + { + 'es/index.html': [ + '

Hola

', + '

Adiós

', + ], + 'es/changelog/index.html': [ + '

Some changes

', + '

In the changelog

', + ], + }, + ['es/changelog.md.po'], + id='languages=[en,es]-exclude=[changelog.md]', + ), + pytest.param( + { + 'index.md': ( + 'Hello\n\nBye' + ), + 'changelog1.md': ( + 'Some changes 1\n\nIn the changelog 1' + ), + 'changelog2.md': ( + 'Some changes 2\n\nIn the changelog 2' + ), + }, + { + 'es/index.md.po': { + 'Hello': 'Hola', + 'Bye': 'Adiós', + }, + 'fr/index.md.po': { + 'Hello': 'Salut', + 'Bye': 'Adieu', + }, + }, + { + 'languages': ['en', 'es', 'fr'], + 'exclude': ['changelog1.md', 'changelog2.md'], + }, + {}, + { + 'es/index.html': [ + '

Hola

', + '

Adiós

', + ], + 'es/changelog1/index.html': [ + '

Some changes 1

', + '

In the changelog 1

', + ], + 'fr/changelog1/index.html': [ + '

Some changes 1

', + '

In the changelog 1

', + ], + 'es/changelog2/index.html': [ + '

Some changes 2

', + '

In the changelog 2

', + ], + 'fr/changelog2/index.html': [ + '

Some changes 2

', + '

In the changelog 2

', + ], + }, + [ + 'es/changelog1.md.po', + 'es/changelog2.md.po', + 'fr/changelog1.md.po', + 'fr/changelog2.md.po', + ], + id='languages=[en,es,fr]-exclude=[changelog1.md,changelog2.md]', + ), +) + + +@pytest.mark.parametrize( + ( + 'input_files_contents', + 'translations', + 'plugin_config', + 'additional_config', + 'expected_output_files', + 'unexistent_files', + ), + TESTS, +) +def test_exclude( + input_files_contents, + translations, + plugin_config, + additional_config, + expected_output_files, + unexistent_files, + mkdocs_build, +): + def check_po_translation_files_not_exists(context): + for unexistent_file in unexistent_files: + fpath = os.path.join(context['docs_dir'], unexistent_file) + assert not os.path.exists(fpath) + + mkdocs_build( + input_files_contents, + translations, + plugin_config, + additional_config, + expected_output_files, + callback_after_first_build=check_po_translation_files_not_exists, + ) diff --git a/tests/test_min_translated_messages.py b/tests/test_min_translated_messages.py index 44ca5ec..d180743 100644 --- a/tests/test_min_translated_messages.py +++ b/tests/test_min_translated_messages.py @@ -87,7 +87,7 @@ ), TESTS, ) -def test_navigation_and_page_building_plugins( +def test_min_translated_messages( input_files_contents, translations, plugin_config, diff --git a/tests/test_plugin_config.py b/tests/test_plugin_config.py index d114d40..4039d2f 100644 --- a/tests/test_plugin_config.py +++ b/tests/test_plugin_config.py @@ -482,6 +482,41 @@ def test_plugin_config( None, id='min_translated_messages=45', ), + pytest.param( + { + 'exclude': 45, + 'languages': [ + 'en', + 'es', + ], + }, + {}, + mkdocs.config.base.ValidationError, + ( + 'Expected mdpo\'s "exclude" setting to be a list,' + ' but found the value 45 of type int' + ), + id='exclude=', + ), + pytest.param( + { + 'exclude': [ + 'string', + 45, + ], + 'languages': [ + 'en', + 'es', + ], + }, + {}, + mkdocs.config.base.ValidationError, + ( + 'Expected mdpo\'s setting "exclude[1]" value to be' + ' a string, but found the value 45 of type int' + ), + id='exclude=[,]', + ), ), ) def test_plugin_config_errors(