diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md index df49aaa65..bdc7cbdaa 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. -!!! 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. +* 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. + +* 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 %} -