From 13f8343e39dfd2d6d1bd2c1b164827211091f8ce Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 1 Aug 2023 15:04:59 +0700 Subject: [PATCH 1/4] 13319 add documentation for internationalization --- docs/development/internationalization.md | 79 ++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 80 insertions(+) create mode 100644 docs/development/internationalization.md diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md new file mode 100644 index 00000000000..276fa974708 --- /dev/null +++ b/docs/development/internationalization.md @@ -0,0 +1,79 @@ +# Internationalization + +NetBox follows the [Django translation guide](https://docs.djangoproject.com/en/4.2/topics/i18n/translation/) to mark translatable strings. + +## General Guidance + +* In models, forms and tables wrap strings with gettext_lazy function. +* In templates wrap strings with the **{% trans %}** tag. + +!!! f-strings + Python f-strings are great, but do not work with internationalization. If a parameterized strings needs to be displayed (for example a help_string) it will need to use .format method instead of f-strings. + +## Models + +1. Import gettext_lazy. +2. Make sure all model fields have a verbose_name defined. +3. Wrap all verbose_name and help_text fields with the gettext_lazy shortcut. + +``` +from django.utils.translation import gettext_lazy as _ + +class Circuit(PrimaryModel): + commit_rate = models.PositiveIntegerField( + ... + verbose_name=_('commit rate (Kbps)'), + help_text=_("Committed rate") + ) + +``` +**Note:** The Django docs specifically state for internationalization: "It is recommended to always provide explicit verbose_name and verbose_name_plural options" + +## Forms + +1. Import gettext_lazy +2. Make sure all form-fields have a lable defined +3. Wrap all lable and fieldsets headers wtih the gettext_lazy shorcut + +``` +from django.utils.translation import gettext_lazy as _ + +class CircuitBulkEditForm(NetBoxModelBulkEditForm): + description = forms.CharField( + label=_('Description'), + ... + ) + + fieldsets = ( + (_('Circuit'), ('provider', 'type', 'status', 'description')), + ) + +``` + +## Tables + +1. Import gettext_lazy +2. Make sure all table-fields have a verbose_name defined + +``` +from django.utils.translation import gettext_lazy as _ + +class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): + provider = tables.Column( + verbose_name=_('Provider'), + ... + ) +``` + +## Templates + +1. Add **{% load i18n %}** at the top of the template files +2. Wrap displayable strings with the **trans** tag + +``` +{% load i18n %} +
{% trans "Circuit" %}
+``` + +!!! note + These just cover the most standard use cases, please read over the [Django translation guide](https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#standard-translation) for dealing with pluralization, model methods, time display and other specialized cases. diff --git a/mkdocs.yml b/mkdocs.yml index cde4a0acdb6..7ec7521dd6f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -272,6 +272,7 @@ nav: - Web UI: 'development/web-ui.md' - Release Checklist: 'development/release-checklist.md' - git Cheat Sheet: 'development/git-cheat-sheet.md' + - internationalization: 'development/internationalization.md' - Release Notes: - Summary: 'release-notes/index.md' - Version 3.6: 'release-notes/version-3.6.md' From 50885d72e381a991e48fd0746006ad37436a8b37 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 3 Aug 2023 20:14:50 +0700 Subject: [PATCH 2/4] 13319 add verbose name to model --- docs/development/internationalization.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md index 276fa974708..2cf2ef76d60 100644 --- a/docs/development/internationalization.md +++ b/docs/development/internationalization.md @@ -13,8 +13,9 @@ NetBox follows the [Django translation guide](https://docs.djangoproject.com/en/ ## Models 1. Import gettext_lazy. -2. Make sure all model fields have a verbose_name defined. -3. Wrap all verbose_name and help_text fields with the gettext_lazy shortcut. +2. Define both verbose_name and verbose_name_plural in the model Meta and wrap them strings with the gettext_lazy shortcut. +3. Make sure all model fields have a verbose_name defined. +4. Wrap all verbose_name and help_text fields with the gettext_lazy shortcut. ``` from django.utils.translation import gettext_lazy as _ @@ -26,6 +27,9 @@ class Circuit(PrimaryModel): help_text=_("Committed rate") ) + class Meta: + verbose_name = _('circuit') + verbose_name_plural = _('circuits') ``` **Note:** The Django docs specifically state for internationalization: "It is recommended to always provide explicit verbose_name and verbose_name_plural options" From a1b8ab96ac6a37c96af52cbfe60579e26e5ac209 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 3 Aug 2023 20:16:23 +0700 Subject: [PATCH 3/4] 13319 fix typo --- docs/development/internationalization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md index 2cf2ef76d60..df49aaa65ad 100644 --- a/docs/development/internationalization.md +++ b/docs/development/internationalization.md @@ -13,7 +13,7 @@ NetBox follows the [Django translation guide](https://docs.djangoproject.com/en/ ## Models 1. Import gettext_lazy. -2. Define both verbose_name and verbose_name_plural in the model Meta and wrap them strings with the gettext_lazy shortcut. +2. Define both verbose_name and verbose_name_plural in the model Meta and wrap the strings with the gettext_lazy shortcut. 3. Make sure all model fields have a verbose_name defined. 4. Wrap all verbose_name and help_text fields with the gettext_lazy shortcut. From 4c4a5ab1d3540cfe0bb4b6a454b96cd96b0d3fd9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 11 Aug 2023 10:59:54 -0400 Subject: [PATCH 4/4] Flesh out developer doc for i18n --- docs/development/internationalization.md | 88 +++++++++++++++++------- mkdocs.yml | 2 +- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md index df49aaa65ad..bdc7cbdaa8f 100644 --- a/docs/development/internationalization.md +++ b/docs/development/internationalization.md @@ -1,23 +1,49 @@ # Internationalization -NetBox follows the [Django translation guide](https://docs.djangoproject.com/en/4.2/topics/i18n/translation/) to mark translatable strings. +Beginning with NetBox v4.0, NetBox will leverage [Django's automatic translation](https://docs.djangoproject.com/en/stable/topics/i18n/translation/) to support languages other than English. This page details the areas of the project which require special attention to ensure functioning translation support. Briefly, these include: + +* The `verbose_name` and `verbose_name_plural` Meta attributes for each model +* The `verbose_name` and (if defined) `help_text` for each model field +* The `label` for each form field +* Headers for `fieldsets` on each form class +* The `verbose_name` for each table column +* All human-readable strings within templates must be wrapped with `{% trans %}` or `{% blocktrans %}` + +The rest of this document elaborates on each of the items above. ## General Guidance -* In models, forms and tables wrap strings with gettext_lazy function. -* In templates wrap strings with the **{% trans %}** tag. +* Wrap human-readable strings with Django's `gettext()` or `gettext_lazy()` utility functions to enable automatic translation. Generally, `gettext_lazy()` is preferred (and sometimes required) to defer translation until the string is displayed. + +* By convention, the preferred translation function is typically imported as an underscore (`_`) to minimize boilerplate code. Thus, you will often see translation as e.g. `_("Some text")`. It is still an option to import and use alternative translation functions (e.g. `pgettext()` and `ngettext()`) normally as needed. + +* Avoid passing markup and other non-natural language where possible. Everything wrapped by a translation function gets exported to a messages file for translation by a human. -!!! f-strings - Python f-strings are great, but do not work with internationalization. If a parameterized strings needs to be displayed (for example a help_string) it will need to use .format method instead of f-strings. +* Where the intended meaning of the translated string may not be obvious, use `pgettext()` or `pgettext_lazy()` to include assisting context for the translator. For example: + + ```python + # Context, string + pgettext("month name", "May") + ``` + +* **Format strings do not support translation.** Avoid "f" strings for messages that must support translation. Instead, use `format()` to accomplish variable replacement: + + ```python + # Translation will not work + f"There are {count} objects" + + # Do this instead + "There are {count} objects".format(count=count) + ``` ## Models -1. Import gettext_lazy. -2. Define both verbose_name and verbose_name_plural in the model Meta and wrap the strings with the gettext_lazy shortcut. -3. Make sure all model fields have a verbose_name defined. -4. Wrap all verbose_name and help_text fields with the gettext_lazy shortcut. +1. Import `gettext_lazy` as `_`. +2. Ensure both `verbose_name` and `verbose_name_plural` are defined under the model's `Meta` class and wrapped with the `gettext_lazy()` shortcut. +3. Ensure each model field specifies a `verbose_name` wrapped with `gettext_lazy()`. +4. Ensure any `help_text` attributes on model fields are also wrapped with `gettext_lazy()`. -``` +```python from django.utils.translation import gettext_lazy as _ class Circuit(PrimaryModel): @@ -31,15 +57,14 @@ class Circuit(PrimaryModel): verbose_name = _('circuit') verbose_name_plural = _('circuits') ``` -**Note:** The Django docs specifically state for internationalization: "It is recommended to always provide explicit verbose_name and verbose_name_plural options" ## Forms -1. Import gettext_lazy -2. Make sure all form-fields have a lable defined -3. Wrap all lable and fieldsets headers wtih the gettext_lazy shorcut +1. Import `gettext_lazy` as `_`. +2. All form fields must specify a `label` wrapped with `gettext_lazy()`. +3. All headers under a form's `fieldsets` property must be wrapped with `gettext_lazy()`. -``` +```python from django.utils.translation import gettext_lazy as _ class CircuitBulkEditForm(NetBoxModelBulkEditForm): @@ -51,15 +76,14 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): fieldsets = ( (_('Circuit'), ('provider', 'type', 'status', 'description')), ) - ``` ## Tables -1. Import gettext_lazy -2. Make sure all table-fields have a verbose_name defined +1. Import `gettext_lazy` as `_`. +2. All table columns must specify a `verbose_name` wrapped with `gettext_lazy()`. -``` +```python from django.utils.translation import gettext_lazy as _ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): @@ -71,13 +95,29 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): ## Templates -1. Add **{% load i18n %}** at the top of the template files -2. Wrap displayable strings with the **trans** tag +1. Ensure translation support is enabled by including `{% load i18n %}` at the top of the template. +2. Use the [`{% trans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#translate-template-tag) tag (short for "translate") to wrap short strings. +3. Longer strings may be enclosed between [`{% blocktrans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#blocktranslate-template-tag) and `{% endblocktrans %}` tags to improve readability and to enable variable replacement. +4. Avoid passing HTML within translated strings where possible, as this can complicate the work needed of human translators to develop message maps. ``` {% load i18n %} -
{% trans "Circuit" %}
+ +{# A short string #} +
{% trans "Circuit List" %}
+ +{# A longer string with a context variable #} +{% blocktrans with count=object.circuits.count %} + There are {count} circuits. Would you like to continue? +{% endblocktrans %} ``` -!!! note - These just cover the most standard use cases, please read over the [Django translation guide](https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#standard-translation) for dealing with pluralization, model methods, time display and other specialized cases. +!!! warning + The `{% blocktrans %}` tag supports only **limited variable replacement**, comparable to the `format()` method on Python strings. It does not permit access to object attributes or the use of other template tags or filters inside it. Ensure that any necessary context is passed as simple variables. + +!!! info + The `{% trans %}` and `{% blocktrans %}` support the inclusion of contextual hints for translators using the `context` argument: + + ```nohighlight + {% trans "May" context "month name" %} + ``` diff --git a/mkdocs.yml b/mkdocs.yml index 7ec7521dd6f..9bd88383cef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -270,9 +270,9 @@ nav: - Application Registry: 'development/application-registry.md' - User Preferences: 'development/user-preferences.md' - Web UI: 'development/web-ui.md' + - Internationalization: 'development/internationalization.md' - Release Checklist: 'development/release-checklist.md' - git Cheat Sheet: 'development/git-cheat-sheet.md' - - internationalization: 'development/internationalization.md' - Release Notes: - Summary: 'release-notes/index.md' - Version 3.6: 'release-notes/version-3.6.md'