Просмотр исходного кода

10300 initial translation support use gettext

Arthur 3 лет назад
Родитель
Сommit
6eba5d4d96
67 измененных файлов с 1176 добавлено и 1118 удалено
  1. 26 25
      netbox/circuits/filtersets.py
  2. 6 6
      netbox/circuits/forms/bulk_edit.py
  3. 7 6
      netbox/circuits/forms/bulk_import.py
  4. 6 6
      netbox/circuits/forms/model_forms.py
  5. 2 1
      netbox/circuits/models/circuits.py
  6. 4 3
      netbox/dcim/api/serializers.py
  7. 150 149
      netbox/dcim/filtersets.py
  8. 3 2
      netbox/dcim/forms/bulk_create.py
  9. 24 24
      netbox/dcim/forms/bulk_edit.py
  10. 89 88
      netbox/dcim/forms/bulk_import.py
  11. 3 2
      netbox/dcim/forms/common.py
  12. 12 11
      netbox/dcim/forms/connections.py
  13. 39 39
      netbox/dcim/forms/model_forms.py
  14. 11 10
      netbox/dcim/forms/object_create.py
  15. 3 2
      netbox/dcim/forms/object_import.py
  16. 7 6
      netbox/dcim/models/device_component_templates.py
  17. 18 17
      netbox/dcim/models/device_components.py
  18. 13 12
      netbox/dcim/models/devices.py
  19. 2 1
      netbox/dcim/models/power.py
  20. 11 10
      netbox/dcim/models/racks.py
  21. 4 3
      netbox/dcim/models/sites.py
  22. 43 42
      netbox/extras/filtersets.py
  23. 6 5
      netbox/extras/forms/bulk_edit.py
  24. 11 10
      netbox/extras/forms/bulk_import.py
  25. 2 2
      netbox/extras/forms/filtersets.py
  26. 2 1
      netbox/extras/forms/mixins.py
  27. 8 7
      netbox/extras/forms/model_forms.py
  28. 3 2
      netbox/extras/forms/reports.py
  29. 5 4
      netbox/extras/forms/scripts.py
  30. 23 22
      netbox/extras/models/customfields.py
  31. 37 36
      netbox/extras/models/models.py
  32. 2 1
      netbox/extras/tests/dummy_plugin/navigation.py
  33. 96 95
      netbox/ipam/filtersets.py
  34. 2 1
      netbox/ipam/forms/bulk_create.py
  35. 15 14
      netbox/ipam/forms/bulk_edit.py
  36. 43 42
      netbox/ipam/forms/bulk_import.py
  37. 2 2
      netbox/ipam/forms/filtersets.py
  38. 53 52
      netbox/ipam/forms/model_forms.py
  39. 17 16
      netbox/ipam/models/ip.py
  40. 3 2
      netbox/ipam/models/vlans.py
  41. 4 3
      netbox/ipam/models/vrfs.py
  42. 47 46
      netbox/netbox/config/parameters.py
  43. 2 1
      netbox/netbox/filtersets.py
  44. 2 2
      netbox/netbox/forms/__init__.py
  45. 2 1
      netbox/netbox/forms/base.py
  46. 116 115
      netbox/netbox/navigation/menu.py
  47. 7 6
      netbox/netbox/preferences.py
  48. 19 18
      netbox/tenancy/filtersets.py
  49. 5 4
      netbox/tenancy/forms/bulk_import.py
  50. 6 5
      netbox/users/admin/forms.py
  51. 13 12
      netbox/users/filtersets.py
  52. 5 4
      netbox/users/forms.py
  53. 6 5
      netbox/users/models.py
  54. 4 3
      netbox/utilities/forms/fields/csv.py
  55. 3 2
      netbox/utilities/forms/fields/expandable.py
  56. 3 2
      netbox/utilities/forms/fields/fields.py
  57. 7 6
      netbox/utilities/forms/forms.py
  58. 40 39
      netbox/virtualization/filtersets.py
  59. 2 1
      netbox/virtualization/forms/bulk_create.py
  60. 11 10
      netbox/virtualization/forms/bulk_edit.py
  61. 17 16
      netbox/virtualization/forms/bulk_import.py
  62. 2 2
      netbox/virtualization/forms/filtersets.py
  63. 10 9
      netbox/virtualization/forms/model_forms.py
  64. 6 5
      netbox/wireless/forms/bulk_edit.py
  65. 11 10
      netbox/wireless/forms/bulk_import.py
  66. 2 2
      netbox/wireless/forms/filtersets.py
  67. 11 10
      netbox/wireless/forms/model_forms.py

+ 26 - 25
netbox/circuits/filtersets.py

@@ -1,5 +1,6 @@
 import django_filters
 from django.db.models import Q
+from django.utils.translation import gettext as _
 
 from dcim.filtersets import CabledObjectFilterSet
 from dcim.models import Region, Site, SiteGroup
@@ -24,43 +25,43 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
         queryset=Region.objects.all(),
         field_name='circuits__terminations__site__region',
         lookup_expr='in',
-        label='Region (ID)',
+        label=_('Region (ID)'),
     )
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='circuits__terminations__site__region',
         lookup_expr='in',
         to_field_name='slug',
-        label='Region (slug)',
+        label=_('Region (slug)'),
     )
     site_group_id = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='circuits__terminations__site__group',
         lookup_expr='in',
-        label='Site group (ID)',
+        label=_('Site group (ID)'),
     )
     site_group = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='circuits__terminations__site__group',
         lookup_expr='in',
         to_field_name='slug',
-        label='Site group (slug)',
+        label=_('Site group (slug)'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         field_name='circuits__terminations__site',
         queryset=Site.objects.all(),
-        label='Site',
+        label=_('Site'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='circuits__terminations__site__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
     asn_id = django_filters.ModelMultipleChoiceFilter(
         field_name='asns',
         queryset=ASN.objects.all(),
-        label='ASN (ID)',
+        label=_('ASN (ID)'),
     )
 
     class Meta:
@@ -80,13 +81,13 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
 class ProviderNetworkFilterSet(NetBoxModelFilterSet):
     provider_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Provider.objects.all(),
-        label='Provider (ID)',
+        label=_('Provider (ID)'),
     )
     provider = django_filters.ModelMultipleChoiceFilter(
         field_name='provider__slug',
         queryset=Provider.objects.all(),
         to_field_name='slug',
-        label='Provider (slug)',
+        label=_('Provider (slug)'),
     )
 
     class Meta:
@@ -114,28 +115,28 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
 class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
     provider_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Provider.objects.all(),
-        label='Provider (ID)',
+        label=_('Provider (ID)'),
     )
     provider = django_filters.ModelMultipleChoiceFilter(
         field_name='provider__slug',
         queryset=Provider.objects.all(),
         to_field_name='slug',
-        label='Provider (slug)',
+        label=_('Provider (slug)'),
     )
     provider_network_id = django_filters.ModelMultipleChoiceFilter(
         field_name='terminations__provider_network',
         queryset=ProviderNetwork.objects.all(),
-        label='ProviderNetwork (ID)',
+        label=_('ProviderNetwork (ID)'),
     )
     type_id = django_filters.ModelMultipleChoiceFilter(
         queryset=CircuitType.objects.all(),
-        label='Circuit type (ID)',
+        label=_('Circuit type (ID)'),
     )
     type = django_filters.ModelMultipleChoiceFilter(
         field_name='type__slug',
         queryset=CircuitType.objects.all(),
         to_field_name='slug',
-        label='Circuit type (slug)',
+        label=_('Circuit type (slug)'),
     )
     status = django_filters.MultipleChoiceFilter(
         choices=CircuitStatusChoices,
@@ -145,38 +146,38 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
         queryset=Region.objects.all(),
         field_name='terminations__site__region',
         lookup_expr='in',
-        label='Region (ID)',
+        label=_('Region (ID)'),
     )
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='terminations__site__region',
         lookup_expr='in',
         to_field_name='slug',
-        label='Region (slug)',
+        label=_('Region (slug)'),
     )
     site_group_id = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='terminations__site__group',
         lookup_expr='in',
-        label='Site group (ID)',
+        label=_('Site group (ID)'),
     )
     site_group = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='terminations__site__group',
         lookup_expr='in',
         to_field_name='slug',
-        label='Site group (slug)',
+        label=_('Site group (slug)'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         field_name='terminations__site',
         queryset=Site.objects.all(),
-        label='Site (ID)',
+        label=_('Site (ID)'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='terminations__site__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
 
     class Meta:
@@ -199,25 +200,25 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
 class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     circuit_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Circuit.objects.all(),
-        label='Circuit',
+        label=_('Circuit'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
-        label='Site (ID)',
+        label=_('Site (ID)'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='site__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
     provider_network_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ProviderNetwork.objects.all(),
-        label='ProviderNetwork (ID)',
+        label=_('ProviderNetwork (ID)'),
     )
 
     class Meta:

+ 6 - 6
netbox/circuits/forms/bulk_edit.py

@@ -28,7 +28,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
     account = forms.CharField(
         max_length=30,
         required=False,
-        label='Account number'
+        label=_('Account number')
     )
     description = forms.CharField(
         max_length=200,
@@ -36,7 +36,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
     )
     comments = CommentField(
         widget=SmallTextarea,
-        label='Comments'
+        label=_('Comments')
     )
 
     model = Provider
@@ -56,7 +56,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
     service_id = forms.CharField(
         max_length=100,
         required=False,
-        label='Service ID'
+        label=_('Service ID')
     )
     description = forms.CharField(
         max_length=200,
@@ -64,7 +64,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
     )
     comments = CommentField(
         widget=SmallTextarea,
-        label='Comments'
+        label=_('Comments')
     )
 
     model = ProviderNetwork
@@ -118,7 +118,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
     )
     commit_rate = forms.IntegerField(
         required=False,
-        label='Commit rate (Kbps)'
+        label=_('Commit rate (Kbps)')
     )
     description = forms.CharField(
         max_length=100,
@@ -126,7 +126,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
     )
     comments = CommentField(
         widget=SmallTextarea,
-        label='Comments'
+        label=_('Comments')
     )
 
     model = Circuit

+ 7 - 6
netbox/circuits/forms/bulk_import.py

@@ -1,5 +1,6 @@
 from circuits.choices import CircuitStatusChoices
 from circuits.models import *
+from django.utils.translation import gettext as _
 from netbox.forms import NetBoxModelCSVForm
 from tenancy.models import Tenant
 from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField
@@ -26,7 +27,7 @@ class ProviderNetworkCSVForm(NetBoxModelCSVForm):
     provider = CSVModelChoiceField(
         queryset=Provider.objects.all(),
         to_field_name='name',
-        help_text='Assigned provider'
+        help_text=_('Assigned provider')
     )
 
     class Meta:
@@ -43,7 +44,7 @@ class CircuitTypeCSVForm(NetBoxModelCSVForm):
         model = CircuitType
         fields = ('name', 'slug', 'description', 'tags')
         help_texts = {
-            'name': 'Name of circuit type',
+            'name': _('Name of circuit type'),
         }
 
 
@@ -51,22 +52,22 @@ class CircuitCSVForm(NetBoxModelCSVForm):
     provider = CSVModelChoiceField(
         queryset=Provider.objects.all(),
         to_field_name='name',
-        help_text='Assigned provider'
+        help_text=_('Assigned provider')
     )
     type = CSVModelChoiceField(
         queryset=CircuitType.objects.all(),
         to_field_name='name',
-        help_text='Type of circuit'
+        help_text=_('Type of circuit')
     )
     status = CSVChoiceField(
         choices=CircuitStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:

+ 6 - 6
netbox/circuits/forms/model_forms.py

@@ -39,7 +39,7 @@ class ProviderForm(NetBoxModelForm):
             'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags',
         ]
         help_texts = {
-            'name': "Full name of the provider",
+            'name': _("Full name of the provider"),
         }
 
 
@@ -98,8 +98,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
             'tenant_group', 'tenant', 'comments', 'tags',
         ]
         help_texts = {
-            'cid': "Unique circuit ID",
-            'commit_rate': "Committed rate",
+            'cid': _("Unique circuit ID"),
+            'commit_rate': _("Committed rate"),
         }
         widgets = {
             'status': StaticSelect(),
@@ -157,9 +157,9 @@ class CircuitTerminationForm(NetBoxModelForm):
             'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'tags',
         ]
         help_texts = {
-            'port_speed': "Physical circuit speed",
-            'xconnect_id': "ID of the local cross-connect",
-            'pp_info': "Patch panel ID and port number(s)"
+            'port_speed': _("Physical circuit speed"),
+            'xconnect_id': _("ID of the local cross-connect"),
+            'pp_info': _("Patch panel ID and port number(s)")
         }
         widgets = {
             'term_side': StaticSelect(),

+ 2 - 1
netbox/circuits/models/circuits.py

@@ -3,6 +3,7 @@ from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.urls import reverse
+from django.utils.translation import gettext as _
 
 from circuits.choices import *
 from dcim.models import CabledObjectModel
@@ -168,7 +169,7 @@ class CircuitTermination(
         blank=True,
         null=True,
         verbose_name='Upstream speed (Kbps)',
-        help_text='Upstream speed, if different from port speed'
+        help_text=_('Upstream speed, if different from port speed')
     )
     xconnect_id = models.CharField(
         max_length=50,

+ 4 - 3
netbox/dcim/api/serializers.py

@@ -1,6 +1,7 @@
 import decimal
 
 from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import gettext as _
 from drf_yasg.utils import swagger_serializer_method
 from rest_framework import serializers
 from timezone_field.rest_framework import TimeZoneSerializerField
@@ -197,7 +198,7 @@ class RackSerializer(NetBoxModelSerializer):
     status = ChoiceField(choices=RackStatusChoices, required=False)
     role = NestedRackRoleSerializer(required=False, allow_null=True)
     type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
-    facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label='Facility ID',
+    facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
                                         default=None)
     width = ChoiceField(choices=RackWidthChoices, required=False)
     outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
@@ -311,7 +312,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
     u_height = serializers.DecimalField(
         max_digits=4,
         decimal_places=1,
-        label='Position (U)',
+        label=_('Position (U)'),
         min_value=0,
         default=1.0
     )
@@ -636,7 +637,7 @@ class DeviceSerializer(NetBoxModelSerializer):
         max_digits=4,
         decimal_places=1,
         allow_null=True,
-        label='Position (U)',
+        label=_('Position (U)'),
         min_value=decimal.Decimal(0.5),
         default=None
     )

Разница между файлами не показана из-за своего большого размера
+ 150 - 149
netbox/dcim/filtersets.py


+ 3 - 2
netbox/dcim/forms/bulk_create.py

@@ -1,6 +1,7 @@
 from django import forms
 
 from dcim.models import *
+from django.utils.translation import gettext as _
 from extras.forms import CustomFieldsMixin
 from extras.models import Tag
 from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
@@ -105,9 +106,9 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
     field_order = ('name', 'label', 'position_pattern', 'description', 'tags')
     replication_fields = ('name', 'label', 'position')
     position_pattern = ExpandableNameField(
-        label='Position',
+        label=_('Position'),
         required=False,
-        help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
+        help_text=_('Alphanumeric ranges are supported. (Must match the number of names being created.)')
     )
 
 

+ 24 - 24
netbox/dcim/forms/bulk_edit.py

@@ -1,6 +1,6 @@
 from django import forms
-from django.utils.translation import gettext as _
 from django.contrib.auth.models import User
+from django.utils.translation import gettext as _
 from timezone_field import TimeZoneFormField
 
 from dcim.choices import *
@@ -126,7 +126,7 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm):
     )
     contact_email = forms.EmailField(
         required=False,
-        label='Contact E-mail'
+        label=_('Contact E-mail')
     )
     time_zone = TimeZoneFormField(
         choices=add_blank_choice(TimeZoneFormField().choices),
@@ -248,7 +248,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
     serial = forms.CharField(
         max_length=50,
         required=False,
-        label='Serial Number'
+        label=_('Serial Number')
     )
     asset_tag = forms.CharField(
         max_length=50,
@@ -266,12 +266,12 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
     )
     u_height = forms.IntegerField(
         required=False,
-        label='Height (U)'
+        label=_('Height (U)')
     )
     desc_units = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect,
-        label='Descending units'
+        label=_('Descending units')
     )
     outer_width = forms.IntegerField(
         required=False,
@@ -380,7 +380,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
     is_full_depth = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect(),
-        label='Is full depth'
+        label=_('Is full depth')
     )
     airflow = forms.ChoiceField(
         choices=add_blank_choice(DeviceAirflowChoices),
@@ -456,7 +456,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
     vm_role = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect,
-        label='VM role'
+        label=_('VM role')
     )
     description = forms.CharField(
         max_length=200,
@@ -540,7 +540,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
     serial = forms.CharField(
         max_length=50,
         required=False,
-        label='Serial Number'
+        label=_('Serial Number')
     )
     description = forms.CharField(
         max_length=200,
@@ -577,7 +577,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
     serial = forms.CharField(
         max_length=50,
         required=False,
-        label='Serial Number'
+        label=_('Serial Number')
     )
     description = forms.CharField(
         max_length=200,
@@ -767,7 +767,7 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
     )
     comments = CommentField(
         widget=SmallTextarea,
-        label='Comments'
+        label=_('Comments')
     )
 
     model = PowerFeed
@@ -838,12 +838,12 @@ class PowerPortTemplateBulkEditForm(BulkEditForm):
     maximum_draw = forms.IntegerField(
         min_value=1,
         required=False,
-        help_text="Maximum power draw (watts)"
+        help_text=_("Maximum power draw (watts)")
     )
     allocated_draw = forms.IntegerField(
         min_value=1,
         required=False,
-        help_text="Allocated power draw (watts)"
+        help_text=_("Allocated power draw (watts)")
     )
     description = forms.CharField(
         required=False
@@ -916,7 +916,7 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
     mgmt_only = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect,
-        label='Management only'
+        label=_('Management only')
     )
     description = forms.CharField(
         required=False
@@ -926,14 +926,14 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
         required=False,
         initial='',
         widget=StaticSelect(),
-        label='PoE mode'
+        label=_('PoE mode')
     )
     poe_type = forms.ChoiceField(
         choices=add_blank_choice(InterfacePoETypeChoices),
         required=False,
         initial='',
         widget=StaticSelect(),
-        label='PoE type'
+        label=_('PoE type')
     )
 
     nullable_fields = ('label', 'description', 'poe_mode', 'poe_type')
@@ -1174,31 +1174,31 @@ class InterfaceBulkEditForm(
         query_params={
             'type': 'lag',
         },
-        label='LAG'
+        label=_('LAG')
     )
     speed = forms.IntegerField(
         required=False,
         widget=SelectSpeedWidget(),
-        label='Speed'
+        label=_('Speed')
     )
     mgmt_only = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect,
-        label='Management only'
+        label=_('Management only')
     )
     poe_mode = forms.ChoiceField(
         choices=add_blank_choice(InterfacePoEModeChoices),
         required=False,
         initial='',
         widget=StaticSelect(),
-        label='PoE mode'
+        label=_('PoE mode')
     )
     poe_type = forms.ChoiceField(
         choices=add_blank_choice(InterfacePoETypeChoices),
         required=False,
         initial='',
         widget=StaticSelect(),
-        label='PoE type'
+        label=_('PoE type')
     )
     mark_connected = forms.NullBooleanField(
         required=False,
@@ -1213,7 +1213,7 @@ class InterfaceBulkEditForm(
     vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
-        label='VLAN group'
+        label=_('VLAN group')
     )
     untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
@@ -1221,7 +1221,7 @@ class InterfaceBulkEditForm(
         query_params={
             'group_id': '$vlan_group',
         },
-        label='Untagged VLAN'
+        label=_('Untagged VLAN')
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
@@ -1229,12 +1229,12 @@ class InterfaceBulkEditForm(
         query_params={
             'group_id': '$vlan_group',
         },
-        label='Tagged VLANs'
+        label=_('Tagged VLANs')
     )
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
 
     model = Interface

+ 89 - 88
netbox/dcim/forms/bulk_import.py

@@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.core.exceptions import ObjectDoesNotExist
 from django.utils.safestring import mark_safe
+from django.utils.translation import gettext as _
 
 from dcim.choices import *
 from dcim.constants import *
@@ -52,7 +53,7 @@ class RegionCSVForm(NetBoxModelCSVForm):
         queryset=Region.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Name of parent region'
+        help_text=_('Name of parent region')
     )
 
     class Meta:
@@ -65,7 +66,7 @@ class SiteGroupCSVForm(NetBoxModelCSVForm):
         queryset=SiteGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Name of parent site group'
+        help_text=_('Name of parent site group')
     )
 
     class Meta:
@@ -76,25 +77,25 @@ class SiteGroupCSVForm(NetBoxModelCSVForm):
 class SiteCSVForm(NetBoxModelCSVForm):
     status = CSVChoiceField(
         choices=SiteStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     region = CSVModelChoiceField(
         queryset=Region.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned region'
+        help_text=_('Assigned region')
     )
     group = CSVModelChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned group'
+        help_text=_('Assigned group')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:
@@ -105,7 +106,7 @@ class SiteCSVForm(NetBoxModelCSVForm):
         )
         help_texts = {
             'time_zone': mark_safe(
-                'Time zone (<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">available options</a>)'
+                _('Time zone (<a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">available options</a>)')
             )
         }
 
@@ -114,26 +115,26 @@ class LocationCSVForm(NetBoxModelCSVForm):
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         to_field_name='name',
-        help_text='Assigned site'
+        help_text=_('Assigned site')
     )
     parent = CSVModelChoiceField(
         queryset=Location.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent location',
+        help_text=_('Parent location'),
         error_messages={
-            'invalid_choice': 'Location not found.',
+            'invalid_choice': _('Location not found.'),
         }
     )
     status = CSVChoiceField(
         choices=LocationStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:
@@ -148,7 +149,7 @@ class RackRoleCSVForm(NetBoxModelCSVForm):
         model = RackRole
         fields = ('name', 'slug', 'color', 'description', 'tags')
         help_texts = {
-            'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
+            'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
         }
 
 
@@ -166,31 +167,31 @@ class RackCSVForm(NetBoxModelCSVForm):
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Name of assigned tenant'
+        help_text=_('Name of assigned tenant')
     )
     status = CSVChoiceField(
         choices=RackStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     role = CSVModelChoiceField(
         queryset=RackRole.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Name of assigned role'
+        help_text=_('Name of assigned role')
     )
     type = CSVChoiceField(
         choices=RackTypeChoices,
         required=False,
-        help_text='Rack type'
+        help_text=_('Rack type')
     )
     width = forms.ChoiceField(
         choices=RackWidthChoices,
-        help_text='Rail-to-rail width (in inches)'
+        help_text=_('Rail-to-rail width (in inches)')
     )
     outer_unit = CSVChoiceField(
         choices=RackDimensionUnitChoices,
         required=False,
-        help_text='Unit for outer dimensions'
+        help_text=_('Unit for outer dimensions')
     )
 
     class Meta:
@@ -215,29 +216,29 @@ class RackReservationCSVForm(NetBoxModelCSVForm):
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         to_field_name='name',
-        help_text='Parent site'
+        help_text=_('Parent site')
     )
     location = CSVModelChoiceField(
         queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
-        help_text="Rack's location (if any)"
+        help_text=_("Rack's location (if any)")
     )
     rack = CSVModelChoiceField(
         queryset=Rack.objects.all(),
         to_field_name='name',
-        help_text='Rack'
+        help_text=_('Rack')
     )
     units = SimpleArrayField(
         base_field=forms.IntegerField(),
         required=True,
-        help_text='Comma-separated list of individual unit numbers'
+        help_text=_('Comma-separated list of individual unit numbers')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:
@@ -275,7 +276,7 @@ class DeviceRoleCSVForm(NetBoxModelCSVForm):
         model = DeviceRole
         fields = ('name', 'slug', 'color', 'vm_role', 'description', 'tags')
         help_texts = {
-            'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
+            'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
         }
 
 
@@ -285,7 +286,7 @@ class PlatformCSVForm(NetBoxModelCSVForm):
         queryset=Manufacturer.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Limit platform assignments to this manufacturer'
+        help_text=_('Limit platform assignments to this manufacturer')
     )
 
     class Meta:
@@ -297,45 +298,45 @@ class BaseDeviceCSVForm(NetBoxModelCSVForm):
     device_role = CSVModelChoiceField(
         queryset=DeviceRole.objects.all(),
         to_field_name='name',
-        help_text='Assigned role'
+        help_text=_('Assigned role')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     manufacturer = CSVModelChoiceField(
         queryset=Manufacturer.objects.all(),
         to_field_name='name',
-        help_text='Device type manufacturer'
+        help_text=_('Device type manufacturer')
     )
     device_type = CSVModelChoiceField(
         queryset=DeviceType.objects.all(),
         to_field_name='model',
-        help_text='Device type model'
+        help_text=_('Device type model')
     )
     platform = CSVModelChoiceField(
         queryset=Platform.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned platform'
+        help_text=_('Assigned platform')
     )
     status = CSVChoiceField(
         choices=DeviceStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     virtual_chassis = CSVModelChoiceField(
         queryset=VirtualChassis.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Virtual chassis'
+        help_text=_('Virtual chassis')
     )
     cluster = CSVModelChoiceField(
         queryset=Cluster.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Virtualization cluster'
+        help_text=_('Virtualization cluster')
     )
 
     class Meta:
@@ -360,29 +361,29 @@ class DeviceCSVForm(BaseDeviceCSVForm):
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         to_field_name='name',
-        help_text='Assigned site'
+        help_text=_('Assigned site')
     )
     location = CSVModelChoiceField(
         queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
-        help_text="Assigned location (if any)"
+        help_text=_("Assigned location (if any)")
     )
     rack = CSVModelChoiceField(
         queryset=Rack.objects.all(),
         to_field_name='name',
         required=False,
-        help_text="Assigned rack (if any)"
+        help_text=_("Assigned rack (if any)")
     )
     face = CSVChoiceField(
         choices=DeviceFaceChoices,
         required=False,
-        help_text='Mounted rack face'
+        help_text=_('Mounted rack face')
     )
     airflow = CSVChoiceField(
         choices=DeviceAirflowChoices,
         required=False,
-        help_text='Airflow direction'
+        help_text=_('Airflow direction')
     )
 
     class Meta(BaseDeviceCSVForm.Meta):
@@ -442,12 +443,12 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
     parent = CSVModelChoiceField(
         queryset=Device.objects.all(),
         to_field_name='name',
-        help_text='Parent device'
+        help_text=_('Parent device')
     )
     device_bay = CSVModelChoiceField(
         queryset=DeviceBay.objects.all(),
         to_field_name='name',
-        help_text='Device bay in which this device is installed'
+        help_text=_('Device bay in which this device is installed')
     )
 
     class Meta(BaseDeviceCSVForm.Meta):
@@ -492,14 +493,14 @@ class ConsolePortCSVForm(NetBoxModelCSVForm):
     type = CSVChoiceField(
         choices=ConsolePortTypeChoices,
         required=False,
-        help_text='Port type'
+        help_text=_('Port type')
     )
     speed = CSVTypedChoiceField(
         choices=ConsolePortSpeedChoices,
         coerce=int,
         empty_value=None,
         required=False,
-        help_text='Port speed in bps'
+        help_text=_('Port speed in bps')
     )
 
     class Meta:
@@ -515,14 +516,14 @@ class ConsoleServerPortCSVForm(NetBoxModelCSVForm):
     type = CSVChoiceField(
         choices=ConsolePortTypeChoices,
         required=False,
-        help_text='Port type'
+        help_text=_('Port type')
     )
     speed = CSVTypedChoiceField(
         choices=ConsolePortSpeedChoices,
         coerce=int,
         empty_value=None,
         required=False,
-        help_text='Port speed in bps'
+        help_text=_('Port speed in bps')
     )
 
     class Meta:
@@ -538,7 +539,7 @@ class PowerPortCSVForm(NetBoxModelCSVForm):
     type = CSVChoiceField(
         choices=PowerPortTypeChoices,
         required=False,
-        help_text='Port type'
+        help_text=_('Port type')
     )
 
     class Meta:
@@ -556,18 +557,18 @@ class PowerOutletCSVForm(NetBoxModelCSVForm):
     type = CSVChoiceField(
         choices=PowerOutletTypeChoices,
         required=False,
-        help_text='Outlet type'
+        help_text=_('Outlet type')
     )
     power_port = CSVModelChoiceField(
         queryset=PowerPort.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Local power port which feeds this outlet'
+        help_text=_('Local power port which feeds this outlet')
     )
     feed_leg = CSVChoiceField(
         choices=PowerOutletFeedLegChoices,
         required=False,
-        help_text='Electrical phase (for three-phase circuits)'
+        help_text=_('Electrical phase (for three-phase circuits)')
     )
 
     class Meta:
@@ -606,23 +607,23 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
         queryset=Interface.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent interface'
+        help_text=_('Parent interface')
     )
     bridge = CSVModelChoiceField(
         queryset=Interface.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Bridged interface'
+        help_text=_('Bridged interface')
     )
     lag = CSVModelChoiceField(
         queryset=Interface.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent LAG interface'
+        help_text=_('Parent LAG interface')
     )
     type = CSVChoiceField(
         choices=InterfaceTypeChoices,
-        help_text='Physical medium'
+        help_text=_('Physical medium')
     )
     duplex = CSVChoiceField(
         choices=InterfaceDuplexChoices,
@@ -631,28 +632,28 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
     poe_mode = CSVChoiceField(
         choices=InterfacePoEModeChoices,
         required=False,
-        help_text='PoE mode'
+        help_text=_('PoE mode')
     )
     poe_type = CSVChoiceField(
         choices=InterfacePoETypeChoices,
         required=False,
-        help_text='PoE type'
+        help_text=_('PoE type')
     )
     mode = CSVChoiceField(
         choices=InterfaceModeChoices,
         required=False,
-        help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
+        help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
     )
     vrf = CSVModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
         to_field_name='rd',
-        help_text='Assigned VRF'
+        help_text=_('Assigned VRF')
     )
     rf_role = CSVChoiceField(
         choices=WirelessRoleChoices,
         required=False,
-        help_text='Wireless role (AP/station)'
+        help_text=_('Wireless role (AP/station)')
     )
 
     class Meta:
@@ -692,11 +693,11 @@ class FrontPortCSVForm(NetBoxModelCSVForm):
     rear_port = CSVModelChoiceField(
         queryset=RearPort.objects.all(),
         to_field_name='name',
-        help_text='Corresponding rear port'
+        help_text=_('Corresponding rear port')
     )
     type = CSVChoiceField(
         choices=PortTypeChoices,
-        help_text='Physical medium classification'
+        help_text=_('Physical medium classification')
     )
 
     class Meta:
@@ -706,7 +707,7 @@ class FrontPortCSVForm(NetBoxModelCSVForm):
             'description', 'tags'
         )
         help_texts = {
-            'rear_port_position': 'Mapped position on corresponding rear port',
+            'rear_port_position': _('Mapped position on corresponding rear port'),
         }
 
     def __init__(self, *args, **kwargs):
@@ -738,7 +739,7 @@ class RearPortCSVForm(NetBoxModelCSVForm):
         to_field_name='name'
     )
     type = CSVChoiceField(
-        help_text='Physical medium classification',
+        help_text=_('Physical medium classification'),
         choices=PortTypeChoices,
     )
 
@@ -746,7 +747,7 @@ class RearPortCSVForm(NetBoxModelCSVForm):
         model = RearPort
         fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'tags')
         help_texts = {
-            'positions': 'Number of front ports which may be mapped'
+            'positions': _('Number of front ports which may be mapped')
         }
 
 
@@ -770,9 +771,9 @@ class DeviceBayCSVForm(NetBoxModelCSVForm):
         queryset=Device.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Child device installed within this bay',
+        help_text=_('Child device installed within this bay'),
         error_messages={
-            'invalid_choice': 'Child device not found.',
+            'invalid_choice': _('Child device not found.'),
         }
     )
 
@@ -826,7 +827,7 @@ class InventoryItemCSVForm(NetBoxModelCSVForm):
         queryset=Device.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Parent inventory item'
+        help_text=_('Parent inventory item')
     )
 
     class Meta:
@@ -863,7 +864,7 @@ class InventoryItemRoleCSVForm(NetBoxModelCSVForm):
         model = InventoryItemRole
         fields = ('name', 'slug', 'color', 'description')
         help_texts = {
-            'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
+            'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
         }
 
 
@@ -876,53 +877,53 @@ class CableCSVForm(NetBoxModelCSVForm):
     side_a_device = CSVModelChoiceField(
         queryset=Device.objects.all(),
         to_field_name='name',
-        help_text='Side A device'
+        help_text=_('Side A device')
     )
     side_a_type = CSVContentTypeField(
         queryset=ContentType.objects.all(),
         limit_choices_to=CABLE_TERMINATION_MODELS,
-        help_text='Side A type'
+        help_text=_('Side A type')
     )
     side_a_name = forms.CharField(
-        help_text='Side A component name'
+        help_text=_('Side A component name')
     )
 
     # Termination B
     side_b_device = CSVModelChoiceField(
         queryset=Device.objects.all(),
         to_field_name='name',
-        help_text='Side B device'
+        help_text=_('Side B device')
     )
     side_b_type = CSVContentTypeField(
         queryset=ContentType.objects.all(),
         limit_choices_to=CABLE_TERMINATION_MODELS,
-        help_text='Side B type'
+        help_text=_('Side B type')
     )
     side_b_name = forms.CharField(
-        help_text='Side B component name'
+        help_text=_('Side B component name')
     )
 
     # Cable attributes
     status = CSVChoiceField(
         choices=LinkStatusChoices,
         required=False,
-        help_text='Connection status'
+        help_text=_('Connection status')
     )
     type = CSVChoiceField(
         choices=CableTypeChoices,
         required=False,
-        help_text='Physical medium classification'
+        help_text=_('Physical medium classification')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     length_unit = CSVChoiceField(
         choices=CableLengthUnitChoices,
         required=False,
-        help_text='Length unit'
+        help_text=_('Length unit')
     )
 
     class Meta:
@@ -932,7 +933,7 @@ class CableCSVForm(NetBoxModelCSVForm):
             'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
         ]
         help_texts = {
-            'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
+            'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
         }
 
     def _clean_side(self, side):
@@ -981,7 +982,7 @@ class VirtualChassisCSVForm(NetBoxModelCSVForm):
         queryset=Device.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Master device'
+        help_text=_('Master device')
     )
 
     class Meta:
@@ -997,7 +998,7 @@ class PowerPanelCSVForm(NetBoxModelCSVForm):
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         to_field_name='name',
-        help_text='Name of parent site'
+        help_text=_('Name of parent site')
     )
     location = CSVModelChoiceField(
         queryset=Location.objects.all(),
@@ -1023,40 +1024,40 @@ class PowerFeedCSVForm(NetBoxModelCSVForm):
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         to_field_name='name',
-        help_text='Assigned site'
+        help_text=_('Assigned site')
     )
     power_panel = CSVModelChoiceField(
         queryset=PowerPanel.objects.all(),
         to_field_name='name',
-        help_text='Upstream power panel'
+        help_text=_('Upstream power panel')
     )
     location = CSVModelChoiceField(
         queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
-        help_text="Rack's location (if any)"
+        help_text=_("Rack's location (if any)")
     )
     rack = CSVModelChoiceField(
         queryset=Rack.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Rack'
+        help_text=_('Rack')
     )
     status = CSVChoiceField(
         choices=PowerFeedStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     type = CSVChoiceField(
         choices=PowerFeedTypeChoices,
-        help_text='Primary or redundant'
+        help_text=_('Primary or redundant')
     )
     supply = CSVChoiceField(
         choices=PowerFeedSupplyChoices,
-        help_text='Supply type (AC/DC)'
+        help_text=_('Supply type (AC/DC)')
     )
     phase = CSVChoiceField(
         choices=PowerFeedPhaseChoices,
-        help_text='Single or three-phase'
+        help_text=_('Single or three-phase')
     )
 
     class Meta:

+ 3 - 2
netbox/dcim/forms/common.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from dcim.choices import *
 from dcim.constants import *
@@ -12,13 +13,13 @@ class InterfaceCommonForm(forms.Form):
     mac_address = forms.CharField(
         empty_value=None,
         required=False,
-        label='MAC address'
+        label=_('MAC address')
     )
     mtu = forms.IntegerField(
         required=False,
         min_value=INTERFACE_MTU_MIN,
         max_value=INTERFACE_MTU_MAX,
-        label='MTU'
+        label=_('MTU')
     )
 
     def clean(self):

+ 12 - 11
netbox/dcim/forms/connections.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from circuits.models import Circuit, CircuitTermination, Provider
 from dcim.models import *
@@ -16,7 +17,7 @@ def get_cable_form(a_type, b_type):
 
                 attrs[f'termination_{cable_end}_region'] = DynamicModelChoiceField(
                     queryset=Region.objects.all(),
-                    label='Region',
+                    label=_('Region'),
                     required=False,
                     initial_params={
                         'sites': f'$termination_{cable_end}_site'
@@ -24,7 +25,7 @@ def get_cable_form(a_type, b_type):
                 )
                 attrs[f'termination_{cable_end}_sitegroup'] = DynamicModelChoiceField(
                     queryset=SiteGroup.objects.all(),
-                    label='Site group',
+                    label=_('Site group'),
                     required=False,
                     initial_params={
                         'sites': f'$termination_{cable_end}_site'
@@ -32,7 +33,7 @@ def get_cable_form(a_type, b_type):
                 )
                 attrs[f'termination_{cable_end}_site'] = DynamicModelChoiceField(
                     queryset=Site.objects.all(),
-                    label='Site',
+                    label=_('Site'),
                     required=False,
                     query_params={
                         'region_id': f'$termination_{cable_end}_region',
@@ -41,7 +42,7 @@ def get_cable_form(a_type, b_type):
                 )
                 attrs[f'termination_{cable_end}_location'] = DynamicModelChoiceField(
                     queryset=Location.objects.all(),
-                    label='Location',
+                    label=_('Location'),
                     required=False,
                     null_option='None',
                     query_params={
@@ -54,7 +55,7 @@ def get_cable_form(a_type, b_type):
 
                     attrs[f'termination_{cable_end}_rack'] = DynamicModelChoiceField(
                         queryset=Rack.objects.all(),
-                        label='Rack',
+                        label=_('Rack'),
                         required=False,
                         null_option='None',
                         initial_params={
@@ -67,7 +68,7 @@ def get_cable_form(a_type, b_type):
                     )
                     attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField(
                         queryset=Device.objects.all(),
-                        label='Device',
+                        label=_('Device'),
                         required=False,
                         initial_params={
                             f'{term_cls._meta.model_name}s__in': f'${cable_end}_terminations'
@@ -93,7 +94,7 @@ def get_cable_form(a_type, b_type):
 
                     attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField(
                         queryset=PowerPanel.objects.all(),
-                        label='Power Panel',
+                        label=_('Power Panel'),
                         required=False,
                         initial_params={
                             'powerfeeds__in': f'${cable_end}_terminations'
@@ -105,7 +106,7 @@ def get_cable_form(a_type, b_type):
                     )
                     attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
                         queryset=term_cls.objects.all(),
-                        label='Power Feed',
+                        label=_('Power Feed'),
                         disabled_indicator='_occupied',
                         query_params={
                             'power_panel_id': f'$termination_{cable_end}_powerpanel',
@@ -117,7 +118,7 @@ def get_cable_form(a_type, b_type):
 
                     attrs[f'termination_{cable_end}_provider'] = DynamicModelChoiceField(
                         queryset=Provider.objects.all(),
-                        label='Provider',
+                        label=_('Provider'),
                         initial_params={
                             'circuits': f'$termination_{cable_end}_circuit'
                         },
@@ -125,7 +126,7 @@ def get_cable_form(a_type, b_type):
                     )
                     attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField(
                         queryset=Circuit.objects.all(),
-                        label='Circuit',
+                        label=_('Circuit'),
                         initial_params={
                             'terminations__in': f'${cable_end}_terminations'
                         },
@@ -136,7 +137,7 @@ def get_cable_form(a_type, b_type):
                     )
                     attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
                         queryset=term_cls.objects.all(),
-                        label='Side',
+                        label=_('Side'),
                         disabled_indicator='_occupied',
                         query_params={
                             'circuit_id': f'$termination_{cable_end}_circuit',

+ 39 - 39
netbox/dcim/forms/model_forms.py

@@ -1,7 +1,7 @@
 from django import forms
-from django.utils.translation import gettext as _
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import gettext as _
 from timezone_field import TimeZoneFormField
 
 from dcim.choices import *
@@ -163,14 +163,14 @@ class SiteForm(TenancyForm, NetBoxModelForm):
             'time_zone': StaticSelect(),
         }
         help_texts = {
-            'name': "Full name of the site",
-            'facility': "Data center provider and facility (e.g. Equinix NY7)",
-            'time_zone': "Local time zone",
-            'description': "Short description (will appear in sites list)",
-            'physical_address': "Physical location of the building (e.g. for GPS)",
-            'shipping_address': "If different from the physical address",
-            'latitude': "Latitude in decimal format (xx.yyyyyy)",
-            'longitude': "Longitude in decimal format (xx.yyyyyy)"
+            'name': _("Full name of the site"),
+            'facility': _("Data center provider and facility (e.g. Equinix NY7)"),
+            'time_zone': _("Local time zone"),
+            'description': _("Short description (will appear in sites list)"),
+            'physical_address': _("Physical location of the building (e.g. for GPS)"),
+            'shipping_address': _("If different from the physical address"),
+            'latitude': _("Latitude in decimal format (xx.yyyyyy)"),
+            'longitude': _("Longitude in decimal format (xx.yyyyyy)")
         }
 
 
@@ -282,10 +282,10 @@ class RackForm(TenancyForm, NetBoxModelForm):
             'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'description', 'comments', 'tags',
         ]
         help_texts = {
-            'site': "The site at which the rack exists",
-            'name': "Organizational rack name",
-            'facility_id': "The unique rack ID assigned by the facility",
-            'u_height': "Height in rack units",
+            'site': _("The site at which the rack exists"),
+            'name': _("Organizational rack name"),
+            'facility_id': _("The unique rack ID assigned by the facility"),
+            'u_height': _("Height in rack units"),
         }
         widgets = {
             'status': StaticSelect(),
@@ -335,7 +335,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
     )
     units = NumericArrayField(
         base_field=forms.IntegerField(),
-        help_text="Comma-separated list of numeric unit IDs. A range may be specified using a hyphen."
+        help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
     )
     user = forms.ModelChoiceField(
         queryset=User.objects.order_by(
@@ -519,7 +519,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
     )
     position = forms.DecimalField(
         required=False,
-        help_text="The lowest-numbered unit occupied by the device",
+        help_text=_("The lowest-numbered unit occupied by the device"),
         widget=APISelect(
             api_url='/api/dcim/racks/{{rack}}/elevation/',
             attrs={
@@ -577,13 +577,13 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
     )
     vc_position = forms.IntegerField(
         required=False,
-        label='Position',
-        help_text="The position in the virtual chassis this device is identified by"
+        label=_('Position'),
+        help_text=_("The position in the virtual chassis this device is identified by")
     )
     vc_priority = forms.IntegerField(
         required=False,
-        label='Priority',
-        help_text="The priority of the device in the virtual chassis"
+        label=_('Priority'),
+        help_text=_("The priority of the device in the virtual chassis")
     )
 
     class Meta:
@@ -595,10 +595,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
             'description', 'comments', 'tags', 'local_context_data'
         ]
         help_texts = {
-            'device_role': "The function this device serves",
-            'serial': "Chassis serial number",
-            'local_context_data': "Local config context data overwrites all source contexts in the final rendered "
-                                  "config context",
+            'device_role': _("The function this device serves"),
+            'serial': _("Chassis serial number"),
+            'local_context_data': _("Local config context data overwrites all source contexts in the final rendered "
+                                    "config context"),
         }
         widgets = {
             'face': StaticSelect(),
@@ -695,13 +695,13 @@ class ModuleForm(NetBoxModelForm):
     replicate_components = forms.BooleanField(
         required=False,
         initial=True,
-        help_text="Automatically populate components associated with this module type"
+        help_text=_("Automatically populate components associated with this module type")
     )
 
     adopt_components = forms.BooleanField(
         required=False,
         initial=False,
-        help_text="Adopt already existing components"
+        help_text=_("Adopt already existing components")
     )
 
     fieldsets = (
@@ -1390,7 +1390,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     parent = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         required=False,
-        label='Parent interface',
+        label=_('Parent interface'),
         query_params={
             'device_id': '$device',
         }
@@ -1398,7 +1398,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     bridge = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         required=False,
-        label='Bridged interface',
+        label=_('Bridged interface'),
         query_params={
             'device_id': '$device',
         }
@@ -1406,7 +1406,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     lag = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         required=False,
-        label='LAG interface',
+        label=_('LAG interface'),
         query_params={
             'device_id': '$device',
             'type': 'lag',
@@ -1415,12 +1415,12 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     wireless_lan_group = DynamicModelChoiceField(
         queryset=WirelessLANGroup.objects.all(),
         required=False,
-        label='Wireless LAN group'
+        label=_('Wireless LAN group')
     )
     wireless_lans = DynamicModelMultipleChoiceField(
         queryset=WirelessLAN.objects.all(),
         required=False,
-        label='Wireless LANs',
+        label=_('Wireless LANs'),
         query_params={
             'group_id': '$wireless_lan_group',
         }
@@ -1428,12 +1428,12 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
-        label='VLAN group'
+        label=_('VLAN group')
     )
     untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        label='Untagged VLAN',
+        label=_('Untagged VLAN'),
         query_params={
             'group_id': '$vlan_group',
             'available_on_device': '$device',
@@ -1442,7 +1442,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        label='Tagged VLANs',
+        label=_('Tagged VLANs'),
         query_params={
             'group_id': '$vlan_group',
             'available_on_device': '$device',
@@ -1451,13 +1451,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
 
     wwn = forms.CharField(
         empty_value=None,
         required=False,
-        label='WWN'
+        label=_('WWN')
     )
 
     fieldsets = (
@@ -1495,8 +1495,8 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
         }
         help_texts = {
             'mode': INTERFACE_MODE_HELP_TEXT,
-            'rf_channel_frequency': "Populated by selected channel (if set)",
-            'rf_channel_width': "Populated by selected channel (if set)",
+            'rf_channel_frequency': _("Populated by selected channel (if set)"),
+            'rf_channel_width': _("Populated by selected channel (if set)"),
         }
 
 
@@ -1570,8 +1570,8 @@ class DeviceBayForm(DeviceComponentForm):
 class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
     installed_device = forms.ModelChoiceField(
         queryset=Device.objects.all(),
-        label='Child Device',
-        help_text="Child devices must first be created and assigned to the site/rack of the parent device.",
+        label=_('Child Device'),
+        help_text=_("Child devices must first be created and assigned to the site/rack of the parent device."),
         widget=StaticSelect(),
     )
 

+ 11 - 10
netbox/dcim/forms/object_create.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from dcim.models import *
 from netbox.forms import NetBoxModelForm
@@ -39,7 +40,7 @@ class ComponentCreateForm(forms.Form):
     name = ExpandableNameField()
     label = ExpandableNameField(
         required=False,
-        help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
+        help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
     )
 
     # Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
@@ -97,8 +98,8 @@ class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemp
 class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
     rear_port = forms.MultipleChoiceField(
         choices=[],
-        label='Rear ports',
-        help_text='Select one rear port assignment for each front port being created.',
+        label=_('Rear ports'),
+        help_text=_('Select one rear port assignment for each front port being created.'),
     )
 
     # Override fieldsets from FrontPortTemplateForm to omit rear_port_position
@@ -166,9 +167,9 @@ class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemp
 
 class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
     position = ExpandableNameField(
-        label='Position',
+        label=_('Position'),
         required=False,
-        help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
+        help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
     )
     replication_fields = ('name', 'label', 'position')
 
@@ -226,8 +227,8 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
 class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
     rear_port = forms.MultipleChoiceField(
         choices=[],
-        label='Rear ports',
-        help_text='Select one rear port assignment for each front port being created.',
+        label=_('Rear ports'),
+        help_text=_('Select one rear port assignment for each front port being created.'),
     )
 
     # Override fieldsets from FrontPortForm to omit rear_port_position
@@ -290,9 +291,9 @@ class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
 
 class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
     position = ExpandableNameField(
-        label='Position',
+        label=_('Position'),
         required=False,
-        help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
+        help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
     )
     replication_fields = ('name', 'label', 'position')
 
@@ -352,7 +353,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
     initial_position = forms.IntegerField(
         initial=1,
         required=False,
-        help_text='Position of the first member device. Increases by one for each additional member.'
+        help_text=_('Position of the first member device. Increases by one for each additional member.')
     )
 
     class Meta:

+ 3 - 2
netbox/dcim/forms/object_import.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
 from dcim.models import *
@@ -115,12 +116,12 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
     poe_mode = forms.ChoiceField(
         choices=InterfacePoEModeChoices,
         required=False,
-        label='PoE mode'
+        label=_('PoE mode')
     )
     poe_type = forms.ChoiceField(
         choices=InterfacePoETypeChoices,
         required=False,
-        label='PoE type'
+        label=_('PoE type')
     )
 
     class Meta:

+ 7 - 6
netbox/dcim/models/device_component_templates.py

@@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
+from django.utils.translation import gettext as _
 from mptt.models import MPTTModel, TreeForeignKey
 
 from dcim.choices import *
@@ -52,7 +53,7 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
     label = models.CharField(
         max_length=64,
         blank=True,
-        help_text="Physical label"
+        help_text=_("Physical label")
     )
     description = models.CharField(
         max_length=200,
@@ -222,13 +223,13 @@ class PowerPortTemplate(ModularComponentTemplateModel):
         blank=True,
         null=True,
         validators=[MinValueValidator(1)],
-        help_text="Maximum power draw (watts)"
+        help_text=_("Maximum power draw (watts)")
     )
     allocated_draw = models.PositiveSmallIntegerField(
         blank=True,
         null=True,
         validators=[MinValueValidator(1)],
-        help_text="Allocated power draw (watts)"
+        help_text=_("Allocated power draw (watts)")
     )
 
     component_model = PowerPort
@@ -283,7 +284,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
         max_length=50,
         choices=PowerOutletFeedLegChoices,
         blank=True,
-        help_text="Phase (for three-phase feeds)"
+        help_text=_("Phase (for three-phase feeds)")
     )
 
     component_model = PowerOutlet
@@ -526,7 +527,7 @@ class ModuleBayTemplate(ComponentTemplateModel):
     position = models.CharField(
         max_length=30,
         blank=True,
-        help_text='Identifier to reference when renaming installed components'
+        help_text=_('Identifier to reference when renaming installed components')
     )
 
     component_model = ModuleBay
@@ -621,7 +622,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
         max_length=50,
         verbose_name='Part ID',
         blank=True,
-        help_text='Manufacturer-assigned part identifier'
+        help_text=_('Manufacturer-assigned part identifier')
     )
 
     objects = TreeManager()

+ 18 - 17
netbox/dcim/models/device_components.py

@@ -7,6 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db.models import Sum
 from django.urls import reverse
+from django.utils.translation import gettext as _
 from mptt.models import MPTTModel, TreeForeignKey
 
 from dcim.choices import *
@@ -60,7 +61,7 @@ class ComponentModel(NetBoxModel):
     label = models.CharField(
         max_length=64,
         blank=True,
-        help_text="Physical label"
+        help_text=_("Physical label")
     )
     description = models.CharField(
         max_length=200,
@@ -129,7 +130,7 @@ class CabledObjectModel(models.Model):
     )
     mark_connected = models.BooleanField(
         default=False,
-        help_text="Treat as if a cable is connected"
+        help_text=_("Treat as if a cable is connected")
     )
 
     cable_terminations = GenericRelation(
@@ -261,13 +262,13 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint):
         max_length=50,
         choices=ConsolePortTypeChoices,
         blank=True,
-        help_text='Physical port type'
+        help_text=_('Physical port type')
     )
     speed = models.PositiveIntegerField(
         choices=ConsolePortSpeedChoices,
         blank=True,
         null=True,
-        help_text='Port speed in bits per second'
+        help_text=_('Port speed in bits per second')
     )
 
     clone_fields = ('device', 'module', 'type', 'speed')
@@ -284,13 +285,13 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
         max_length=50,
         choices=ConsolePortTypeChoices,
         blank=True,
-        help_text='Physical port type'
+        help_text=_('Physical port type')
     )
     speed = models.PositiveIntegerField(
         choices=ConsolePortSpeedChoices,
         blank=True,
         null=True,
-        help_text='Port speed in bits per second'
+        help_text=_('Port speed in bits per second')
     )
 
     clone_fields = ('device', 'module', 'type', 'speed')
@@ -311,19 +312,19 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
         max_length=50,
         choices=PowerPortTypeChoices,
         blank=True,
-        help_text='Physical port type'
+        help_text=_('Physical port type')
     )
     maximum_draw = models.PositiveSmallIntegerField(
         blank=True,
         null=True,
         validators=[MinValueValidator(1)],
-        help_text="Maximum power draw (watts)"
+        help_text=_("Maximum power draw (watts)")
     )
     allocated_draw = models.PositiveSmallIntegerField(
         blank=True,
         null=True,
         validators=[MinValueValidator(1)],
-        help_text="Allocated power draw (watts)"
+        help_text=_("Allocated power draw (watts)")
     )
 
     clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
@@ -420,7 +421,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
         max_length=50,
         choices=PowerOutletTypeChoices,
         blank=True,
-        help_text='Physical port type'
+        help_text=_('Physical port type')
     )
     power_port = models.ForeignKey(
         to='dcim.PowerPort',
@@ -433,7 +434,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
         max_length=50,
         choices=PowerOutletFeedLegChoices,
         blank=True,
-        help_text="Phase (for three-phase feeds)"
+        help_text=_("Phase (for three-phase feeds)")
     )
 
     clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
@@ -550,7 +551,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
     mgmt_only = models.BooleanField(
         default=False,
         verbose_name='Management only',
-        help_text='This interface is used only for out-of-band management'
+        help_text=_('This interface is used only for out-of-band management')
     )
     speed = models.PositiveIntegerField(
         blank=True,
@@ -567,7 +568,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
         null=True,
         blank=True,
         verbose_name='WWN',
-        help_text='64-bit World Wide Name'
+        help_text=_('64-bit World Wide Name')
     )
     rf_role = models.CharField(
         max_length=30,
@@ -970,7 +971,7 @@ class ModuleBay(ComponentModel):
     position = models.CharField(
         max_length=30,
         blank=True,
-        help_text='Identifier to reference when renaming installed components'
+        help_text=_('Identifier to reference when renaming installed components')
     )
 
     clone_fields = ('device',)
@@ -1084,7 +1085,7 @@ class InventoryItem(MPTTModel, ComponentModel):
         max_length=50,
         verbose_name='Part ID',
         blank=True,
-        help_text='Manufacturer-assigned part identifier'
+        help_text=_('Manufacturer-assigned part identifier')
     )
     serial = models.CharField(
         max_length=50,
@@ -1097,11 +1098,11 @@ class InventoryItem(MPTTModel, ComponentModel):
         blank=True,
         null=True,
         verbose_name='Asset tag',
-        help_text='A unique tag used to identify this item'
+        help_text=_('A unique tag used to identify this item')
     )
     discovered = models.BooleanField(
         default=False,
-        help_text='This item was automatically discovered'
+        help_text=_('This item was automatically discovered')
     )
 
     objects = TreeManager()

+ 13 - 12
netbox/dcim/models/devices.py

@@ -12,6 +12,7 @@ from django.db.models import F, ProtectedError
 from django.db.models.functions import Lower
 from django.urls import reverse
 from django.utils.safestring import mark_safe
+from django.utils.translation import gettext as _
 
 from dcim.choices import *
 from dcim.constants import *
@@ -84,7 +85,7 @@ class DeviceType(PrimaryModel, WeightMixin):
     part_number = models.CharField(
         max_length=50,
         blank=True,
-        help_text='Discrete part number (optional)'
+        help_text=_('Discrete part number (optional)')
     )
     u_height = models.DecimalField(
         max_digits=4,
@@ -95,15 +96,15 @@ class DeviceType(PrimaryModel, WeightMixin):
     is_full_depth = models.BooleanField(
         default=True,
         verbose_name='Is full depth',
-        help_text='Device consumes both front and rear rack faces'
+        help_text=_('Device consumes both front and rear rack faces')
     )
     subdevice_role = models.CharField(
         max_length=50,
         choices=SubdeviceRoleChoices,
         blank=True,
         verbose_name='Parent/child status',
-        help_text='Parent devices house child devices in device bays. Leave blank '
-                  'if this device type is neither a parent nor a child.'
+        help_text=_('Parent devices house child devices in device bays. Leave blank '
+                    'if this device type is neither a parent nor a child.')
     )
     airflow = models.CharField(
         max_length=50,
@@ -314,7 +315,7 @@ class ModuleType(PrimaryModel, WeightMixin):
     part_number = models.CharField(
         max_length=50,
         blank=True,
-        help_text='Discrete part number (optional)'
+        help_text=_('Discrete part number (optional)')
     )
 
     # Generic relations
@@ -400,7 +401,7 @@ class DeviceRole(OrganizationalModel):
     vm_role = models.BooleanField(
         default=True,
         verbose_name='VM Role',
-        help_text='Virtual machines may be assigned to this role'
+        help_text=_('Virtual machines may be assigned to this role')
     )
 
     def get_absolute_url(self):
@@ -419,19 +420,19 @@ class Platform(OrganizationalModel):
         related_name='platforms',
         blank=True,
         null=True,
-        help_text='Optionally limit this platform to devices of a certain manufacturer'
+        help_text=_('Optionally limit this platform to devices of a certain manufacturer')
     )
     napalm_driver = models.CharField(
         max_length=50,
         blank=True,
         verbose_name='NAPALM driver',
-        help_text='The name of the NAPALM driver to use when interacting with devices'
+        help_text=_('The name of the NAPALM driver to use when interacting with devices')
     )
     napalm_args = models.JSONField(
         blank=True,
         null=True,
         verbose_name='NAPALM arguments',
-        help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
+        help_text=_('Additional arguments to pass when initiating the NAPALM driver (JSON format)')
     )
 
     def get_absolute_url(self):
@@ -496,7 +497,7 @@ class Device(PrimaryModel, ConfigContextModel):
         null=True,
         unique=True,
         verbose_name='Asset tag',
-        help_text='A unique tag used to identify this device'
+        help_text=_('A unique tag used to identify this device')
     )
     site = models.ForeignKey(
         to='dcim.Site',
@@ -524,7 +525,7 @@ class Device(PrimaryModel, ConfigContextModel):
         null=True,
         validators=[MinValueValidator(1), MaxValueValidator(99.5)],
         verbose_name='Position (U)',
-        help_text='The lowest-numbered unit occupied by the device'
+        help_text=_('The lowest-numbered unit occupied by the device')
     )
     face = models.CharField(
         max_length=50,
@@ -929,7 +930,7 @@ class Module(PrimaryModel, ConfigContextModel):
         null=True,
         unique=True,
         verbose_name='Asset tag',
-        help_text='A unique tag used to identify this device'
+        help_text=_('A unique tag used to identify this device')
     )
 
     clone_fields = ('device', 'module_type')

+ 2 - 1
netbox/dcim/models/power.py

@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.urls import reverse
+from django.utils.translation import gettext as _
 
 from dcim.choices import *
 from netbox.config import ConfigItem
@@ -125,7 +126,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
     max_utilization = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1), MaxValueValidator(100)],
         default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
-        help_text="Maximum permissible draw (percentage)"
+        help_text=_("Maximum permissible draw (percentage)")
     )
     available_power = models.PositiveIntegerField(
         default=0,

+ 11 - 10
netbox/dcim/models/racks.py

@@ -10,6 +10,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db.models import Count
 from django.urls import reverse
+from django.utils.translation import gettext as _
 
 from dcim.choices import *
 from dcim.constants import *
@@ -64,7 +65,7 @@ class Rack(PrimaryModel, WeightMixin):
         blank=True,
         null=True,
         verbose_name='Facility ID',
-        help_text='Locally-assigned identifier'
+        help_text=_('Locally-assigned identifier')
     )
     site = models.ForeignKey(
         to='dcim.Site',
@@ -96,7 +97,7 @@ class Rack(PrimaryModel, WeightMixin):
         related_name='racks',
         blank=True,
         null=True,
-        help_text='Functional role'
+        help_text=_('Functional role')
     )
     serial = models.CharField(
         max_length=50,
@@ -109,7 +110,7 @@ class Rack(PrimaryModel, WeightMixin):
         null=True,
         unique=True,
         verbose_name='Asset tag',
-        help_text='A unique tag used to identify this rack'
+        help_text=_('A unique tag used to identify this rack')
     )
     type = models.CharField(
         choices=RackTypeChoices,
@@ -121,28 +122,28 @@ class Rack(PrimaryModel, WeightMixin):
         choices=RackWidthChoices,
         default=RackWidthChoices.WIDTH_19IN,
         verbose_name='Width',
-        help_text='Rail-to-rail width'
+        help_text=_('Rail-to-rail width')
     )
     u_height = models.PositiveSmallIntegerField(
         default=RACK_U_HEIGHT_DEFAULT,
         verbose_name='Height (U)',
         validators=[MinValueValidator(1), MaxValueValidator(100)],
-        help_text='Height in rack units'
+        help_text=_('Height in rack units')
     )
     desc_units = models.BooleanField(
         default=False,
         verbose_name='Descending units',
-        help_text='Units are numbered top-to-bottom'
+        help_text=_('Units are numbered top-to-bottom')
     )
     outer_width = models.PositiveSmallIntegerField(
         blank=True,
         null=True,
-        help_text='Outer dimension of rack (width)'
+        help_text=_('Outer dimension of rack (width)')
     )
     outer_depth = models.PositiveSmallIntegerField(
         blank=True,
         null=True,
-        help_text='Outer dimension of rack (depth)'
+        help_text=_('Outer dimension of rack (depth)')
     )
     outer_unit = models.CharField(
         max_length=50,
@@ -153,8 +154,8 @@ class Rack(PrimaryModel, WeightMixin):
         blank=True,
         null=True,
         help_text=(
-            'Maximum depth of a mounted device, in millimeters. For four-post racks, this is the '
-            'distance between the front and rear rails.'
+            _('Maximum depth of a mounted device, in millimeters. For four-post racks, this is the '
+              'distance between the front and rear rails.')
         )
     )
 

+ 4 - 3
netbox/dcim/models/sites.py

@@ -2,6 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.urls import reverse
+from django.utils.translation import gettext as _
 from timezone_field import TimeZoneField
 
 from dcim.choices import *
@@ -178,7 +179,7 @@ class Site(PrimaryModel):
     facility = models.CharField(
         max_length=50,
         blank=True,
-        help_text='Local facility ID or description'
+        help_text=_('Local facility ID or description')
     )
     asns = models.ManyToManyField(
         to='ipam.ASN',
@@ -201,14 +202,14 @@ class Site(PrimaryModel):
         decimal_places=6,
         blank=True,
         null=True,
-        help_text='GPS coordinate (latitude)'
+        help_text=_('GPS coordinate (latitude)')
     )
     longitude = models.DecimalField(
         max_digits=9,
         decimal_places=6,
         blank=True,
         null=True,
-        help_text='GPS coordinate (longitude)'
+        help_text=_('GPS coordinate (longitude)')
     )
 
     # Generic relations

+ 43 - 42
netbox/extras/filtersets.py

@@ -2,6 +2,7 @@ import django_filters
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
+from django.utils.translation import gettext as _
 
 from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
@@ -32,7 +33,7 @@ __all__ = (
 class WebhookFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     content_type_id = MultiValueNumberFilter(
         field_name='content_types__id'
@@ -61,7 +62,7 @@ class WebhookFilterSet(BaseFilterSet):
 class CustomFieldFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     type = django_filters.MultipleChoiceFilter(
         choices=CustomFieldTypeChoices
@@ -92,7 +93,7 @@ class CustomFieldFilterSet(BaseFilterSet):
 class CustomLinkFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     content_type_id = MultiValueNumberFilter(
         field_name='content_types__id'
@@ -119,7 +120,7 @@ class CustomLinkFilterSet(BaseFilterSet):
 class ExportTemplateFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     content_type_id = MultiValueNumberFilter(
         field_name='content_types__id'
@@ -142,7 +143,7 @@ class ExportTemplateFilterSet(BaseFilterSet):
 class SavedFilterFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     content_type_id = MultiValueNumberFilter(
         field_name='content_types__id'
@@ -150,13 +151,13 @@ class SavedFilterFilterSet(BaseFilterSet):
     content_types = ContentTypeFilter()
     user_id = django_filters.ModelMultipleChoiceFilter(
         queryset=User.objects.all(),
-        label='User (ID)',
+        label=_('User (ID)'),
     )
     user = django_filters.ModelMultipleChoiceFilter(
         field_name='user__username',
         queryset=User.objects.all(),
         to_field_name='username',
-        label='User (name)',
+        label=_('User (name)'),
     )
     usable = django_filters.BooleanFilter(
         method='_usable'
@@ -191,7 +192,7 @@ class SavedFilterFilterSet(BaseFilterSet):
 class ImageAttachmentFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     created = django_filters.DateTimeFilter()
     content_type = ContentTypeFilter()
@@ -211,13 +212,13 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
     assigned_object_type = ContentTypeFilter()
     created_by_id = django_filters.ModelMultipleChoiceFilter(
         queryset=User.objects.all(),
-        label='User (ID)',
+        label=_('User (ID)'),
     )
     created_by = django_filters.ModelMultipleChoiceFilter(
         field_name='created_by__username',
         queryset=User.objects.all(),
         to_field_name='username',
-        label='User (name)',
+        label=_('User (name)'),
     )
     kind = django_filters.MultipleChoiceFilter(
         choices=JournalEntryKindChoices
@@ -236,7 +237,7 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
 class TagFilterSet(ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     content_type = MultiValueCharFilter(
         method='_content_type'
@@ -288,138 +289,138 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
 class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     region_id = django_filters.ModelMultipleChoiceFilter(
         field_name='regions',
         queryset=Region.objects.all(),
-        label='Region',
+        label=_('Region'),
     )
     region = django_filters.ModelMultipleChoiceFilter(
         field_name='regions__slug',
         queryset=Region.objects.all(),
         to_field_name='slug',
-        label='Region (slug)',
+        label=_('Region (slug)'),
     )
     site_group = django_filters.ModelMultipleChoiceFilter(
         field_name='site_groups__slug',
         queryset=SiteGroup.objects.all(),
         to_field_name='slug',
-        label='Site group (slug)',
+        label=_('Site group (slug)'),
     )
     site_group_id = django_filters.ModelMultipleChoiceFilter(
         field_name='site_groups',
         queryset=SiteGroup.objects.all(),
-        label='Site group',
+        label=_('Site group'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         field_name='sites',
         queryset=Site.objects.all(),
-        label='Site',
+        label=_('Site'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='sites__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
     location_id = django_filters.ModelMultipleChoiceFilter(
         field_name='locations',
         queryset=Location.objects.all(),
-        label='Location',
+        label=_('Location'),
     )
     location = django_filters.ModelMultipleChoiceFilter(
         field_name='locations__slug',
         queryset=Location.objects.all(),
         to_field_name='slug',
-        label='Location (slug)',
+        label=_('Location (slug)'),
     )
     device_type_id = django_filters.ModelMultipleChoiceFilter(
         field_name='device_types',
         queryset=DeviceType.objects.all(),
-        label='Device type',
+        label=_('Device type'),
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
         field_name='roles',
         queryset=DeviceRole.objects.all(),
-        label='Role',
+        label=_('Role'),
     )
     role = django_filters.ModelMultipleChoiceFilter(
         field_name='roles__slug',
         queryset=DeviceRole.objects.all(),
         to_field_name='slug',
-        label='Role (slug)',
+        label=_('Role (slug)'),
     )
     platform_id = django_filters.ModelMultipleChoiceFilter(
         field_name='platforms',
         queryset=Platform.objects.all(),
-        label='Platform',
+        label=_('Platform'),
     )
     platform = django_filters.ModelMultipleChoiceFilter(
         field_name='platforms__slug',
         queryset=Platform.objects.all(),
         to_field_name='slug',
-        label='Platform (slug)',
+        label=_('Platform (slug)'),
     )
     cluster_type_id = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster_types',
         queryset=ClusterType.objects.all(),
-        label='Cluster type',
+        label=_('Cluster type'),
     )
     cluster_type = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster_types__slug',
         queryset=ClusterType.objects.all(),
         to_field_name='slug',
-        label='Cluster type (slug)',
+        label=_('Cluster type (slug)'),
     )
     cluster_group_id = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster_groups',
         queryset=ClusterGroup.objects.all(),
-        label='Cluster group',
+        label=_('Cluster group'),
     )
     cluster_group = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster_groups__slug',
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
-        label='Cluster group (slug)',
+        label=_('Cluster group (slug)'),
     )
     cluster_id = django_filters.ModelMultipleChoiceFilter(
         field_name='clusters',
         queryset=Cluster.objects.all(),
-        label='Cluster',
+        label=_('Cluster'),
     )
     tenant_group_id = django_filters.ModelMultipleChoiceFilter(
         field_name='tenant_groups',
         queryset=TenantGroup.objects.all(),
-        label='Tenant group',
+        label=_('Tenant group'),
     )
     tenant_group = django_filters.ModelMultipleChoiceFilter(
         field_name='tenant_groups__slug',
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',
-        label='Tenant group (slug)',
+        label=_('Tenant group (slug)'),
     )
     tenant_id = django_filters.ModelMultipleChoiceFilter(
         field_name='tenants',
         queryset=Tenant.objects.all(),
-        label='Tenant',
+        label=_('Tenant'),
     )
     tenant = django_filters.ModelMultipleChoiceFilter(
         field_name='tenants__slug',
         queryset=Tenant.objects.all(),
         to_field_name='slug',
-        label='Tenant (slug)',
+        label=_('Tenant (slug)'),
     )
     tag_id = django_filters.ModelMultipleChoiceFilter(
         field_name='tags',
         queryset=Tag.objects.all(),
-        label='Tag',
+        label=_('Tag'),
     )
     tag = django_filters.ModelMultipleChoiceFilter(
         field_name='tags__slug',
         queryset=Tag.objects.all(),
         to_field_name='slug',
-        label='Tag (slug)',
+        label=_('Tag (slug)'),
     )
 
     class Meta:
@@ -443,7 +444,7 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
 class LocalConfigContextFilterSet(django_filters.FilterSet):
     local_context_data = django_filters.BooleanFilter(
         method='_local_context_data',
-        label='Has local config context data',
+        label=_('Has local config context data'),
     )
 
     def _local_context_data(self, queryset, name, value):
@@ -453,19 +454,19 @@ class LocalConfigContextFilterSet(django_filters.FilterSet):
 class ObjectChangeFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     time = django_filters.DateTimeFromToRangeFilter()
     changed_object_type = ContentTypeFilter()
     user_id = django_filters.ModelMultipleChoiceFilter(
         queryset=User.objects.all(),
-        label='User (ID)',
+        label=_('User (ID)'),
     )
     user = django_filters.ModelMultipleChoiceFilter(
         field_name='user__username',
         queryset=User.objects.all(),
         to_field_name='username',
-        label='User name',
+        label=_('User name'),
     )
 
     class Meta:
@@ -491,7 +492,7 @@ class ObjectChangeFilterSet(BaseFilterSet):
 class JobResultFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     created = django_filters.DateTimeFilter()
     created__before = django_filters.DateTimeFilter(
@@ -547,7 +548,7 @@ class JobResultFilterSet(BaseFilterSet):
 class ContentTypeFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
 
     class Meta:

+ 6 - 5
netbox/extras/forms/bulk_edit.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from extras.choices import *
 from extras.models import *
@@ -37,7 +38,7 @@ class CustomFieldBulkEditForm(BulkEditForm):
         required=False
     )
     ui_visibility = forms.ChoiceField(
-        label="UI visibility",
+        label=_("UI visibility"),
         choices=add_blank_choice(CustomFieldVisibilityChoices),
         required=False,
         initial='',
@@ -143,23 +144,23 @@ class WebhookBulkEditForm(BulkEditForm):
     http_method = forms.ChoiceField(
         choices=add_blank_choice(WebhookHttpMethodChoices),
         required=False,
-        label='HTTP method'
+        label=_('HTTP method')
     )
     payload_url = forms.CharField(
         required=False,
-        label='Payload URL'
+        label=_('Payload URL')
     )
     ssl_verification = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect(),
-        label='SSL verification'
+        label=_('SSL verification')
     )
     secret = forms.CharField(
         required=False
     )
     ca_file_path = forms.CharField(
         required=False,
-        label='CA file path'
+        label=_('CA file path')
     )
 
     nullable_fields = ('secret', 'conditions', 'ca_file_path')

+ 11 - 10
netbox/extras/forms/bulk_import.py

@@ -2,6 +2,7 @@ from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.forms import SimpleArrayField
 from django.utils.safestring import mark_safe
+from django.utils.translation import gettext as _
 
 from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices
 from extras.models import *
@@ -22,26 +23,26 @@ class CustomFieldCSVForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
-        help_text="One or more assigned object types"
+        help_text=_("One or more assigned object types")
     )
     type = CSVChoiceField(
         choices=CustomFieldTypeChoices,
-        help_text='Field data type (e.g. text, integer, etc.)'
+        help_text=_('Field data type (e.g. text, integer, etc.)')
     )
     object_type = CSVContentTypeField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
         required=False,
-        help_text="Object type (for object or multi-object fields)"
+        help_text=_("Object type (for object or multi-object fields)")
     )
     choices = SimpleArrayField(
         base_field=forms.CharField(),
         required=False,
-        help_text='Comma-separated list of field choices'
+        help_text=_('Comma-separated list of field choices')
     )
     ui_visibility = CSVChoiceField(
         choices=CustomFieldVisibilityChoices,
-        help_text='How the custom field is displayed in the user interface'
+        help_text=_('How the custom field is displayed in the user interface')
     )
 
     class Meta:
@@ -57,7 +58,7 @@ class CustomLinkCSVForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_links'),
-        help_text="One or more assigned object types"
+        help_text=_("One or more assigned object types")
     )
 
     class Meta:
@@ -72,7 +73,7 @@ class ExportTemplateCSVForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('export_templates'),
-        help_text="One or more assigned object types"
+        help_text=_("One or more assigned object types")
     )
 
     class Meta:
@@ -85,7 +86,7 @@ class ExportTemplateCSVForm(CSVModelForm):
 class SavedFilterCSVForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
         queryset=ContentType.objects.all(),
-        help_text="One or more assigned object types"
+        help_text=_("One or more assigned object types")
     )
 
     class Meta:
@@ -99,7 +100,7 @@ class WebhookCSVForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('webhooks'),
-        help_text="One or more assigned object types"
+        help_text=_("One or more assigned object types")
     )
 
     class Meta:
@@ -118,5 +119,5 @@ class TagCSVForm(CSVModelForm):
         model = Tag
         fields = ('name', 'slug', 'color', 'description')
         help_texts = {
-            'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
+            'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
         }

+ 2 - 2
netbox/extras/forms/filtersets.py

@@ -41,7 +41,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
         required=False,
-        label='Object type'
+        label=_('Object type')
     )
     type = MultipleChoiceField(
         choices=CustomFieldTypeChoices,
@@ -209,7 +209,7 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('webhooks'),
         required=False,
-        label='Object type'
+        label=_('Object type')
     )
     http_method = MultipleChoiceField(
         choices=WebhookHttpMethodChoices,

+ 2 - 1
netbox/extras/forms/mixins.py

@@ -1,5 +1,6 @@
 from django.contrib.contenttypes.models import ContentType
 from django import forms
+from django.utils.translation import gettext as _
 
 from extras.models import *
 from extras.choices import CustomFieldVisibilityChoices
@@ -66,7 +67,7 @@ class SavedFiltersMixin(forms.Form):
     filter = DynamicModelMultipleChoiceField(
         queryset=SavedFilter.objects.all(),
         required=False,
-        label='Saved Filter',
+        label=_('Saved Filter'),
         query_params={
             'usable': True,
         }

+ 8 - 7
netbox/extras/forms/model_forms.py

@@ -1,6 +1,7 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.http import QueryDict
+from django.utils.translation import gettext as _
 
 from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 from extras.choices import *
@@ -31,14 +32,14 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
     content_types = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
-        label='Model(s)'
+        label=_('Model(s)')
     )
     object_type = ContentTypeChoiceField(
         queryset=ContentType.objects.all(),
         # TODO: Come up with a canonical way to register suitable models
         limit_choices_to=FeatureQuery('webhooks'),
         required=False,
-        help_text="Type of the related object (for object/multi-object fields only)"
+        help_text=_("Type of the related object (for object/multi-object fields only)")
     )
 
     fieldsets = (
@@ -54,8 +55,8 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
         model = CustomField
         fields = '__all__'
         help_texts = {
-            'type': "The type of data stored in this field. For object/multi-object fields, select the related object "
-                    "type below."
+            'type': _("The type of data stored in this field. For object/multi-object fields, select the related object "
+                      "type below.")
         }
         widgets = {
             'type': StaticSelect(),
@@ -84,9 +85,9 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
             'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
         }
         help_texts = {
-            'link_text': 'Jinja2 template code for the link text. Reference the object as <code>{{ object }}</code>. '
-                         'Links which render as empty text will not be displayed.',
-            'link_url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ object }}</code>.',
+            'link_text': _('Jinja2 template code for the link text. Reference the object as <code>{{ object }}</code>. '
+                           'Links which render as empty text will not be displayed.'),
+            'link_url': _('Jinja2 template code for the link URL. Reference the object as <code>{{ object }}</code>.'),
         }
 
 

+ 3 - 2
netbox/extras/forms/reports.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from utilities.forms import BootstrapMixin, DateTimePicker
 
@@ -11,6 +12,6 @@ class ReportForm(BootstrapMixin, forms.Form):
     schedule_at = forms.DateTimeField(
         required=False,
         widget=DateTimePicker(),
-        label="Schedule at",
-        help_text="Schedule execution of report to a set time",
+        label=_("Schedule at"),
+        help_text=_("Schedule execution of report to a set time"),
     )

+ 5 - 4
netbox/extras/forms/scripts.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from utilities.forms import BootstrapMixin, DateTimePicker
 
@@ -11,14 +12,14 @@ class ScriptForm(BootstrapMixin, forms.Form):
     _commit = forms.BooleanField(
         required=False,
         initial=True,
-        label="Commit changes",
-        help_text="Commit changes to the database (uncheck for a dry-run)"
+        label=_("Commit changes"),
+        help_text=_("Commit changes to the database (uncheck for a dry-run)")
     )
     _schedule_at = forms.DateTimeField(
         required=False,
         widget=DateTimePicker(),
-        label="Schedule at",
-        help_text="Schedule execution of script to a set time",
+        label=_("Schedule at"),
+        help_text=_("Schedule execution of script to a set time"),
     )
 
     def __init__(self, *args, **kwargs):

+ 23 - 22
netbox/extras/models/customfields.py

@@ -11,6 +11,7 @@ from django.db import models
 from django.urls import reverse
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
+from django.utils.translation import gettext as _
 
 from extras.choices import *
 from extras.utils import FeatureQuery
@@ -57,25 +58,25 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
         to=ContentType,
         related_name='custom_fields',
         limit_choices_to=FeatureQuery('custom_fields'),
-        help_text='The object(s) to which this field applies.'
+        help_text=_('The object(s) to which this field applies.')
     )
     type = models.CharField(
         max_length=50,
         choices=CustomFieldTypeChoices,
         default=CustomFieldTypeChoices.TYPE_TEXT,
-        help_text='The type of data this custom field holds'
+        help_text=_('The type of data this custom field holds')
     )
     object_type = models.ForeignKey(
         to=ContentType,
         on_delete=models.PROTECT,
         blank=True,
         null=True,
-        help_text='The type of NetBox object this field maps to (for object fields)'
+        help_text=_('The type of NetBox object this field maps to (for object fields)')
     )
     name = models.CharField(
         max_length=50,
         unique=True,
-        help_text='Internal field name',
+        help_text=_('Internal field name'),
         validators=(
             RegexValidator(
                 regex=r'^[a-z0-9_]+$',
@@ -87,13 +88,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
     label = models.CharField(
         max_length=50,
         blank=True,
-        help_text='Name of the field as displayed to users (if not provided, '
-                  'the field\'s name will be used)'
+        help_text=_('Name of the field as displayed to users (if not provided, '
+                    'the field\'s name will be used)')
     )
     group_name = models.CharField(
         max_length=50,
         blank=True,
-        help_text="Custom fields within the same group will be displayed together"
+        help_text=_("Custom fields within the same group will be displayed together")
     )
     description = models.CharField(
         max_length=200,
@@ -101,64 +102,64 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
     )
     required = models.BooleanField(
         default=False,
-        help_text='If true, this field is required when creating new objects '
-                  'or editing an existing object.'
+        help_text=_('If true, this field is required when creating new objects '
+                    'or editing an existing object.')
     )
     search_weight = models.PositiveSmallIntegerField(
         default=1000,
-        help_text='Weighting for search. Lower values are considered more important. '
-                  'Fields with a search weight of zero will be ignored.'
+        help_text=_('Weighting for search. Lower values are considered more important. '
+                    'Fields with a search weight of zero will be ignored.')
     )
     filter_logic = models.CharField(
         max_length=50,
         choices=CustomFieldFilterLogicChoices,
         default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
-        help_text='Loose matches any instance of a given string; exact '
-                  'matches the entire field.'
+        help_text=_('Loose matches any instance of a given string; exact '
+                    'matches the entire field.')
     )
     default = models.JSONField(
         blank=True,
         null=True,
-        help_text='Default value for the field (must be a JSON value). Encapsulate '
-                  'strings with double quotes (e.g. "Foo").'
+        help_text=_('Default value for the field (must be a JSON value). Encapsulate '
+                    'strings with double quotes (e.g. "Foo").')
     )
     weight = models.PositiveSmallIntegerField(
         default=100,
         verbose_name='Display weight',
-        help_text='Fields with higher weights appear lower in a form.'
+        help_text=_('Fields with higher weights appear lower in a form.')
     )
     validation_minimum = models.IntegerField(
         blank=True,
         null=True,
         verbose_name='Minimum value',
-        help_text='Minimum allowed value (for numeric fields)'
+        help_text=_('Minimum allowed value (for numeric fields)')
     )
     validation_maximum = models.IntegerField(
         blank=True,
         null=True,
         verbose_name='Maximum value',
-        help_text='Maximum allowed value (for numeric fields)'
+        help_text=_('Maximum allowed value (for numeric fields)')
     )
     validation_regex = models.CharField(
         blank=True,
         validators=[validate_regex],
         max_length=500,
         verbose_name='Validation regex',
-        help_text='Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. '
-                  'For example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
+        help_text=_('Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. '
+                    'For example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.')
     )
     choices = ArrayField(
         base_field=models.CharField(max_length=100),
         blank=True,
         null=True,
-        help_text='Comma-separated list of available choices (for selection fields)'
+        help_text=_('Comma-separated list of available choices (for selection fields)')
     )
     ui_visibility = models.CharField(
         max_length=50,
         choices=CustomFieldVisibilityChoices,
         default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
         verbose_name='UI visibility',
-        help_text='Specifies the visibility of custom field in the UI'
+        help_text=_('Specifies the visibility of custom field in the UI')
     )
 
     objects = CustomFieldManager()

+ 37 - 36
netbox/extras/models/models.py

@@ -12,6 +12,7 @@ from django.http import HttpResponse, QueryDict
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.formats import date_format
+from django.utils.translation import gettext as _
 from rest_framework.utils.encoders import JSONEncoder
 import django_rq
 
@@ -51,7 +52,7 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
         related_name='webhooks',
         verbose_name='Object types',
         limit_choices_to=FeatureQuery('webhooks'),
-        help_text="The object(s) to which this Webhook applies."
+        help_text=_("The object(s) to which this Webhook applies.")
     )
     name = models.CharField(
         max_length=150,
@@ -59,21 +60,21 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
     )
     type_create = models.BooleanField(
         default=False,
-        help_text="Call this webhook when a matching object is created."
+        help_text=_("Call this webhook when a matching object is created.")
     )
     type_update = models.BooleanField(
         default=False,
-        help_text="Call this webhook when a matching object is updated."
+        help_text=_("Call this webhook when a matching object is updated.")
     )
     type_delete = models.BooleanField(
         default=False,
-        help_text="Call this webhook when a matching object is deleted."
+        help_text=_("Call this webhook when a matching object is deleted.")
     )
     payload_url = models.CharField(
         max_length=500,
         verbose_name='URL',
-        help_text='This URL will be called using the HTTP method defined when the webhook is called. '
-                  'Jinja2 template processing is supported with the same context as the request body.'
+        help_text=_('This URL will be called using the HTTP method defined when the webhook is called. '
+                    'Jinja2 template processing is supported with the same context as the request body.')
     )
     enabled = models.BooleanField(
         default=True
@@ -88,46 +89,46 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
         max_length=100,
         default=HTTP_CONTENT_TYPE_JSON,
         verbose_name='HTTP content type',
-        help_text='The complete list of official content types is available '
-                  '<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.'
+        help_text=_('The complete list of official content types is available '
+                    '<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.')
     )
     additional_headers = models.TextField(
         blank=True,
-        help_text="User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. "
-                  "Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is "
-                  "supported with the same context as the request body (below)."
+        help_text=_("User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. "
+                    "Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is "
+                    "supported with the same context as the request body (below).")
     )
     body_template = models.TextField(
         blank=True,
-        help_text='Jinja2 template for a custom request body. If blank, a JSON object representing the change will be '
-                  'included. Available context data includes: <code>event</code>, <code>model</code>, '
-                  '<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>.'
+        help_text=_('Jinja2 template for a custom request body. If blank, a JSON object representing the change will be '
+                    'included. Available context data includes: <code>event</code>, <code>model</code>, '
+                    '<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>.')
     )
     secret = models.CharField(
         max_length=255,
         blank=True,
-        help_text="When provided, the request will include a 'X-Hook-Signature' "
-                  "header containing a HMAC hex digest of the payload body using "
-                  "the secret as the key. The secret is not transmitted in "
-                  "the request."
+        help_text=_("When provided, the request will include a 'X-Hook-Signature' "
+                    "header containing a HMAC hex digest of the payload body using "
+                    "the secret as the key. The secret is not transmitted in "
+                    "the request.")
     )
     conditions = models.JSONField(
         blank=True,
         null=True,
-        help_text="A set of conditions which determine whether the webhook will be generated."
+        help_text=_("A set of conditions which determine whether the webhook will be generated.")
     )
     ssl_verification = models.BooleanField(
         default=True,
         verbose_name='SSL verification',
-        help_text="Enable SSL certificate verification. Disable with caution!"
+        help_text=_("Enable SSL certificate verification. Disable with caution!")
     )
     ca_file_path = models.CharField(
         max_length=4096,
         null=True,
         blank=True,
         verbose_name='CA File Path',
-        help_text='The specific CA certificate file to use for SSL verification. '
-                  'Leave blank to use the system defaults.'
+        help_text=_('The specific CA certificate file to use for SSL verification. '
+                    'Leave blank to use the system defaults.')
     )
 
     class Meta:
@@ -201,7 +202,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
     content_types = models.ManyToManyField(
         to=ContentType,
         related_name='custom_links',
-        help_text='The object type(s) to which this link applies.'
+        help_text=_('The object type(s) to which this link applies.')
     )
     name = models.CharField(
         max_length=100,
@@ -211,11 +212,11 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
         default=True
     )
     link_text = models.TextField(
-        help_text="Jinja2 template code for link text"
+        help_text=_("Jinja2 template code for link text")
     )
     link_url = models.TextField(
         verbose_name='Link URL',
-        help_text="Jinja2 template code for link URL"
+        help_text=_("Jinja2 template code for link URL")
     )
     weight = models.PositiveSmallIntegerField(
         default=100
@@ -223,17 +224,17 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
     group_name = models.CharField(
         max_length=50,
         blank=True,
-        help_text="Links with the same group will appear as a dropdown menu"
+        help_text=_("Links with the same group will appear as a dropdown menu")
     )
     button_class = models.CharField(
         max_length=30,
         choices=CustomLinkButtonClassChoices,
         default=CustomLinkButtonClassChoices.DEFAULT,
-        help_text="The class of the first link in a group will be used for the dropdown button"
+        help_text=_("The class of the first link in a group will be used for the dropdown button")
     )
     new_window = models.BooleanField(
         default=False,
-        help_text="Force link to open in a new window"
+        help_text=_("Force link to open in a new window")
     )
 
     clone_fields = (
@@ -272,7 +273,7 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
     content_types = models.ManyToManyField(
         to=ContentType,
         related_name='export_templates',
-        help_text='The object type(s) to which this template applies.'
+        help_text=_('The object type(s) to which this template applies.')
     )
     name = models.CharField(
         max_length=100
@@ -282,23 +283,23 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
         blank=True
     )
     template_code = models.TextField(
-        help_text='Jinja2 template code. The list of objects being exported is passed as a context variable named '
-                  '<code>queryset</code>.'
+        help_text=_('Jinja2 template code. The list of objects being exported is passed as a context variable named '
+                    '<code>queryset</code>.')
     )
     mime_type = models.CharField(
         max_length=50,
         blank=True,
         verbose_name='MIME type',
-        help_text='Defaults to <code>text/plain</code>'
+        help_text=_('Defaults to <code>text/plain</code>')
     )
     file_extension = models.CharField(
         max_length=15,
         blank=True,
-        help_text='Extension to append to the rendered filename'
+        help_text=_('Extension to append to the rendered filename')
     )
     as_attachment = models.BooleanField(
         default=True,
-        help_text="Download file as attachment"
+        help_text=_("Download file as attachment")
     )
 
     class Meta:
@@ -358,7 +359,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
     content_types = models.ManyToManyField(
         to=ContentType,
         related_name='saved_filters',
-        help_text='The object type(s) to which this filter applies.'
+        help_text=_('The object type(s) to which this filter applies.')
     )
     name = models.CharField(
         max_length=100,
@@ -553,7 +554,7 @@ class JobResult(models.Model):
         related_name='job_results',
         verbose_name='Object types',
         limit_choices_to=FeatureQuery('job_results'),
-        help_text="The object type to which this job result applies",
+        help_text=_("The object type to which this job result applies"),
         on_delete=models.CASCADE,
     )
     created = models.DateTimeField(

+ 2 - 1
netbox/extras/tests/dummy_plugin/navigation.py

@@ -1,3 +1,4 @@
+from django.utils.translation import gettext as _
 from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
 
 
@@ -25,7 +26,7 @@ items = (
 )
 
 menu = PluginMenu(
-    label='Dummy',
+    label=_('Dummy'),
     groups=(('Group 1', items),),
 )
 menu_items = items

+ 96 - 95
netbox/ipam/filtersets.py

@@ -3,6 +3,7 @@ import netaddr
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.db.models import Q
+from django.utils.translation import gettext as _
 from netaddr.core import AddrFormatError
 
 from dcim.models import Device, Interface, Region, Site, SiteGroup
@@ -41,24 +42,24 @@ class VRFFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
     import_target_id = django_filters.ModelMultipleChoiceFilter(
         field_name='import_targets',
         queryset=RouteTarget.objects.all(),
-        label='Import target',
+        label=_('Import target'),
     )
     import_target = django_filters.ModelMultipleChoiceFilter(
         field_name='import_targets__name',
         queryset=RouteTarget.objects.all(),
         to_field_name='name',
-        label='Import target (name)',
+        label=_('Import target (name)'),
     )
     export_target_id = django_filters.ModelMultipleChoiceFilter(
         field_name='export_targets',
         queryset=RouteTarget.objects.all(),
-        label='Export target',
+        label=_('Export target'),
     )
     export_target = django_filters.ModelMultipleChoiceFilter(
         field_name='export_targets__name',
         queryset=RouteTarget.objects.all(),
         to_field_name='name',
-        label='Export target (name)',
+        label=_('Export target (name)'),
     )
 
     def search(self, queryset, name, value):
@@ -79,24 +80,24 @@ class RouteTargetFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
     importing_vrf_id = django_filters.ModelMultipleChoiceFilter(
         field_name='importing_vrfs',
         queryset=VRF.objects.all(),
-        label='Importing VRF',
+        label=_('Importing VRF'),
     )
     importing_vrf = django_filters.ModelMultipleChoiceFilter(
         field_name='importing_vrfs__rd',
         queryset=VRF.objects.all(),
         to_field_name='rd',
-        label='Import VRF (RD)',
+        label=_('Import VRF (RD)'),
     )
     exporting_vrf_id = django_filters.ModelMultipleChoiceFilter(
         field_name='exporting_vrfs',
         queryset=VRF.objects.all(),
-        label='Exporting VRF',
+        label=_('Exporting VRF'),
     )
     exporting_vrf = django_filters.ModelMultipleChoiceFilter(
         field_name='exporting_vrfs__rd',
         queryset=VRF.objects.all(),
         to_field_name='rd',
-        label='Export VRF (RD)',
+        label=_('Export VRF (RD)'),
     )
 
     def search(self, queryset, name, value):
@@ -126,17 +127,17 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
     )
     prefix = django_filters.CharFilter(
         method='filter_prefix',
-        label='Prefix',
+        label=_('Prefix'),
     )
     rir_id = django_filters.ModelMultipleChoiceFilter(
         queryset=RIR.objects.all(),
-        label='RIR (ID)',
+        label=_('RIR (ID)'),
     )
     rir = django_filters.ModelMultipleChoiceFilter(
         field_name='rir__slug',
         queryset=RIR.objects.all(),
         to_field_name='slug',
-        label='RIR (slug)',
+        label=_('RIR (slug)'),
     )
 
     class Meta:
@@ -169,24 +170,24 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
 class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
     rir_id = django_filters.ModelMultipleChoiceFilter(
         queryset=RIR.objects.all(),
-        label='RIR (ID)',
+        label=_('RIR (ID)'),
     )
     rir = django_filters.ModelMultipleChoiceFilter(
         field_name='rir__slug',
         queryset=RIR.objects.all(),
         to_field_name='slug',
-        label='RIR (slug)',
+        label=_('RIR (slug)'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         field_name='sites',
         queryset=Site.objects.all(),
-        label='Site (ID)',
+        label=_('Site (ID)'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='sites__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
 
     class Meta:
@@ -218,19 +219,19 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
     )
     prefix = MultiValueCharFilter(
         method='filter_prefix',
-        label='Prefix',
+        label=_('Prefix'),
     )
     within = django_filters.CharFilter(
         method='search_within',
-        label='Within prefix',
+        label=_('Within prefix'),
     )
     within_include = django_filters.CharFilter(
         method='search_within_include',
-        label='Within and including prefix',
+        label=_('Within and including prefix'),
     )
     contains = django_filters.CharFilter(
         method='search_contains',
-        label='Prefixes which contain this prefix or IP',
+        label=_('Prefixes which contain this prefix or IP'),
     )
     depth = MultiValueNumberFilter(
         field_name='_depth'
@@ -252,78 +253,78 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
     )
     vrf_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VRF.objects.all(),
-        label='VRF',
+        label=_('VRF'),
     )
     vrf = django_filters.ModelMultipleChoiceFilter(
         field_name='vrf__rd',
         queryset=VRF.objects.all(),
         to_field_name='rd',
-        label='VRF (RD)',
+        label=_('VRF (RD)'),
     )
     present_in_vrf_id = django_filters.ModelChoiceFilter(
         queryset=VRF.objects.all(),
         method='filter_present_in_vrf',
-        label='VRF'
+        label=_('VRF')
     )
     present_in_vrf = django_filters.ModelChoiceFilter(
         queryset=VRF.objects.all(),
         method='filter_present_in_vrf',
         to_field_name='rd',
-        label='VRF (RD)',
+        label=_('VRF (RD)'),
     )
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
         lookup_expr='in',
-        label='Region (ID)',
+        label=_('Region (ID)'),
     )
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
         lookup_expr='in',
         to_field_name='slug',
-        label='Region (slug)',
+        label=_('Region (slug)'),
     )
     site_group_id = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='site__group',
         lookup_expr='in',
-        label='Site group (ID)',
+        label=_('Site group (ID)'),
     )
     site_group = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='site__group',
         lookup_expr='in',
         to_field_name='slug',
-        label='Site group (slug)',
+        label=_('Site group (slug)'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
-        label='Site (ID)',
+        label=_('Site (ID)'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='site__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
     vlan_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VLAN.objects.all(),
-        label='VLAN (ID)',
+        label=_('VLAN (ID)'),
     )
     vlan_vid = django_filters.NumberFilter(
         field_name='vlan__vid',
-        label='VLAN number (1-4094)',
+        label=_('VLAN number (1-4094)'),
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Role.objects.all(),
-        label='Role (ID)',
+        label=_('Role (ID)'),
     )
     role = django_filters.ModelMultipleChoiceFilter(
         field_name='role__slug',
         queryset=Role.objects.all(),
         to_field_name='slug',
-        label='Role (slug)',
+        label=_('Role (slug)'),
     )
     status = django_filters.MultipleChoiceFilter(
         choices=PrefixStatusChoices,
@@ -406,27 +407,27 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
     )
     contains = django_filters.CharFilter(
         method='search_contains',
-        label='Ranges which contain this prefix or IP',
+        label=_('Ranges which contain this prefix or IP'),
     )
     vrf_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VRF.objects.all(),
-        label='VRF',
+        label=_('VRF'),
     )
     vrf = django_filters.ModelMultipleChoiceFilter(
         field_name='vrf__rd',
         queryset=VRF.objects.all(),
         to_field_name='rd',
-        label='VRF (RD)',
+        label=_('VRF (RD)'),
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Role.objects.all(),
-        label='Role (ID)',
+        label=_('Role (ID)'),
     )
     role = django_filters.ModelMultipleChoiceFilter(
         field_name='role__slug',
         queryset=Role.objects.all(),
         to_field_name='slug',
-        label='Role (slug)',
+        label=_('Role (slug)'),
     )
     status = django_filters.MultipleChoiceFilter(
         choices=IPRangeStatusChoices,
@@ -468,87 +469,87 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
     )
     parent = MultiValueCharFilter(
         method='search_by_parent',
-        label='Parent prefix',
+        label=_('Parent prefix'),
     )
     address = MultiValueCharFilter(
         method='filter_address',
-        label='Address',
+        label=_('Address'),
     )
     mask_length = django_filters.NumberFilter(
         method='filter_mask_length',
-        label='Mask length',
+        label=_('Mask length'),
     )
     vrf_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VRF.objects.all(),
-        label='VRF',
+        label=_('VRF'),
     )
     vrf = django_filters.ModelMultipleChoiceFilter(
         field_name='vrf__rd',
         queryset=VRF.objects.all(),
         to_field_name='rd',
-        label='VRF (RD)',
+        label=_('VRF (RD)'),
     )
     present_in_vrf_id = django_filters.ModelChoiceFilter(
         queryset=VRF.objects.all(),
         method='filter_present_in_vrf',
-        label='VRF'
+        label=_('VRF')
     )
     present_in_vrf = django_filters.ModelChoiceFilter(
         queryset=VRF.objects.all(),
         method='filter_present_in_vrf',
         to_field_name='rd',
-        label='VRF (RD)',
+        label=_('VRF (RD)'),
     )
     device = MultiValueCharFilter(
         method='filter_device',
         field_name='name',
-        label='Device (name)',
+        label=_('Device (name)'),
     )
     device_id = MultiValueNumberFilter(
         method='filter_device',
         field_name='pk',
-        label='Device (ID)',
+        label=_('Device (ID)'),
     )
     virtual_machine = MultiValueCharFilter(
         method='filter_virtual_machine',
         field_name='name',
-        label='Virtual machine (name)',
+        label=_('Virtual machine (name)'),
     )
     virtual_machine_id = MultiValueNumberFilter(
         method='filter_virtual_machine',
         field_name='pk',
-        label='Virtual machine (ID)',
+        label=_('Virtual machine (ID)'),
     )
     interface = django_filters.ModelMultipleChoiceFilter(
         field_name='interface__name',
         queryset=Interface.objects.all(),
         to_field_name='name',
-        label='Interface (name)',
+        label=_('Interface (name)'),
     )
     interface_id = django_filters.ModelMultipleChoiceFilter(
         field_name='interface',
         queryset=Interface.objects.all(),
-        label='Interface (ID)',
+        label=_('Interface (ID)'),
     )
     vminterface = django_filters.ModelMultipleChoiceFilter(
         field_name='vminterface__name',
         queryset=VMInterface.objects.all(),
         to_field_name='name',
-        label='VM interface (name)',
+        label=_('VM interface (name)'),
     )
     vminterface_id = django_filters.ModelMultipleChoiceFilter(
         field_name='vminterface',
         queryset=VMInterface.objects.all(),
-        label='VM interface (ID)',
+        label=_('VM interface (ID)'),
     )
     fhrpgroup_id = django_filters.ModelMultipleChoiceFilter(
         field_name='fhrpgroup',
         queryset=FHRPGroup.objects.all(),
-        label='FHRP group (ID)',
+        label=_('FHRP group (ID)'),
     )
     assigned_to_interface = django_filters.BooleanFilter(
         method='_assigned_to_interface',
-        label='Is assigned to an interface',
+        label=_('Is assigned to an interface'),
     )
     status = django_filters.MultipleChoiceFilter(
         choices=IPAddressStatusChoices,
@@ -688,27 +689,27 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
     interface_type = ContentTypeFilter()
     group_id = django_filters.ModelMultipleChoiceFilter(
         queryset=FHRPGroup.objects.all(),
-        label='Group (ID)',
+        label=_('Group (ID)'),
     )
     device = MultiValueCharFilter(
         method='filter_device',
         field_name='name',
-        label='Device (name)',
+        label=_('Device (name)'),
     )
     device_id = MultiValueNumberFilter(
         method='filter_device',
         field_name='pk',
-        label='Device (ID)',
+        label=_('Device (ID)'),
     )
     virtual_machine = MultiValueCharFilter(
         method='filter_virtual_machine',
         field_name='name',
-        label='Virtual machine (name)',
+        label=_('Virtual machine (name)'),
     )
     virtual_machine_id = MultiValueNumberFilter(
         method='filter_virtual_machine',
         field_name='pk',
-        label='Virtual machine (ID)',
+        label=_('Virtual machine (ID)'),
     )
 
     class Meta:
@@ -787,57 +788,57 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
         queryset=Region.objects.all(),
         field_name='site__region',
         lookup_expr='in',
-        label='Region (ID)',
+        label=_('Region (ID)'),
     )
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
         lookup_expr='in',
         to_field_name='slug',
-        label='Region (slug)',
+        label=_('Region (slug)'),
     )
     site_group_id = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='site__group',
         lookup_expr='in',
-        label='Site group (ID)',
+        label=_('Site group (ID)'),
     )
     site_group = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='site__group',
         lookup_expr='in',
         to_field_name='slug',
-        label='Site group (slug)',
+        label=_('Site group (slug)'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
-        label='Site (ID)',
+        label=_('Site (ID)'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='site__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
     group_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VLANGroup.objects.all(),
-        label='Group (ID)',
+        label=_('Group (ID)'),
     )
     group = django_filters.ModelMultipleChoiceFilter(
         field_name='group__slug',
         queryset=VLANGroup.objects.all(),
         to_field_name='slug',
-        label='Group',
+        label=_('Group'),
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Role.objects.all(),
-        label='Role (ID)',
+        label=_('Role (ID)'),
     )
     role = django_filters.ModelMultipleChoiceFilter(
         field_name='role__slug',
         queryset=Role.objects.all(),
         to_field_name='slug',
-        label='Role (slug)',
+        label=_('Role (slug)'),
     )
     status = django_filters.MultipleChoiceFilter(
         choices=VLANStatusChoices,
@@ -893,23 +894,23 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet):
 class ServiceFilterSet(NetBoxModelFilterSet):
     device_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Device.objects.all(),
-        label='Device (ID)',
+        label=_('Device (ID)'),
     )
     device = django_filters.ModelMultipleChoiceFilter(
         field_name='device__name',
         queryset=Device.objects.all(),
         to_field_name='name',
-        label='Device (name)',
+        label=_('Device (name)'),
     )
     virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VirtualMachine.objects.all(),
-        label='Virtual machine (ID)',
+        label=_('Virtual machine (ID)'),
     )
     virtual_machine = django_filters.ModelMultipleChoiceFilter(
         field_name='virtual_machine__name',
         queryset=VirtualMachine.objects.all(),
         to_field_name='name',
-        label='Virtual machine (name)',
+        label=_('Virtual machine (name)'),
     )
     port = NumericArrayFilter(
         field_name='ports',
@@ -939,24 +940,24 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
     import_target_id = django_filters.ModelMultipleChoiceFilter(
         field_name='import_targets',
         queryset=RouteTarget.objects.all(),
-        label='Import target',
+        label=_('Import target'),
     )
     import_target = django_filters.ModelMultipleChoiceFilter(
         field_name='import_targets__name',
         queryset=RouteTarget.objects.all(),
         to_field_name='name',
-        label='Import target (name)',
+        label=_('Import target (name)'),
     )
     export_target_id = django_filters.ModelMultipleChoiceFilter(
         field_name='export_targets',
         queryset=RouteTarget.objects.all(),
-        label='Export target',
+        label=_('Export target'),
     )
     export_target = django_filters.ModelMultipleChoiceFilter(
         field_name='export_targets__name',
         queryset=RouteTarget.objects.all(),
         to_field_name='name',
-        label='Export target (name)',
+        label=_('Export target (name)'),
     )
 
     class Meta:
@@ -977,92 +978,92 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
 class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
     l2vpn_id = django_filters.ModelMultipleChoiceFilter(
         queryset=L2VPN.objects.all(),
-        label='L2VPN (ID)',
+        label=_('L2VPN (ID)'),
     )
     l2vpn = django_filters.ModelMultipleChoiceFilter(
         field_name='l2vpn__slug',
         queryset=L2VPN.objects.all(),
         to_field_name='slug',
-        label='L2VPN (slug)',
+        label=_('L2VPN (slug)'),
     )
     region = MultiValueCharFilter(
         method='filter_region',
         field_name='slug',
-        label='Region (slug)',
+        label=_('Region (slug)'),
     )
     region_id = MultiValueNumberFilter(
         method='filter_region',
         field_name='pk',
-        label='Region (ID)',
+        label=_('Region (ID)'),
     )
     site = MultiValueCharFilter(
         method='filter_site',
         field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
     site_id = MultiValueNumberFilter(
         method='filter_site',
         field_name='pk',
-        label='Site (ID)',
+        label=_('Site (ID)'),
     )
     device = django_filters.ModelMultipleChoiceFilter(
         field_name='interface__device__name',
         queryset=Device.objects.all(),
         to_field_name='name',
-        label='Device (name)',
+        label=_('Device (name)'),
     )
     device_id = django_filters.ModelMultipleChoiceFilter(
         field_name='interface__device',
         queryset=Device.objects.all(),
-        label='Device (ID)',
+        label=_('Device (ID)'),
     )
     virtual_machine = django_filters.ModelMultipleChoiceFilter(
         field_name='vminterface__virtual_machine__name',
         queryset=VirtualMachine.objects.all(),
         to_field_name='name',
-        label='Virtual machine (name)',
+        label=_('Virtual machine (name)'),
     )
     virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
         field_name='vminterface__virtual_machine',
         queryset=VirtualMachine.objects.all(),
-        label='Virtual machine (ID)',
+        label=_('Virtual machine (ID)'),
     )
     interface = django_filters.ModelMultipleChoiceFilter(
         field_name='interface__name',
         queryset=Interface.objects.all(),
         to_field_name='name',
-        label='Interface (name)',
+        label=_('Interface (name)'),
     )
     interface_id = django_filters.ModelMultipleChoiceFilter(
         field_name='interface',
         queryset=Interface.objects.all(),
-        label='Interface (ID)',
+        label=_('Interface (ID)'),
     )
     vminterface = django_filters.ModelMultipleChoiceFilter(
         field_name='vminterface__name',
         queryset=VMInterface.objects.all(),
         to_field_name='name',
-        label='VM interface (name)',
+        label=_('VM interface (name)'),
     )
     vminterface_id = django_filters.ModelMultipleChoiceFilter(
         field_name='vminterface',
         queryset=VMInterface.objects.all(),
-        label='VM Interface (ID)',
+        label=_('VM Interface (ID)'),
     )
     vlan = django_filters.ModelMultipleChoiceFilter(
         field_name='vlan__name',
         queryset=VLAN.objects.all(),
         to_field_name='name',
-        label='VLAN (name)',
+        label=_('VLAN (name)'),
     )
     vlan_vid = django_filters.NumberFilter(
         field_name='vlan__vid',
-        label='VLAN number (1-4094)',
+        label=_('VLAN number (1-4094)'),
     )
     vlan_id = django_filters.ModelMultipleChoiceFilter(
         field_name='vlan',
         queryset=VLAN.objects.all(),
-        label='VLAN (ID)',
+        label=_('VLAN (ID)'),
     )
     assigned_object_type = ContentTypeFilter()
 

+ 2 - 1
netbox/ipam/forms/bulk_create.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from utilities.forms import BootstrapMixin, ExpandableIPAddressField
 
@@ -9,5 +10,5 @@ __all__ = (
 
 class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
     pattern = ExpandableIPAddressField(
-        label='Address pattern'
+        label=_('Address pattern')
     )

+ 15 - 14
netbox/ipam/forms/bulk_edit.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from dcim.models import Region, Site, SiteGroup
 from ipam.choices import *
@@ -40,7 +41,7 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
     enforce_unique = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect(),
-        label='Enforce unique space'
+        label=_('Enforce unique space')
     )
     description = forms.CharField(
         max_length=200,
@@ -104,7 +105,7 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         required=False,
-        label='RIR'
+        label=_('RIR')
     )
     tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
@@ -130,7 +131,7 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
         required=False,
-        label='RIR'
+        label=_('RIR')
     )
     tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
@@ -191,7 +192,7 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     prefix_length = forms.IntegerField(
         min_value=PREFIX_LENGTH_MIN,
@@ -214,12 +215,12 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
     is_pool = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect(),
-        label='Is a pool'
+        label=_('Is a pool')
     )
     mark_utilized = forms.NullBooleanField(
         required=False,
         widget=BulkEditNullBooleanSelect(),
-        label='Treat as 100% utilized'
+        label=_('Treat as 100% utilized')
     )
     description = forms.CharField(
         max_length=200,
@@ -245,7 +246,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
@@ -282,7 +283,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     mask_length = forms.IntegerField(
         min_value=IPADDRESS_MASK_LENGTH_MIN,
@@ -306,7 +307,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
     dns_name = forms.CharField(
         max_length=255,
         required=False,
-        label='DNS name'
+        label=_('DNS name')
     )
     description = forms.CharField(
         max_length=200,
@@ -336,18 +337,18 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
     group_id = forms.IntegerField(
         min_value=0,
         required=False,
-        label='Group ID'
+        label=_('Group ID')
     )
     auth_type = forms.ChoiceField(
         choices=add_blank_choice(FHRPGroupAuthTypeChoices),
         required=False,
         widget=StaticSelect(),
-        label='Authentication type'
+        label=_('Authentication type')
     )
     auth_key = forms.CharField(
         max_length=255,
         required=False,
-        label='Authentication key'
+        label=_('Authentication key')
     )
     name = forms.CharField(
         max_length=100,
@@ -379,13 +380,13 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
         min_value=VLAN_VID_MIN,
         max_value=VLAN_VID_MAX,
         required=False,
-        label='Minimum child VLAN VID'
+        label=_('Minimum child VLAN VID')
     )
     max_vid = forms.IntegerField(
         min_value=VLAN_VID_MIN,
         max_value=VLAN_VID_MAX,
         required=False,
-        label='Maximum child VLAN VID'
+        label=_('Maximum child VLAN VID')
     )
     description = forms.CharField(
         max_length=200,

+ 43 - 42
netbox/ipam/forms/bulk_import.py

@@ -1,6 +1,7 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
+from django.utils.translation import gettext as _
 
 from dcim.models import Device, Interface, Site
 from ipam.choices import *
@@ -36,7 +37,7 @@ class VRFCSVForm(NetBoxModelCSVForm):
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:
@@ -49,7 +50,7 @@ class RouteTargetCSVForm(NetBoxModelCSVForm):
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:
@@ -64,7 +65,7 @@ class RIRCSVForm(NetBoxModelCSVForm):
         model = RIR
         fields = ('name', 'slug', 'is_private', 'description', 'tags')
         help_texts = {
-            'name': 'RIR name',
+            'name': _('RIR name'),
         }
 
 
@@ -72,13 +73,13 @@ class AggregateCSVForm(NetBoxModelCSVForm):
     rir = CSVModelChoiceField(
         queryset=RIR.objects.all(),
         to_field_name='name',
-        help_text='Assigned RIR'
+        help_text=_('Assigned RIR')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:
@@ -90,13 +91,13 @@ class ASNCSVForm(NetBoxModelCSVForm):
     rir = CSVModelChoiceField(
         queryset=RIR.objects.all(),
         to_field_name='name',
-        help_text='Assigned RIR'
+        help_text=_('Assigned RIR')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:
@@ -117,41 +118,41 @@ class PrefixCSVForm(NetBoxModelCSVForm):
         queryset=VRF.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned VRF'
+        help_text=_('Assigned VRF')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned site'
+        help_text=_('Assigned site')
     )
     vlan_group = CSVModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text="VLAN's group (if any)"
+        help_text=_("VLAN's group (if any)")
     )
     vlan = CSVModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         to_field_name='vid',
-        help_text="Assigned VLAN"
+        help_text=_("Assigned VLAN")
     )
     status = CSVChoiceField(
         choices=PrefixStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     role = CSVModelChoiceField(
         queryset=Role.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Functional role'
+        help_text=_('Functional role')
     )
 
     class Meta:
@@ -181,23 +182,23 @@ class IPRangeCSVForm(NetBoxModelCSVForm):
         queryset=VRF.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned VRF'
+        help_text=_('Assigned VRF')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     status = CSVChoiceField(
         choices=IPRangeStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     role = CSVModelChoiceField(
         queryset=Role.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Functional role'
+        help_text=_('Functional role')
     )
 
     class Meta:
@@ -212,43 +213,43 @@ class IPAddressCSVForm(NetBoxModelCSVForm):
         queryset=VRF.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned VRF'
+        help_text=_('Assigned VRF')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     status = CSVChoiceField(
         choices=IPAddressStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     role = CSVChoiceField(
         choices=IPAddressRoleChoices,
         required=False,
-        help_text='Functional role'
+        help_text=_('Functional role')
     )
     device = CSVModelChoiceField(
         queryset=Device.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent device of assigned interface (if any)'
+        help_text=_('Parent device of assigned interface (if any)')
     )
     virtual_machine = CSVModelChoiceField(
         queryset=VirtualMachine.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent VM of assigned interface (if any)'
+        help_text=_('Parent VM of assigned interface (if any)')
     )
     interface = CSVModelChoiceField(
         queryset=Interface.objects.none(),  # Can also refer to VMInterface
         required=False,
         to_field_name='name',
-        help_text='Assigned interface'
+        help_text=_('Assigned interface')
     )
     is_primary = forms.BooleanField(
-        help_text='Make this the primary IP for the assigned device',
+        help_text=_('Make this the primary IP for the assigned device'),
         required=False
     )
 
@@ -333,7 +334,7 @@ class VLANGroupCSVForm(NetBoxModelCSVForm):
     scope_type = CSVContentTypeField(
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
         required=False,
-        label='Scope type (app & model)'
+        label=_('Scope type (app & model)')
     )
     min_vid = forms.IntegerField(
         min_value=VLAN_VID_MIN,
@@ -361,29 +362,29 @@ class VLANCSVForm(NetBoxModelCSVForm):
         queryset=Site.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned site'
+        help_text=_('Assigned site')
     )
     group = CSVModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned VLAN group'
+        help_text=_('Assigned VLAN group')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     status = CSVChoiceField(
         choices=VLANStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     role = CSVModelChoiceField(
         queryset=Role.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Functional role'
+        help_text=_('Functional role')
     )
 
     class Meta:
@@ -398,7 +399,7 @@ class VLANCSVForm(NetBoxModelCSVForm):
 class ServiceTemplateCSVForm(NetBoxModelCSVForm):
     protocol = CSVChoiceField(
         choices=ServiceProtocolChoices,
-        help_text='IP protocol'
+        help_text=_('IP protocol')
     )
 
     class Meta:
@@ -411,17 +412,17 @@ class ServiceCSVForm(NetBoxModelCSVForm):
         queryset=Device.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Required if not assigned to a VM'
+        help_text=_('Required if not assigned to a VM')
     )
     virtual_machine = CSVModelChoiceField(
         queryset=VirtualMachine.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Required if not assigned to a device'
+        help_text=_('Required if not assigned to a device')
     )
     protocol = CSVChoiceField(
         choices=ServiceProtocolChoices,
-        help_text='IP protocol'
+        help_text=_('IP protocol')
     )
 
     class Meta:
@@ -437,7 +438,7 @@ class L2VPNCSVForm(NetBoxModelCSVForm):
     )
     type = CSVChoiceField(
         choices=L2VPNTypeChoices,
-        help_text='L2VPN type'
+        help_text=_('L2VPN type')
     )
 
     class Meta:
@@ -450,31 +451,31 @@ class L2VPNTerminationCSVForm(NetBoxModelCSVForm):
         queryset=L2VPN.objects.all(),
         required=True,
         to_field_name='name',
-        label='L2VPN',
+        label=_('L2VPN'),
     )
     device = CSVModelChoiceField(
         queryset=Device.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent device (for interface)'
+        help_text=_('Parent device (for interface)')
     )
     virtual_machine = CSVModelChoiceField(
         queryset=VirtualMachine.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent virtual machine (for interface)'
+        help_text=_('Parent virtual machine (for interface)')
     )
     interface = CSVModelChoiceField(
         queryset=Interface.objects.none(),  # Can also refer to VMInterface
         required=False,
         to_field_name='name',
-        help_text='Assigned interface (device or VM)'
+        help_text=_('Assigned interface (device or VM)')
     )
     vlan = CSVModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned VLAN'
+        help_text=_('Assigned VLAN')
     )
 
     class Meta:

+ 2 - 2
netbox/ipam/forms/filtersets.py

@@ -397,13 +397,13 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
         required=False,
         min_value=VLAN_VID_MIN,
         max_value=VLAN_VID_MAX,
-        label='Minimum VID'
+        label=_('Minimum VID')
     )
     max_vid = forms.IntegerField(
         required=False,
         min_value=VLAN_VID_MIN,
         max_value=VLAN_VID_MAX,
-        label='Maximum VID'
+        label=_('Maximum VID')
     )
     tag = TagFilterField(model)
 

+ 53 - 52
netbox/ipam/forms/model_forms.py

@@ -1,6 +1,7 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
+from django.utils.translation import gettext as _
 
 from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
 from ipam.choices import *
@@ -67,7 +68,7 @@ class VRFForm(TenancyForm, NetBoxModelForm):
             'rd': "RD",
         }
         help_texts = {
-            'rd': "Route distinguisher in any format",
+            'rd': _("Route distinguisher in any format"),
         }
 
 
@@ -104,7 +105,7 @@ class RIRForm(NetBoxModelForm):
 class AggregateForm(TenancyForm, NetBoxModelForm):
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
-        label='RIR'
+        label=_('RIR')
     )
     comments = CommentField()
 
@@ -119,8 +120,8 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
             'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
         ]
         help_texts = {
-            'prefix': "IPv4 or IPv6 network",
-            'rir': "Regional Internet Registry responsible for this prefix",
+            'prefix': _("IPv4 or IPv6 network"),
+            'rir': _("Regional Internet Registry responsible for this prefix"),
         }
         widgets = {
             'date_added': DatePicker(),
@@ -130,11 +131,11 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
 class ASNForm(TenancyForm, NetBoxModelForm):
     rir = DynamicModelChoiceField(
         queryset=RIR.objects.all(),
-        label='RIR',
+        label=_('RIR'),
     )
     sites = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
-        label='Sites',
+        label=_('Sites'),
         required=False
     )
     comments = CommentField()
@@ -150,8 +151,8 @@ class ASNForm(TenancyForm, NetBoxModelForm):
             'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
         ]
         help_texts = {
-            'asn': "AS number",
-            'rir': "Regional Internet Registry responsible for this prefix",
+            'asn': _("AS number"),
+            'rir': _("Regional Internet Registry responsible for this prefix"),
         }
         widgets = {
             'date_added': DatePicker(),
@@ -189,7 +190,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
@@ -217,7 +218,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
     vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
-        label='VLAN group',
+        label=_('VLAN group'),
         null_option='None',
         query_params={
             'site': '$site'
@@ -229,7 +230,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        label='VLAN',
+        label=_('VLAN'),
         query_params={
             'site_id': '$site',
             'group_id': '$vlan_group',
@@ -262,7 +263,7 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     role = DynamicModelChoiceField(
         queryset=Role.objects.all(),
@@ -311,7 +312,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     vminterface = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         required=False,
-        label='Interface',
+        label=_('Interface'),
         query_params={
             'virtual_machine_id': '$virtual_machine'
         }
@@ -319,17 +320,17 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     fhrpgroup = DynamicModelChoiceField(
         queryset=FHRPGroup.objects.all(),
         required=False,
-        label='FHRP Group'
+        label=_('FHRP Group')
     )
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     nat_region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label='Region',
+        label=_('Region'),
         initial_params={
             'sites': '$nat_site'
         }
@@ -337,7 +338,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     nat_site_group = DynamicModelChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label='Site group',
+        label=_('Site group'),
         initial_params={
             'sites': '$nat_site'
         }
@@ -345,7 +346,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     nat_site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
-        label='Site',
+        label=_('Site'),
         query_params={
             'region_id': '$nat_region',
             'group_id': '$nat_site_group',
@@ -354,7 +355,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     nat_rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         required=False,
-        label='Rack',
+        label=_('Rack'),
         null_option='None',
         query_params={
             'site_id': '$site'
@@ -363,7 +364,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     nat_device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         required=False,
-        label='Device',
+        label=_('Device'),
         query_params={
             'site_id': '$site',
             'rack_id': '$nat_rack',
@@ -372,12 +373,12 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     nat_cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         required=False,
-        label='Cluster'
+        label=_('Cluster')
     )
     nat_virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.all(),
         required=False,
-        label='Virtual Machine',
+        label=_('Virtual Machine'),
         query_params={
             'cluster_id': '$nat_cluster',
         }
@@ -385,12 +386,12 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     nat_vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     nat_inside = DynamicModelChoiceField(
         queryset=IPAddress.objects.all(),
         required=False,
-        label='IP Address',
+        label=_('IP Address'),
         query_params={
             'device_id': '$nat_device',
             'virtual_machine_id': '$nat_virtual_machine',
@@ -399,7 +400,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     )
     primary_for_parent = forms.BooleanField(
         required=False,
-        label='Make this the primary IP for the device/VM'
+        label=_('Make this the primary IP for the device/VM')
     )
     comments = CommentField()
 
@@ -500,7 +501,7 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
 
     class Meta:
@@ -518,11 +519,11 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
     vrf_id = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     q = forms.CharField(
         required=False,
-        label='Search',
+        label=_('Search'),
     )
 
 
@@ -532,16 +533,16 @@ class FHRPGroupForm(NetBoxModelForm):
     ip_vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     ip_address = IPNetworkFormField(
         required=False,
-        label='Address'
+        label=_('Address')
     )
     ip_status = forms.ChoiceField(
         choices=add_blank_choice(IPAddressStatusChoices),
         required=False,
-        label='Status'
+        label=_('Status')
     )
     comments = CommentField()
 
@@ -633,7 +634,7 @@ class VLANGroupForm(NetBoxModelForm):
         initial_params={
             'sites': '$site'
         },
-        label='Site group'
+        label=_('Site group')
     )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
@@ -670,7 +671,7 @@ class VLANGroupForm(NetBoxModelForm):
         initial_params={
             'clusters': '$cluster'
         },
-        label='Cluster group'
+        label=_('Cluster group')
     )
     cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
@@ -734,7 +735,7 @@ class VLANForm(TenancyForm, NetBoxModelForm):
         ),
         required=False,
         widget=StaticSelect,
-        label='Group scope'
+        label=_('Group scope')
     )
     group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
@@ -742,7 +743,7 @@ class VLANForm(TenancyForm, NetBoxModelForm):
         query_params={
             'scope_type': '$scope_type',
         },
-        label='VLAN Group'
+        label=_('VLAN Group')
     )
 
     # Site assignment fields
@@ -752,7 +753,7 @@ class VLANForm(TenancyForm, NetBoxModelForm):
         initial_params={
             'sites': '$site'
         },
-        label='Region'
+        label=_('Region')
     )
     sitegroup = DynamicModelChoiceField(
         queryset=SiteGroup.objects.all(),
@@ -760,7 +761,7 @@ class VLANForm(TenancyForm, NetBoxModelForm):
         initial_params={
             'sites': '$site'
         },
-        label='Site group'
+        label=_('Site group')
     )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
@@ -786,12 +787,12 @@ class VLANForm(TenancyForm, NetBoxModelForm):
             'tags',
         ]
         help_texts = {
-            'site': "Leave blank if this VLAN spans multiple sites",
-            'group': "VLAN group (optional)",
-            'vid': "Configured VLAN ID",
-            'name': "Configured VLAN name",
-            'status': "Operational status of this VLAN",
-            'role': "The primary function of this VLAN",
+            'site': _("Leave blank if this VLAN spans multiple sites"),
+            'group': _("VLAN group (optional)"),
+            'vid': _("Configured VLAN ID"),
+            'name': _("Configured VLAN name"),
+            'status': _("Operational status of this VLAN"),
+            'role': _("The primary function of this VLAN"),
         }
         widgets = {
             'status': StaticSelect(),
@@ -804,7 +805,7 @@ class ServiceTemplateForm(NetBoxModelForm):
             min_value=SERVICE_PORT_MIN,
             max_value=SERVICE_PORT_MAX
         ),
-        help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
+        help_text=_("Comma-separated list of one or more port numbers. A range may be specified using a hyphen.")
     )
     comments = CommentField()
 
@@ -836,12 +837,12 @@ class ServiceForm(NetBoxModelForm):
             min_value=SERVICE_PORT_MIN,
             max_value=SERVICE_PORT_MAX
         ),
-        help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
+        help_text=_("Comma-separated list of one or more port numbers. A range may be specified using a hyphen.")
     )
     ipaddresses = DynamicModelMultipleChoiceField(
         queryset=IPAddress.objects.all(),
         required=False,
-        label='IP Addresses',
+        label=_('IP Addresses'),
         query_params={
             'device_id': '$device',
             'virtual_machine_id': '$virtual_machine',
@@ -855,8 +856,8 @@ class ServiceForm(NetBoxModelForm):
             'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags',
         ]
         help_texts = {
-            'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
-                           "reachable via all IPs assigned to the device.",
+            'ipaddresses': _("IP address assignment is optional. If no IPs are selected, the service is assumed to be "
+                             "reachable via all IPs assigned to the device."),
         }
         widgets = {
             'protocol': StaticSelect(),
@@ -937,12 +938,12 @@ class L2VPNTerminationForm(NetBoxModelForm):
         queryset=L2VPN.objects.all(),
         required=True,
         query_params={},
-        label='L2VPN',
+        label=_('L2VPN'),
         fetch_trigger='open'
     )
     device_vlan = DynamicModelChoiceField(
         queryset=Device.objects.all(),
-        label="Available on Device",
+        label=_("Available on Device"),
         required=False,
         query_params={}
     )
@@ -952,7 +953,7 @@ class L2VPNTerminationForm(NetBoxModelForm):
         query_params={
             'available_on_device': '$device_vlan'
         },
-        label='VLAN'
+        label=_('VLAN')
     )
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
@@ -977,7 +978,7 @@ class L2VPNTerminationForm(NetBoxModelForm):
         query_params={
             'virtual_machine_id': '$virtual_machine'
         },
-        label='Interface'
+        label=_('Interface')
     )
 
     class Meta:

+ 17 - 16
netbox/ipam/models/ip.py

@@ -6,6 +6,7 @@ from django.db import models
 from django.db.models import F
 from django.urls import reverse
 from django.utils.functional import cached_property
+from django.utils.translation import gettext as _
 
 from dcim.fields import ASNField
 from dcim.models import Device
@@ -64,7 +65,7 @@ class RIR(OrganizationalModel):
     is_private = models.BooleanField(
         default=False,
         verbose_name='Private',
-        help_text='IP space managed by this RIR is considered private'
+        help_text=_('IP space managed by this RIR is considered private')
     )
 
     class Meta:
@@ -84,7 +85,7 @@ class ASN(PrimaryModel):
     asn = ASNField(
         unique=True,
         verbose_name='ASN',
-        help_text='32-bit autonomous system number'
+        help_text=_('32-bit autonomous system number')
     )
     rir = models.ForeignKey(
         to='ipam.RIR',
@@ -263,7 +264,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
     assigned to a VLAN where appropriate.
     """
     prefix = IPNetworkField(
-        help_text='IPv4 or IPv6 network with mask'
+        help_text=_('IPv4 or IPv6 network with mask')
     )
     site = models.ForeignKey(
         to='dcim.Site',
@@ -300,7 +301,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
         choices=PrefixStatusChoices,
         default=PrefixStatusChoices.STATUS_ACTIVE,
         verbose_name='Status',
-        help_text='Operational status of this prefix'
+        help_text=_('Operational status of this prefix')
     )
     role = models.ForeignKey(
         to='ipam.Role',
@@ -308,16 +309,16 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
         related_name='prefixes',
         blank=True,
         null=True,
-        help_text='The primary function of this prefix'
+        help_text=_('The primary function of this prefix')
     )
     is_pool = models.BooleanField(
         verbose_name='Is a pool',
         default=False,
-        help_text='All IP addresses within this prefix are considered usable'
+        help_text=_('All IP addresses within this prefix are considered usable')
     )
     mark_utilized = models.BooleanField(
         default=False,
-        help_text="Treat as 100% utilized"
+        help_text=_("Treat as 100% utilized")
     )
 
     # Cached depth & child counts
@@ -538,10 +539,10 @@ class IPRange(PrimaryModel):
     A range of IP addresses, defined by start and end addresses.
     """
     start_address = IPAddressField(
-        help_text='IPv4 or IPv6 address (with mask)'
+        help_text=_('IPv4 or IPv6 address (with mask)')
     )
     end_address = IPAddressField(
-        help_text='IPv4 or IPv6 address (with mask)'
+        help_text=_('IPv4 or IPv6 address (with mask)')
     )
     size = models.PositiveIntegerField(
         editable=False
@@ -565,7 +566,7 @@ class IPRange(PrimaryModel):
         max_length=50,
         choices=IPRangeStatusChoices,
         default=IPRangeStatusChoices.STATUS_ACTIVE,
-        help_text='Operational status of this range'
+        help_text=_('Operational status of this range')
     )
     role = models.ForeignKey(
         to='ipam.Role',
@@ -573,7 +574,7 @@ class IPRange(PrimaryModel):
         related_name='ip_ranges',
         blank=True,
         null=True,
-        help_text='The primary function of this range'
+        help_text=_('The primary function of this range')
     )
 
     clone_fields = (
@@ -736,7 +737,7 @@ class IPAddress(PrimaryModel):
     which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
     """
     address = IPAddressField(
-        help_text='IPv4 or IPv6 address (with mask)'
+        help_text=_('IPv4 or IPv6 address (with mask)')
     )
     vrf = models.ForeignKey(
         to='ipam.VRF',
@@ -757,13 +758,13 @@ class IPAddress(PrimaryModel):
         max_length=50,
         choices=IPAddressStatusChoices,
         default=IPAddressStatusChoices.STATUS_ACTIVE,
-        help_text='The operational status of this IP'
+        help_text=_('The operational status of this IP')
     )
     role = models.CharField(
         max_length=50,
         choices=IPAddressRoleChoices,
         blank=True,
-        help_text='The functional role of this IP'
+        help_text=_('The functional role of this IP')
     )
     assigned_object_type = models.ForeignKey(
         to=ContentType,
@@ -788,14 +789,14 @@ class IPAddress(PrimaryModel):
         blank=True,
         null=True,
         verbose_name='NAT (Inside)',
-        help_text='The IP for which this address is the "outside" IP'
+        help_text=_('The IP for which this address is the "outside" IP')
     )
     dns_name = models.CharField(
         max_length=255,
         blank=True,
         validators=[DNSValidator],
         verbose_name='DNS Name',
-        help_text='Hostname or FQDN (not case-sensitive)'
+        help_text=_('Hostname or FQDN (not case-sensitive)')
     )
 
     objects = IPAddressManager()

+ 3 - 2
netbox/ipam/models/vlans.py

@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.urls import reverse
+from django.utils.translation import gettext as _
 
 from dcim.models import Interface
 from ipam.choices import *
@@ -50,7 +51,7 @@ class VLANGroup(OrganizationalModel):
             MinValueValidator(VLAN_VID_MIN),
             MaxValueValidator(VLAN_VID_MAX)
         ),
-        help_text='Lowest permissible ID of a child VLAN'
+        help_text=_('Lowest permissible ID of a child VLAN')
     )
     max_vid = models.PositiveSmallIntegerField(
         verbose_name='Maximum VLAN ID',
@@ -59,7 +60,7 @@ class VLANGroup(OrganizationalModel):
             MinValueValidator(VLAN_VID_MIN),
             MaxValueValidator(VLAN_VID_MAX)
         ),
-        help_text='Highest permissible ID of a child VLAN'
+        help_text=_('Highest permissible ID of a child VLAN')
     )
 
     class Meta:

+ 4 - 3
netbox/ipam/models/vrfs.py

@@ -1,5 +1,6 @@
 from django.db import models
 from django.urls import reverse
+from django.utils.translation import gettext as _
 
 from ipam.constants import *
 from netbox.models import PrimaryModel
@@ -26,7 +27,7 @@ class VRF(PrimaryModel):
         blank=True,
         null=True,
         verbose_name='Route distinguisher',
-        help_text='Unique route distinguisher (as defined in RFC 4364)'
+        help_text=_('Unique route distinguisher (as defined in RFC 4364)')
     )
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
@@ -38,7 +39,7 @@ class VRF(PrimaryModel):
     enforce_unique = models.BooleanField(
         default=True,
         verbose_name='Enforce unique space',
-        help_text='Prevent duplicate prefixes/IP addresses within this VRF'
+        help_text=_('Prevent duplicate prefixes/IP addresses within this VRF')
     )
     import_targets = models.ManyToManyField(
         to='ipam.RouteTarget',
@@ -76,7 +77,7 @@ class RouteTarget(PrimaryModel):
     name = models.CharField(
         max_length=VRF_RD_MAX_LENGTH,  # Same format options as VRF RD (RFC 4360 section 4)
         unique=True,
-        help_text='Route target value (formatted in accordance with RFC 4360)'
+        help_text=_('Route target value (formatted in accordance with RFC 4360)')
     )
     tenant = models.ForeignKey(
         to='tenancy.Tenant',

+ 47 - 46
netbox/netbox/config/parameters.py

@@ -1,5 +1,6 @@
 from django import forms
 from django.contrib.postgres.forms import SimpleArrayField
+from django.utils.translation import gettext_lazy as _
 
 
 class ConfigParam:
@@ -18,9 +19,9 @@ PARAMS = (
     # Banners
     ConfigParam(
         name='BANNER_LOGIN',
-        label='Login banner',
+        label=_('Login banner'),
         default='',
-        description="Additional content to display on the login page",
+        description=_("Additional content to display on the login page"),
         field_kwargs={
             'widget': forms.Textarea(
                 attrs={'class': 'vLargeTextField'}
@@ -29,9 +30,9 @@ PARAMS = (
     ),
     ConfigParam(
         name='BANNER_TOP',
-        label='Top banner',
+        label=_('Top banner'),
         default='',
-        description="Additional content to display at the top of every page",
+        description=_("Additional content to display at the top of every page"),
         field_kwargs={
             'widget': forms.Textarea(
                 attrs={'class': 'vLargeTextField'}
@@ -40,9 +41,9 @@ PARAMS = (
     ),
     ConfigParam(
         name='BANNER_BOTTOM',
-        label='Bottom banner',
+        label=_('Bottom banner'),
         default='',
-        description="Additional content to display at the bottom of every page",
+        description=_("Additional content to display at the bottom of every page"),
         field_kwargs={
             'widget': forms.Textarea(
                 attrs={'class': 'vLargeTextField'}
@@ -53,69 +54,69 @@ PARAMS = (
     # IPAM
     ConfigParam(
         name='ENFORCE_GLOBAL_UNIQUE',
-        label='Globally unique IP space',
+        label=_('Globally unique IP space'),
         default=False,
-        description="Enforce unique IP addressing within the global table",
+        description=_("Enforce unique IP addressing within the global table"),
         field=forms.BooleanField
     ),
     ConfigParam(
         name='PREFER_IPV4',
-        label='Prefer IPv4',
+        label=_('Prefer IPv4'),
         default=False,
-        description="Prefer IPv4 addresses over IPv6",
+        description=_("Prefer IPv4 addresses over IPv6"),
         field=forms.BooleanField
     ),
 
     # Racks
     ConfigParam(
         name='RACK_ELEVATION_DEFAULT_UNIT_HEIGHT',
-        label='Rack unit height',
+        label=_('Rack unit height'),
         default=22,
-        description="Default unit height for rendered rack elevations",
+        description=_("Default unit height for rendered rack elevations"),
         field=forms.IntegerField
     ),
     ConfigParam(
         name='RACK_ELEVATION_DEFAULT_UNIT_WIDTH',
-        label='Rack unit width',
+        label=_('Rack unit width'),
         default=220,
-        description="Default unit width for rendered rack elevations",
+        description=_("Default unit width for rendered rack elevations"),
         field=forms.IntegerField
     ),
 
     # Power
     ConfigParam(
         name='POWERFEED_DEFAULT_VOLTAGE',
-        label='Powerfeed voltage',
+        label=_('Powerfeed voltage'),
         default=120,
-        description="Default voltage for powerfeeds",
+        description=_("Default voltage for powerfeeds"),
         field=forms.IntegerField
     ),
 
     ConfigParam(
         name='POWERFEED_DEFAULT_AMPERAGE',
-        label='Powerfeed amperage',
+        label=_('Powerfeed amperage'),
         default=15,
-        description="Default amperage for powerfeeds",
+        description=_("Default amperage for powerfeeds"),
         field=forms.IntegerField
     ),
 
     ConfigParam(
         name='POWERFEED_DEFAULT_MAX_UTILIZATION',
-        label='Powerfeed max utilization',
+        label=_('Powerfeed max utilization'),
         default=80,
-        description="Default max utilization for powerfeeds",
+        description=_("Default max utilization for powerfeeds"),
         field=forms.IntegerField
     ),
 
     # Security
     ConfigParam(
         name='ALLOWED_URL_SCHEMES',
-        label='Allowed URL schemes',
+        label=_('Allowed URL schemes'),
         default=(
             'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc',
             'xmpp',
         ),
-        description="Permitted schemes for URLs in user-provided content",
+        description=_("Permitted schemes for URLs in user-provided content"),
         field=SimpleArrayField,
         field_kwargs={'base_field': forms.CharField()}
     ),
@@ -123,13 +124,13 @@ PARAMS = (
     # Pagination
     ConfigParam(
         name='PAGINATE_COUNT',
-        label='Default page size',
+        label=_('Default page size'),
         default=50,
         field=forms.IntegerField
     ),
     ConfigParam(
         name='MAX_PAGE_SIZE',
-        label='Maximum page size',
+        label=_('Maximum page size'),
         default=1000,
         field=forms.IntegerField
     ),
@@ -137,9 +138,9 @@ PARAMS = (
     # Validation
     ConfigParam(
         name='CUSTOM_VALIDATORS',
-        label='Custom validators',
+        label=_('Custom validators'),
         default={},
-        description="Custom validation rules (JSON)",
+        description=_("Custom validation rules (JSON)"),
         field=forms.JSONField,
         field_kwargs={
             'widget': forms.Textarea(
@@ -151,28 +152,28 @@ PARAMS = (
     # NAPALM
     ConfigParam(
         name='NAPALM_USERNAME',
-        label='NAPALM username',
+        label=_('NAPALM username'),
         default='',
-        description="Username to use when connecting to devices via NAPALM"
+        description=_("Username to use when connecting to devices via NAPALM")
     ),
     ConfigParam(
         name='NAPALM_PASSWORD',
-        label='NAPALM password',
+        label=_('NAPALM password'),
         default='',
-        description="Password to use when connecting to devices via NAPALM"
+        description=_("Password to use when connecting to devices via NAPALM")
     ),
     ConfigParam(
         name='NAPALM_TIMEOUT',
-        label='NAPALM timeout',
+        label=_('NAPALM timeout'),
         default=30,
-        description="NAPALM connection timeout (in seconds)",
+        description=_("NAPALM connection timeout (in seconds)"),
         field=forms.IntegerField
     ),
     ConfigParam(
         name='NAPALM_ARGS',
-        label='NAPALM arguments',
+        label=_('NAPALM arguments'),
         default={},
-        description="Additional arguments to pass when invoking a NAPALM driver (as JSON data)",
+        description=_("Additional arguments to pass when invoking a NAPALM driver (as JSON data)"),
         field=forms.JSONField,
         field_kwargs={
             'widget': forms.Textarea(
@@ -184,46 +185,46 @@ PARAMS = (
     # User preferences
     ConfigParam(
         name='DEFAULT_USER_PREFERENCES',
-        label='Default preferences',
+        label=_('Default preferences'),
         default={},
-        description="Default preferences for new users",
+        description=_("Default preferences for new users"),
         field=forms.JSONField
     ),
 
     # Miscellaneous
     ConfigParam(
         name='MAINTENANCE_MODE',
-        label='Maintenance mode',
+        label=_('Maintenance mode'),
         default=False,
-        description="Enable maintenance mode",
+        description=_("Enable maintenance mode"),
         field=forms.BooleanField
     ),
     ConfigParam(
         name='GRAPHQL_ENABLED',
-        label='GraphQL enabled',
+        label=_('GraphQL enabled'),
         default=True,
-        description="Enable the GraphQL API",
+        description=_("Enable the GraphQL API"),
         field=forms.BooleanField
     ),
     ConfigParam(
         name='CHANGELOG_RETENTION',
-        label='Changelog retention',
+        label=_('Changelog retention'),
         default=90,
-        description="Days to retain changelog history (set to zero for unlimited)",
+        description=_("Days to retain changelog history (set to zero for unlimited)"),
         field=forms.IntegerField
     ),
     ConfigParam(
         name='JOBRESULT_RETENTION',
-        label='Job result retention',
+        label=_('Job result retention'),
         default=90,
-        description="Days to retain job result history (set to zero for unlimited)",
+        description=_("Days to retain job result history (set to zero for unlimited)"),
         field=forms.IntegerField
     ),
     ConfigParam(
         name='MAPS_URL',
-        label='Maps URL',
+        label=_('Maps URL'),
         default='https://maps.google.com/?q=',
-        description="Base URL for mapping geographic locations"
+        description=_("Base URL for mapping geographic locations")
     ),
 
 )

+ 2 - 1
netbox/netbox/filtersets.py

@@ -5,6 +5,7 @@ from django.db import models
 from django_filters.exceptions import FieldLookupError
 from django_filters.utils import get_model_field, resolve_field
 from django.shortcuts import get_object_or_404
+from django.utils.translation import gettext as _
 
 from extras.choices import CustomFieldFilterLogicChoices
 from extras.filters import TagFilter
@@ -235,7 +236,7 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet):
     """
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     tag = TagFilter()
 

+ 2 - 2
netbox/netbox/forms/__init__.py

@@ -17,7 +17,7 @@ LOOKUP_CHOICES = (
 
 class SearchForm(BootstrapMixin, forms.Form):
     q = forms.CharField(
-        label='Search',
+        label=_('Search'),
         widget=forms.TextInput(
             attrs={
                 'hx-get': '',
@@ -29,7 +29,7 @@ class SearchForm(BootstrapMixin, forms.Form):
     obj_types = forms.MultipleChoiceField(
         choices=[],
         required=False,
-        label='Object type(s)',
+        label=_('Object type(s)'),
         widget=StaticSelectMultiple()
     )
     lookup = forms.ChoiceField(

+ 2 - 1
netbox/netbox/forms/base.py

@@ -2,6 +2,7 @@ from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.db.models import Q
+from django.utils.translation import gettext as _
 
 from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
 from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin
@@ -132,7 +133,7 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMi
     """
     q = forms.CharField(
         required=False,
-        label='Search'
+        label=_('Search')
     )
 
     def __init__(self, *args, **kwargs):

+ 116 - 115
netbox/netbox/navigation/menu.py

@@ -1,119 +1,120 @@
+from django.utils.translation import gettext as _
+
 from netbox.registry import registry
 from . import *
 
-
 #
 # Nav menus
 #
 
 ORGANIZATION_MENU = Menu(
-    label='Organization',
+    label=_('Organization'),
     icon_class='mdi mdi-domain',
     groups=(
         MenuGroup(
-            label='Sites',
+            label=_('Sites'),
             items=(
-                get_model_item('dcim', 'site', 'Sites'),
-                get_model_item('dcim', 'region', 'Regions'),
-                get_model_item('dcim', 'sitegroup', 'Site Groups'),
-                get_model_item('dcim', 'location', 'Locations'),
+                get_model_item('dcim', 'site', _('Sites')),
+                get_model_item('dcim', 'region', _('Regions')),
+                get_model_item('dcim', 'sitegroup', _('Site Groups')),
+                get_model_item('dcim', 'location', _('Locations')),
             ),
         ),
         MenuGroup(
-            label='Racks',
+            label=_('Racks'),
             items=(
-                get_model_item('dcim', 'rack', 'Racks'),
-                get_model_item('dcim', 'rackrole', 'Rack Roles'),
-                get_model_item('dcim', 'rackreservation', 'Reservations'),
+                get_model_item('dcim', 'rack', _('Racks')),
+                get_model_item('dcim', 'rackrole', _('Rack Roles')),
+                get_model_item('dcim', 'rackreservation', _('Reservations')),
                 MenuItem(
                     link='dcim:rack_elevation_list',
-                    link_text='Elevations',
+                    link_text=_('Elevations'),
                     permissions=['dcim.view_rack']
                 ),
             ),
         ),
         MenuGroup(
-            label='Tenancy',
+            label=_('Tenancy'),
             items=(
-                get_model_item('tenancy', 'tenant', 'Tenants'),
-                get_model_item('tenancy', 'tenantgroup', 'Tenant Groups'),
+                get_model_item('tenancy', 'tenant', _('Tenants')),
+                get_model_item('tenancy', 'tenantgroup', _('Tenant Groups')),
             ),
         ),
         MenuGroup(
-            label='Contacts',
+            label=_('Contacts'),
             items=(
-                get_model_item('tenancy', 'contact', 'Contacts'),
-                get_model_item('tenancy', 'contactgroup', 'Contact Groups'),
-                get_model_item('tenancy', 'contactrole', 'Contact Roles'),
+                get_model_item('tenancy', 'contact', _('Contacts')),
+                get_model_item('tenancy', 'contactgroup', _('Contact Groups')),
+                get_model_item('tenancy', 'contactrole', _('Contact Roles')),
             ),
         ),
     ),
 )
 
 DEVICES_MENU = Menu(
-    label='Devices',
+    label=_('Devices'),
     icon_class='mdi mdi-server',
     groups=(
         MenuGroup(
-            label='Devices',
+            label=_('Devices'),
             items=(
-                get_model_item('dcim', 'device', 'Devices'),
-                get_model_item('dcim', 'module', 'Modules'),
-                get_model_item('dcim', 'devicerole', 'Device Roles'),
-                get_model_item('dcim', 'platform', 'Platforms'),
-                get_model_item('dcim', 'virtualchassis', 'Virtual Chassis'),
-                get_model_item('dcim', 'virtualdevicecontext', 'Virtual Device Contexts'),
+                get_model_item('dcim', 'device', _('Devices')),
+                get_model_item('dcim', 'module', _('Modules')),
+                get_model_item('dcim', 'devicerole', _('Device Roles')),
+                get_model_item('dcim', 'platform', _('Platforms')),
+                get_model_item('dcim', 'virtualchassis', _('Virtual Chassis')),
+                get_model_item('dcim', 'virtualdevicecontext', _('Virtual Device Contexts')),
             ),
         ),
         MenuGroup(
-            label='Device Types',
+            label=_('Device Types'),
             items=(
-                get_model_item('dcim', 'devicetype', 'Device Types'),
-                get_model_item('dcim', 'moduletype', 'Module Types'),
-                get_model_item('dcim', 'manufacturer', 'Manufacturers'),
+                get_model_item('dcim', 'devicetype', _('Device Types')),
+                get_model_item('dcim', 'moduletype', _('Module Types')),
+                get_model_item('dcim', 'manufacturer', _('Manufacturers')),
             ),
         ),
         MenuGroup(
-            label='Device Components',
+            label=_('Device Components'),
             items=(
-                get_model_item('dcim', 'interface', 'Interfaces', actions=['import']),
-                get_model_item('dcim', 'frontport', 'Front Ports', actions=['import']),
-                get_model_item('dcim', 'rearport', 'Rear Ports', actions=['import']),
-                get_model_item('dcim', 'consoleport', 'Console Ports', actions=['import']),
-                get_model_item('dcim', 'consoleserverport', 'Console Server Ports', actions=['import']),
-                get_model_item('dcim', 'powerport', 'Power Ports', actions=['import']),
-                get_model_item('dcim', 'poweroutlet', 'Power Outlets', actions=['import']),
-                get_model_item('dcim', 'modulebay', 'Module Bays', actions=['import']),
-                get_model_item('dcim', 'devicebay', 'Device Bays', actions=['import']),
-                get_model_item('dcim', 'inventoryitem', 'Inventory Items', actions=['import']),
-                get_model_item('dcim', 'inventoryitemrole', 'Inventory Item Roles'),
+                get_model_item('dcim', 'interface', _('Interfaces'), actions=['import']),
+                get_model_item('dcim', 'frontport', _('Front Ports'), actions=['import']),
+                get_model_item('dcim', 'rearport', _('Rear Ports'), actions=['import']),
+                get_model_item('dcim', 'consoleport', _('Console Ports'), actions=['import']),
+                get_model_item('dcim', 'consoleserverport', _('Console Server Ports'), actions=['import']),
+                get_model_item('dcim', 'powerport', _('Power Ports'), actions=['import']),
+                get_model_item('dcim', 'poweroutlet', _('Power Outlets'), actions=['import']),
+                get_model_item('dcim', 'modulebay', _('Module Bays'), actions=['import']),
+                get_model_item('dcim', 'devicebay', _('Device Bays'), actions=['import']),
+                get_model_item('dcim', 'inventoryitem', _('Inventory Items'), actions=['import']),
+                get_model_item('dcim', 'inventoryitemrole', _('Inventory Item Roles')),
             ),
         ),
     ),
 )
 
 CONNECTIONS_MENU = Menu(
-    label='Connections',
+    label=_('Connections'),
     icon_class='mdi mdi-connection',
     groups=(
         MenuGroup(
-            label='Connections',
+            label=_('Connections'),
             items=(
-                get_model_item('dcim', 'cable', 'Cables', actions=['import']),
-                get_model_item('wireless', 'wirelesslink', 'Wireless Links', actions=['import']),
+                get_model_item('dcim', 'cable', _('Cables'), actions=['import']),
+                get_model_item('wireless', 'wirelesslink', _('Wireless Links'), actions=['import']),
                 MenuItem(
                     link='dcim:interface_connections_list',
-                    link_text='Interface Connections',
+                    link_text=_('Interface Connections'),
                     permissions=['dcim.view_interface']
                 ),
                 MenuItem(
                     link='dcim:console_connections_list',
-                    link_text='Console Connections',
+                    link_text=_('Console Connections'),
                     permissions=['dcim.view_consoleport']
                 ),
                 MenuItem(
                     link='dcim:power_connections_list',
-                    link_text='Power Connections',
+                    link_text=_('Power Connections'),
                     permissions=['dcim.view_powerport']
                 ),
             ),
@@ -122,192 +123,192 @@ CONNECTIONS_MENU = Menu(
 )
 
 WIRELESS_MENU = Menu(
-    label='Wireless',
+    label=_('Wireless'),
     icon_class='mdi mdi-wifi',
     groups=(
         MenuGroup(
-            label='Wireless',
+            label=_('Wireless'),
             items=(
-                get_model_item('wireless', 'wirelesslan', 'Wireless LANs'),
-                get_model_item('wireless', 'wirelesslangroup', 'Wireless LAN Groups'),
+                get_model_item('wireless', 'wirelesslan', _('Wireless LANs')),
+                get_model_item('wireless', 'wirelesslangroup', _('Wireless LAN Groups')),
             ),
         ),
     ),
 )
 
 IPAM_MENU = Menu(
-    label='IPAM',
+    label=_('IPAM'),
     icon_class='mdi mdi-counter',
     groups=(
         MenuGroup(
-            label='IP Addresses',
+            label=_('IP Addresses'),
             items=(
-                get_model_item('ipam', 'ipaddress', 'IP Addresses'),
-                get_model_item('ipam', 'iprange', 'IP Ranges'),
+                get_model_item('ipam', 'ipaddress', _('IP Addresses')),
+                get_model_item('ipam', 'iprange', _('IP Ranges')),
             ),
         ),
         MenuGroup(
-            label='Prefixes',
+            label=_('Prefixes'),
             items=(
-                get_model_item('ipam', 'prefix', 'Prefixes'),
-                get_model_item('ipam', 'role', 'Prefix & VLAN Roles'),
+                get_model_item('ipam', 'prefix', _('Prefixes')),
+                get_model_item('ipam', 'role', _('Prefix & VLAN Roles')),
             ),
         ),
         MenuGroup(
-            label='ASNs',
+            label=_('ASNs'),
             items=(
-                get_model_item('ipam', 'asn', 'ASNs'),
+                get_model_item('ipam', 'asn', _('ASNs')),
             ),
         ),
         MenuGroup(
-            label='Aggregates',
+            label=_('Aggregates'),
             items=(
-                get_model_item('ipam', 'aggregate', 'Aggregates'),
-                get_model_item('ipam', 'rir', 'RIRs'),
+                get_model_item('ipam', 'aggregate', _('Aggregates')),
+                get_model_item('ipam', 'rir', _('RIRs')),
             ),
         ),
         MenuGroup(
-            label='VRFs',
+            label=_('VRFs'),
             items=(
-                get_model_item('ipam', 'vrf', 'VRFs'),
-                get_model_item('ipam', 'routetarget', 'Route Targets'),
+                get_model_item('ipam', 'vrf', _('VRFs')),
+                get_model_item('ipam', 'routetarget', _('Route Targets')),
             ),
         ),
         MenuGroup(
-            label='VLANs',
+            label=_('VLANs'),
             items=(
-                get_model_item('ipam', 'vlan', 'VLANs'),
-                get_model_item('ipam', 'vlangroup', 'VLAN Groups'),
+                get_model_item('ipam', 'vlan', _('VLANs')),
+                get_model_item('ipam', 'vlangroup', _('VLAN Groups')),
             ),
         ),
         MenuGroup(
-            label='Other',
+            label=_('Other'),
             items=(
-                get_model_item('ipam', 'fhrpgroup', 'FHRP Groups'),
-                get_model_item('ipam', 'servicetemplate', 'Service Templates'),
-                get_model_item('ipam', 'service', 'Services'),
+                get_model_item('ipam', 'fhrpgroup', _('FHRP Groups')),
+                get_model_item('ipam', 'servicetemplate', _('Service Templates')),
+                get_model_item('ipam', 'service', _('Services')),
             ),
         ),
     ),
 )
 
 OVERLAY_MENU = Menu(
-    label='Overlay',
+    label=_('Overlay'),
     icon_class='mdi mdi-graph-outline',
     groups=(
         MenuGroup(
             label='L2VPNs',
             items=(
-                get_model_item('ipam', 'l2vpn', 'L2VPNs'),
-                get_model_item('ipam', 'l2vpntermination', 'Terminations'),
+                get_model_item('ipam', 'l2vpn', _('L2VPNs')),
+                get_model_item('ipam', 'l2vpntermination', _('Terminations')),
             ),
         ),
     ),
 )
 
 VIRTUALIZATION_MENU = Menu(
-    label='Virtualization',
+    label=_('Virtualization'),
     icon_class='mdi mdi-monitor',
     groups=(
         MenuGroup(
-            label='Virtual Machines',
+            label=_('Virtual Machines'),
             items=(
-                get_model_item('virtualization', 'virtualmachine', 'Virtual Machines'),
-                get_model_item('virtualization', 'vminterface', 'Interfaces', actions=['import']),
+                get_model_item('virtualization', 'virtualmachine', _('Virtual Machines')),
+                get_model_item('virtualization', 'vminterface', _('Interfaces'), actions=['import']),
             ),
         ),
         MenuGroup(
-            label='Clusters',
+            label=_('Clusters'),
             items=(
-                get_model_item('virtualization', 'cluster', 'Clusters'),
-                get_model_item('virtualization', 'clustertype', 'Cluster Types'),
-                get_model_item('virtualization', 'clustergroup', 'Cluster Groups'),
+                get_model_item('virtualization', 'cluster', _('Clusters')),
+                get_model_item('virtualization', 'clustertype', _('Cluster Types')),
+                get_model_item('virtualization', 'clustergroup', _('Cluster Groups')),
             ),
         ),
     ),
 )
 
 CIRCUITS_MENU = Menu(
-    label='Circuits',
+    label=_('Circuits'),
     icon_class='mdi mdi-transit-connection-variant',
     groups=(
         MenuGroup(
-            label='Circuits',
+            label=_('Circuits'),
             items=(
-                get_model_item('circuits', 'circuit', 'Circuits'),
-                get_model_item('circuits', 'circuittype', 'Circuit Types'),
+                get_model_item('circuits', 'circuit', _('Circuits')),
+                get_model_item('circuits', 'circuittype', _('Circuit Types')),
             ),
         ),
         MenuGroup(
-            label='Providers',
+            label=_('Providers'),
             items=(
-                get_model_item('circuits', 'provider', 'Providers'),
-                get_model_item('circuits', 'providernetwork', 'Provider Networks'),
+                get_model_item('circuits', 'provider', _('Providers')),
+                get_model_item('circuits', 'providernetwork', _('Provider Networks')),
             ),
         ),
     ),
 )
 
 POWER_MENU = Menu(
-    label='Power',
+    label=_('Power'),
     icon_class='mdi mdi-flash',
     groups=(
         MenuGroup(
-            label='Power',
+            label=_('Power'),
             items=(
-                get_model_item('dcim', 'powerfeed', 'Power Feeds'),
-                get_model_item('dcim', 'powerpanel', 'Power Panels'),
+                get_model_item('dcim', 'powerfeed', _('Power Feeds')),
+                get_model_item('dcim', 'powerpanel', _('Power Panels')),
             ),
         ),
     ),
 )
 
 OTHER_MENU = Menu(
-    label='Other',
+    label=_('Other'),
     icon_class='mdi mdi-notification-clear-all',
     groups=(
         MenuGroup(
-            label='Logging',
+            label=_('Logging'),
             items=(
-                get_model_item('extras', 'journalentry', 'Journal Entries', actions=[]),
-                get_model_item('extras', 'objectchange', 'Change Log', actions=[]),
+                get_model_item('extras', 'journalentry', _('Journal Entries'), actions=[]),
+                get_model_item('extras', 'objectchange', _('Change Log'), actions=[]),
             ),
         ),
         MenuGroup(
-            label='Customization',
+            label=_('Customization'),
             items=(
-                get_model_item('extras', 'customfield', 'Custom Fields'),
-                get_model_item('extras', 'customlink', 'Custom Links'),
-                get_model_item('extras', 'exporttemplate', 'Export Templates'),
-                get_model_item('extras', 'savedfilter', 'Saved Filters'),
+                get_model_item('extras', 'customfield', _('Custom Fields')),
+                get_model_item('extras', 'customlink', _('Custom Links')),
+                get_model_item('extras', 'exporttemplate', _('Export Templates')),
+                get_model_item('extras', 'savedfilter', _('Saved Filters')),
             ),
         ),
         MenuGroup(
-            label='Integrations',
+            label=_('Integrations'),
             items=(
-                get_model_item('extras', 'webhook', 'Webhooks'),
+                get_model_item('extras', 'webhook', _('Webhooks')),
                 MenuItem(
                     link='extras:report_list',
-                    link_text='Reports',
+                    link_text=_('Reports'),
                     permissions=['extras.view_report']
                 ),
                 MenuItem(
                     link='extras:script_list',
-                    link_text='Scripts',
+                    link_text=_('Scripts'),
                     permissions=['extras.view_script']
                 ),
                 MenuItem(
                     link='extras:jobresult_list',
-                    link_text='Job Results',
+                    link_text=_('Job Results'),
                     permissions=['extras.view_jobresult'],
                 ),
             ),
         ),
         MenuGroup(
-            label='Other',
+            label=_('Other'),
             items=(
                 get_model_item('extras', 'tag', 'Tags'),
-                get_model_item('extras', 'configcontext', 'Config Contexts', actions=['add']),
+                get_model_item('extras', 'configcontext', _('Config Contexts'), actions=['add']),
             ),
         ),
     ),
@@ -342,7 +343,7 @@ if registry['plugins']['menu_items']:
         for label, items in registry['plugins']['menu_items'].items()
     ]
     plugins_menu = Menu(
-        label="Plugins",
+        label=_("Plugins"),
         icon_class="mdi mdi-puzzle",
         groups=groups
     )

+ 7 - 6
netbox/netbox/preferences.py

@@ -1,3 +1,4 @@
+from django.utils.translation import gettext as _
 from netbox.registry import registry
 from users.preferences import UserPreference
 from utilities.paginator import EnhancedPaginator
@@ -13,7 +14,7 @@ PREFERENCES = {
 
     # User interface
     'ui.colormode': UserPreference(
-        label='Color mode',
+        label=_('Color mode'),
         choices=(
             ('light', 'Light'),
             ('dark', 'Dark'),
@@ -21,25 +22,25 @@ PREFERENCES = {
         default='light',
     ),
     'pagination.per_page': UserPreference(
-        label='Page length',
+        label=_('Page length'),
         choices=get_page_lengths(),
-        description='The number of objects to display per page',
+        description=_('The number of objects to display per page'),
         coerce=lambda x: int(x)
     ),
     'pagination.placement': UserPreference(
-        label='Paginator placement',
+        label=_('Paginator placement'),
         choices=(
             ('bottom', 'Bottom'),
             ('top', 'Top'),
             ('both', 'Both'),
         ),
-        description='Where the paginator controls will be displayed relative to a table',
+        description=_('Where the paginator controls will be displayed relative to a table'),
         default='bottom'
     ),
 
     # Miscellaneous
     'data_format': UserPreference(
-        label='Data format',
+        label=_('Data format'),
         choices=(
             ('json', 'JSON'),
             ('yaml', 'YAML'),

+ 19 - 18
netbox/tenancy/filtersets.py

@@ -1,5 +1,6 @@
 import django_filters
 from django.db.models import Q
+from django.utils.translation import gettext as _
 
 from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
 from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
@@ -25,13 +26,13 @@ __all__ = (
 class ContactGroupFilterSet(OrganizationalModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
-        label='Contact group (ID)',
+        label=_('Contact group (ID)'),
     )
     parent = django_filters.ModelMultipleChoiceFilter(
         field_name='parent__slug',
         queryset=ContactGroup.objects.all(),
         to_field_name='slug',
-        label='Contact group (slug)',
+        label=_('Contact group (slug)'),
     )
 
     class Meta:
@@ -51,14 +52,14 @@ class ContactFilterSet(NetBoxModelFilterSet):
         queryset=ContactGroup.objects.all(),
         field_name='group',
         lookup_expr='in',
-        label='Contact group (ID)',
+        label=_('Contact group (ID)'),
     )
     group = TreeNodeMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
         field_name='group',
         lookup_expr='in',
         to_field_name='slug',
-        label='Contact group (slug)',
+        label=_('Contact group (slug)'),
     )
 
     class Meta:
@@ -83,17 +84,17 @@ class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet):
     content_type = ContentTypeFilter()
     contact_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Contact.objects.all(),
-        label='Contact (ID)',
+        label=_('Contact (ID)'),
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ContactRole.objects.all(),
-        label='Contact role (ID)',
+        label=_('Contact role (ID)'),
     )
     role = django_filters.ModelMultipleChoiceFilter(
         field_name='role__slug',
         queryset=ContactRole.objects.all(),
         to_field_name='slug',
-        label='Contact role (slug)',
+        label=_('Contact role (slug)'),
     )
 
     class Meta:
@@ -105,18 +106,18 @@ class ContactModelFilterSet(django_filters.FilterSet):
     contact = django_filters.ModelMultipleChoiceFilter(
         field_name='contacts__contact',
         queryset=Contact.objects.all(),
-        label='Contact',
+        label=_('Contact'),
     )
     contact_role = django_filters.ModelMultipleChoiceFilter(
         field_name='contacts__role',
         queryset=ContactRole.objects.all(),
-        label='Contact Role'
+        label=_('Contact Role')
     )
     contact_group = TreeNodeMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
         field_name='contacts__contact__group',
         lookup_expr='in',
-        label='Contact group',
+        label=_('Contact group'),
     )
 
 
@@ -127,13 +128,13 @@ class ContactModelFilterSet(django_filters.FilterSet):
 class TenantGroupFilterSet(OrganizationalModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
-        label='Tenant group (ID)',
+        label=_('Tenant group (ID)'),
     )
     parent = django_filters.ModelMultipleChoiceFilter(
         field_name='parent__slug',
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',
-        label='Tenant group (slug)',
+        label=_('Tenant group (slug)'),
     )
 
     class Meta:
@@ -146,14 +147,14 @@ class TenantFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
         queryset=TenantGroup.objects.all(),
         field_name='group',
         lookup_expr='in',
-        label='Tenant group (ID)',
+        label=_('Tenant group (ID)'),
     )
     group = TreeNodeMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
         field_name='group',
         lookup_expr='in',
         to_field_name='slug',
-        label='Tenant group (slug)',
+        label=_('Tenant group (slug)'),
     )
 
     class Meta:
@@ -179,22 +180,22 @@ class TenancyFilterSet(django_filters.FilterSet):
         queryset=TenantGroup.objects.all(),
         field_name='tenant__group',
         lookup_expr='in',
-        label='Tenant Group (ID)',
+        label=_('Tenant Group (ID)'),
     )
     tenant_group = TreeNodeMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
         field_name='tenant__group',
         to_field_name='slug',
         lookup_expr='in',
-        label='Tenant Group (slug)',
+        label=_('Tenant Group (slug)'),
     )
     tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
-        label='Tenant (ID)',
+        label=_('Tenant (ID)'),
     )
     tenant = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         field_name='tenant__slug',
         to_field_name='slug',
-        label='Tenant (slug)',
+        label=_('Tenant (slug)'),
     )

+ 5 - 4
netbox/tenancy/forms/bulk_import.py

@@ -1,3 +1,4 @@
+from django.utils.translation import gettext as _
 from netbox.forms import NetBoxModelCSVForm
 from tenancy.models import *
 from utilities.forms import CSVModelChoiceField, SlugField
@@ -20,7 +21,7 @@ class TenantGroupCSVForm(NetBoxModelCSVForm):
         queryset=TenantGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent group'
+        help_text=_('Parent group')
     )
     slug = SlugField()
 
@@ -35,7 +36,7 @@ class TenantCSVForm(NetBoxModelCSVForm):
         queryset=TenantGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned group'
+        help_text=_('Assigned group')
     )
 
     class Meta:
@@ -52,7 +53,7 @@ class ContactGroupCSVForm(NetBoxModelCSVForm):
         queryset=ContactGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent group'
+        help_text=_('Parent group')
     )
     slug = SlugField()
 
@@ -74,7 +75,7 @@ class ContactCSVForm(NetBoxModelCSVForm):
         queryset=ContactGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned group'
+        help_text=_('Assigned group')
     )
 
     class Meta:

+ 6 - 5
netbox/users/admin/forms.py

@@ -3,6 +3,7 @@ from django.contrib.auth.models import Group, User
 from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import FieldError, ValidationError
+from django.utils.translation import gettext as _
 
 from users.constants import CONSTRAINT_TOKEN_USER, OBJECTPERMISSION_OBJECT_TYPES
 from users.models import ObjectPermission, Token
@@ -46,7 +47,7 @@ class GroupAdminForm(forms.ModelForm):
 class TokenAdminForm(forms.ModelForm):
     key = forms.CharField(
         required=False,
-        help_text="If no key is provided, one will be generated automatically."
+        help_text=_("If no key is provided, one will be generated automatically.")
     )
 
     class Meta:
@@ -70,10 +71,10 @@ class ObjectPermissionForm(forms.ModelForm):
         model = ObjectPermission
         exclude = []
         help_texts = {
-            'actions': 'Actions granted in addition to those listed above',
-            'constraints': 'JSON expression of a queryset filter that will return only permitted objects. Leave null '
-                           'to match all objects of this type. A list of multiple objects will result in a logical OR '
-                           'operation.'
+            'actions': _('Actions granted in addition to those listed above'),
+            'constraints': _('JSON expression of a queryset filter that will return only permitted objects. Leave null '
+                             'to match all objects of this type. A list of multiple objects will result in a logical OR '
+                             'operation.')
         }
         labels = {
             'actions': 'Additional actions'

+ 13 - 12
netbox/users/filtersets.py

@@ -1,6 +1,7 @@
 import django_filters
 from django.contrib.auth.models import Group, User
 from django.db.models import Q
+from django.utils.translation import gettext as _
 
 from netbox.filtersets import BaseFilterSet
 from users.models import ObjectPermission, Token
@@ -15,7 +16,7 @@ __all__ = (
 class GroupFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
 
     class Meta:
@@ -31,18 +32,18 @@ class GroupFilterSet(BaseFilterSet):
 class UserFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     group_id = django_filters.ModelMultipleChoiceFilter(
         field_name='groups',
         queryset=Group.objects.all(),
-        label='Group',
+        label=_('Group'),
     )
     group = django_filters.ModelMultipleChoiceFilter(
         field_name='groups__name',
         queryset=Group.objects.all(),
         to_field_name='name',
-        label='Group (name)',
+        label=_('Group (name)'),
     )
 
     class Meta:
@@ -63,18 +64,18 @@ class UserFilterSet(BaseFilterSet):
 class TokenFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     user_id = django_filters.ModelMultipleChoiceFilter(
         field_name='user',
         queryset=User.objects.all(),
-        label='User',
+        label=_('User'),
     )
     user = django_filters.ModelMultipleChoiceFilter(
         field_name='user__username',
         queryset=User.objects.all(),
         to_field_name='username',
-        label='User (name)',
+        label=_('User (name)'),
     )
     created = django_filters.DateTimeFilter()
     created__gte = django_filters.DateTimeFilter(
@@ -111,29 +112,29 @@ class TokenFilterSet(BaseFilterSet):
 class ObjectPermissionFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
         method='search',
-        label='Search',
+        label=_('Search'),
     )
     user_id = django_filters.ModelMultipleChoiceFilter(
         field_name='users',
         queryset=User.objects.all(),
-        label='User',
+        label=_('User'),
     )
     user = django_filters.ModelMultipleChoiceFilter(
         field_name='users__username',
         queryset=User.objects.all(),
         to_field_name='username',
-        label='User (name)',
+        label=_('User (name)'),
     )
     group_id = django_filters.ModelMultipleChoiceFilter(
         field_name='groups',
         queryset=Group.objects.all(),
-        label='Group',
+        label=_('Group'),
     )
     group = django_filters.ModelMultipleChoiceFilter(
         field_name='groups__name',
         queryset=Group.objects.all(),
         to_field_name='name',
-        label='Group (name)',
+        label=_('Group (name)'),
     )
 
     class Meta:

+ 5 - 4
netbox/users/forms.py

@@ -3,6 +3,7 @@ from django.conf import settings
 from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
 from django.contrib.postgres.forms import SimpleArrayField
 from django.utils.html import mark_safe
+from django.utils.translation import gettext as _
 
 from ipam.formfields import IPNetworkFormField
 from netbox.preferences import PREFERENCES
@@ -100,14 +101,14 @@ class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMe
 class TokenForm(BootstrapMixin, forms.ModelForm):
     key = forms.CharField(
         required=False,
-        help_text="If no key is provided, one will be generated automatically."
+        help_text=_("If no key is provided, one will be generated automatically.")
     )
     allowed_ips = SimpleArrayField(
         base_field=IPNetworkFormField(),
         required=False,
-        label='Allowed IPs',
-        help_text='Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '
-                  'Example: <code>10.1.1.0/24,192.168.10.16/32,2001:db8:1::/64</code>',
+        label=_('Allowed IPs'),
+        help_text=_('Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '
+                    'Example: <code>10.1.1.0/24,192.168.10.16/32,2001:db8:1::/64</code>'),
     )
 
     class Meta:

+ 6 - 5
netbox/users/models.py

@@ -10,6 +10,7 @@ from django.db import models
 from django.db.models.signals import post_save
 from django.dispatch import receiver
 from django.utils import timezone
+from django.utils.translation import gettext as _
 from netaddr import IPNetwork
 
 from ipam.fields import IPNetworkField
@@ -216,7 +217,7 @@ class Token(models.Model):
     )
     write_enabled = models.BooleanField(
         default=True,
-        help_text='Permit create/update/delete operations using this key'
+        help_text=_('Permit create/update/delete operations using this key')
     )
     description = models.CharField(
         max_length=200,
@@ -227,8 +228,8 @@ class Token(models.Model):
         blank=True,
         null=True,
         verbose_name='Allowed IPs',
-        help_text='Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '
-                  'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"',
+        help_text=_('Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '
+                    'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"'),
     )
 
     def __str__(self):
@@ -304,12 +305,12 @@ class ObjectPermission(models.Model):
     )
     actions = ArrayField(
         base_field=models.CharField(max_length=30),
-        help_text="The list of actions granted by this permission"
+        help_text=_("The list of actions granted by this permission")
     )
     constraints = models.JSONField(
         blank=True,
         null=True,
-        help_text="Queryset filter matching the applicable objects of the selected type(s)"
+        help_text=_("Queryset filter matching the applicable objects of the selected type(s)")
     )
 
     objects = RestrictedQuerySet.as_manager()

+ 4 - 3
netbox/utilities/forms/fields/csv.py

@@ -5,6 +5,7 @@ from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
 from django.db.models import Q
+from django.utils.translation import gettext as _
 
 from utilities.choices import unpack_grouped_choices
 from utilities.forms.utils import parse_csv, validate_csv
@@ -50,9 +51,9 @@ class CSVDataField(forms.CharField):
         if not self.initial:
             self.initial = ','.join(self.required_fields) + '\n'
         if not self.help_text:
-            self.help_text = 'Enter the list of column headers followed by one line per record to be imported, using ' \
-                             'commas to separate values. Multi-line data and values containing commas may be wrapped ' \
-                             'in double quotes.'
+            self.help_text = _('Enter the list of column headers followed by one line per record to be imported, using '
+                               'commas to separate values. Multi-line data and values containing commas may be wrapped '
+                               'in double quotes.')
 
     def to_python(self, value):
         reader = csv.reader(StringIO(value.strip()))

+ 3 - 2
netbox/utilities/forms/fields/expandable.py

@@ -1,6 +1,7 @@
 import re
 
 from django import forms
+from django.utils.translation import gettext as _
 
 from utilities.forms.constants import *
 from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipaddress_pattern
@@ -42,8 +43,8 @@ class ExpandableIPAddressField(forms.CharField):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         if not self.help_text:
-            self.help_text = 'Specify a numeric range to create multiple IPs.<br />'\
-                             'Example: <code>192.0.2.[1,5,100-254]/24</code>'
+            self.help_text = _('Specify a numeric range to create multiple IPs.<br />'
+                               'Example: <code>192.0.2.[1,5,100-254]/24</code>')
 
     def to_python(self, value):
         # Hackish address family detection but it's all we have to work with

+ 3 - 2
netbox/utilities/forms/fields/fields.py

@@ -4,6 +4,7 @@ from django import forms
 from django.db.models import Count
 from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
 from django.templatetags.static import static
+from django.utils.translation import gettext as _
 from netaddr import AddrFormatError, EUI
 
 from utilities.forms import widgets
@@ -45,7 +46,7 @@ class SlugField(forms.SlugField):
         slug_source: Name of the form field from which the slug value will be derived
     """
     widget = widgets.SlugWidget
-    help_text = "URL-friendly unique shorthand"
+    help_text = _("URL-friendly unique shorthand")
 
     def __init__(self, *, slug_source='name', help_text=help_text, **kwargs):
         super().__init__(help_text=help_text, **kwargs)
@@ -97,7 +98,7 @@ class JSONField(_JSONField):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         if not self.help_text:
-            self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
+            self.help_text = _('Enter context data in <a href="https://json.org/">JSON</a> format.')
             self.widget.attrs['placeholder'] = ''
             self.widget.attrs['class'] = 'font-monospace'
 

+ 7 - 6
netbox/utilities/forms/forms.py

@@ -5,8 +5,9 @@ from io import StringIO
 
 import yaml
 from django import forms
-from utilities.forms.utils import parse_csv
+from django.utils.translation import gettext as _
 
+from utilities.forms.utils import parse_csv
 from .choices import ImportFormatChoices
 from .widgets import APISelect, APISelectMultiple, ClearableFileInput, StaticSelect
 
@@ -103,7 +104,7 @@ class BulkRenameForm(BootstrapMixin, forms.Form):
     use_regex = forms.BooleanField(
         required=False,
         initial=True,
-        label='Use regular expressions'
+        label=_('Use regular expressions')
     )
 
     def clean(self):
@@ -145,7 +146,7 @@ class ImportForm(BootstrapMixin, forms.Form):
     data = forms.CharField(
         required=False,
         widget=forms.Textarea(attrs={'class': 'font-monospace'}),
-        help_text="Enter object data in CSV, JSON or YAML format."
+        help_text=_("Enter object data in CSV, JSON or YAML format.")
     )
     data_file = forms.FileField(
         label="Data file",
@@ -219,7 +220,7 @@ class FilterForm(BootstrapMixin, forms.Form):
     """
     q = forms.CharField(
         required=False,
-        label='Search'
+        label=_('Search')
     )
 
 
@@ -233,7 +234,7 @@ class TableConfigForm(BootstrapMixin, forms.Form):
         widget=forms.SelectMultiple(
             attrs={'size': 10, 'class': 'form-select'}
         ),
-        label='Available Columns'
+        label=_('Available Columns')
     )
     columns = forms.MultipleChoiceField(
         choices=[],
@@ -241,7 +242,7 @@ class TableConfigForm(BootstrapMixin, forms.Form):
         widget=forms.SelectMultiple(
             attrs={'size': 10, 'class': 'form-select'}
         ),
-        label='Selected Columns'
+        label=_('Selected Columns')
     )
 
     def __init__(self, table, *args, **kwargs):

+ 40 - 39
netbox/virtualization/filtersets.py

@@ -1,5 +1,6 @@
 import django_filters
 from django.db.models import Q
+from django.utils.translation import gettext as _
 
 from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
 from extras.filtersets import LocalConfigContextFilterSet
@@ -38,57 +39,57 @@ class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
         queryset=Region.objects.all(),
         field_name='site__region',
         lookup_expr='in',
-        label='Region (ID)',
+        label=_('Region (ID)'),
     )
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
         lookup_expr='in',
         to_field_name='slug',
-        label='Region (slug)',
+        label=_('Region (slug)'),
     )
     site_group_id = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='site__group',
         lookup_expr='in',
-        label='Site group (ID)',
+        label=_('Site group (ID)'),
     )
     site_group = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='site__group',
         lookup_expr='in',
         to_field_name='slug',
-        label='Site group (slug)',
+        label=_('Site group (slug)'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
-        label='Site (ID)',
+        label=_('Site (ID)'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='site__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
     group_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ClusterGroup.objects.all(),
-        label='Parent group (ID)',
+        label=_('Parent group (ID)'),
     )
     group = django_filters.ModelMultipleChoiceFilter(
         field_name='group__slug',
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
-        label='Parent group (slug)',
+        label=_('Parent group (slug)'),
     )
     type_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ClusterType.objects.all(),
-        label='Cluster type (ID)',
+        label=_('Cluster type (ID)'),
     )
     type = django_filters.ModelMultipleChoiceFilter(
         field_name='type__slug',
         queryset=ClusterType.objects.all(),
         to_field_name='slug',
-        label='Cluster type (slug)',
+        label=_('Cluster type (slug)'),
     )
     status = django_filters.MultipleChoiceFilter(
         choices=ClusterStatusChoices,
@@ -121,111 +122,111 @@ class VirtualMachineFilterSet(
     cluster_group_id = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster__group',
         queryset=ClusterGroup.objects.all(),
-        label='Cluster group (ID)',
+        label=_('Cluster group (ID)'),
     )
     cluster_group = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster__group__slug',
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
-        label='Cluster group (slug)',
+        label=_('Cluster group (slug)'),
     )
     cluster_type_id = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster__type',
         queryset=ClusterType.objects.all(),
-        label='Cluster type (ID)',
+        label=_('Cluster type (ID)'),
     )
     cluster_type = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster__type__slug',
         queryset=ClusterType.objects.all(),
         to_field_name='slug',
-        label='Cluster type (slug)',
+        label=_('Cluster type (slug)'),
     )
     cluster_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Cluster.objects.all(),
-        label='Cluster (ID)',
+        label=_('Cluster (ID)'),
     )
     cluster = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster__name',
         queryset=Cluster.objects.all(),
         to_field_name='name',
-        label='Cluster',
+        label=_('Cluster'),
     )
     device_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Device.objects.all(),
-        label='Device (ID)',
+        label=_('Device (ID)'),
     )
     device = django_filters.ModelMultipleChoiceFilter(
         field_name='device__name',
         queryset=Device.objects.all(),
         to_field_name='name',
-        label='Device',
+        label=_('Device'),
     )
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
         lookup_expr='in',
-        label='Region (ID)',
+        label=_('Region (ID)'),
     )
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
         lookup_expr='in',
         to_field_name='slug',
-        label='Region (slug)',
+        label=_('Region (slug)'),
     )
     site_group_id = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='site__group',
         lookup_expr='in',
-        label='Site group (ID)',
+        label=_('Site group (ID)'),
     )
     site_group = TreeNodeMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         field_name='site__group',
         lookup_expr='in',
         to_field_name='slug',
-        label='Site group (slug)',
+        label=_('Site group (slug)'),
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
-        label='Site (ID)',
+        label=_('Site (ID)'),
     )
     site = django_filters.ModelMultipleChoiceFilter(
         field_name='site__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
-        label='Site (slug)',
+        label=_('Site (slug)'),
     )
     name = MultiValueCharFilter(
         lookup_expr='iexact'
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=DeviceRole.objects.all(),
-        label='Role (ID)',
+        label=_('Role (ID)'),
     )
     role = django_filters.ModelMultipleChoiceFilter(
         field_name='role__slug',
         queryset=DeviceRole.objects.all(),
         to_field_name='slug',
-        label='Role (slug)',
+        label=_('Role (slug)'),
     )
     platform_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Platform.objects.all(),
-        label='Platform (ID)',
+        label=_('Platform (ID)'),
     )
     platform = django_filters.ModelMultipleChoiceFilter(
         field_name='platform__slug',
         queryset=Platform.objects.all(),
         to_field_name='slug',
-        label='Platform (slug)',
+        label=_('Platform (slug)'),
     )
     mac_address = MultiValueMACAddressFilter(
         field_name='interfaces__mac_address',
-        label='MAC address',
+        label=_('MAC address'),
     )
     has_primary_ip = django_filters.BooleanFilter(
         method='_has_primary_ip',
-        label='Has a primary IP',
+        label=_('Has a primary IP'),
     )
 
     class Meta:
@@ -251,48 +252,48 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet):
     cluster_id = django_filters.ModelMultipleChoiceFilter(
         field_name='virtual_machine__cluster',
         queryset=Cluster.objects.all(),
-        label='Cluster (ID)',
+        label=_('Cluster (ID)'),
     )
     cluster = django_filters.ModelMultipleChoiceFilter(
         field_name='virtual_machine__cluster__name',
         queryset=Cluster.objects.all(),
         to_field_name='name',
-        label='Cluster',
+        label=_('Cluster'),
     )
     virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
         field_name='virtual_machine',
         queryset=VirtualMachine.objects.all(),
-        label='Virtual machine (ID)',
+        label=_('Virtual machine (ID)'),
     )
     virtual_machine = django_filters.ModelMultipleChoiceFilter(
         field_name='virtual_machine__name',
         queryset=VirtualMachine.objects.all(),
         to_field_name='name',
-        label='Virtual machine',
+        label=_('Virtual machine'),
     )
     parent_id = django_filters.ModelMultipleChoiceFilter(
         field_name='parent',
         queryset=VMInterface.objects.all(),
-        label='Parent interface (ID)',
+        label=_('Parent interface (ID)'),
     )
     bridge_id = django_filters.ModelMultipleChoiceFilter(
         field_name='bridge',
         queryset=VMInterface.objects.all(),
-        label='Bridged interface (ID)',
+        label=_('Bridged interface (ID)'),
     )
     mac_address = MultiValueMACAddressFilter(
-        label='MAC address',
+        label=_('MAC address'),
     )
     vrf_id = django_filters.ModelMultipleChoiceFilter(
         field_name='vrf',
         queryset=VRF.objects.all(),
-        label='VRF',
+        label=_('VRF'),
     )
     vrf = django_filters.ModelMultipleChoiceFilter(
         field_name='vrf__rd',
         queryset=VRF.objects.all(),
         to_field_name='rd',
-        label='VRF (RD)',
+        label=_('VRF (RD)'),
     )
 
     class Meta:

+ 2 - 1
netbox/virtualization/forms/bulk_create.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from utilities.forms import BootstrapMixin, ExpandableNameField, form_from_model
 from virtualization.models import VMInterface, VirtualMachine
@@ -14,7 +15,7 @@ class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
         widget=forms.MultipleHiddenInput()
     )
     name = ExpandableNameField(
-        label='Name'
+        label=_('Name')
     )
 
     def clean_tags(self):

+ 11 - 10
netbox/virtualization/forms/bulk_edit.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from dcim.choices import InterfaceModeChoices
 from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
@@ -90,7 +91,7 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
     )
     comments = CommentField(
         widget=SmallTextarea,
-        label='Comments'
+        label=_('Comments')
     )
 
     model = Cluster
@@ -147,15 +148,15 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
     )
     vcpus = forms.IntegerField(
         required=False,
-        label='vCPUs'
+        label=_('vCPUs')
     )
     memory = forms.IntegerField(
         required=False,
-        label='Memory (MB)'
+        label=_('Memory (MB)')
     )
     disk = forms.IntegerField(
         required=False,
-        label='Disk (GB)'
+        label=_('Disk (GB)')
     )
     description = forms.CharField(
         max_length=200,
@@ -163,7 +164,7 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
     )
     comments = CommentField(
         widget=SmallTextarea,
-        label='Comments'
+        label=_('Comments')
     )
 
     model = VirtualMachine
@@ -199,7 +200,7 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         min_value=INTERFACE_MTU_MIN,
         max_value=INTERFACE_MTU_MAX,
-        label='MTU'
+        label=_('MTU')
     )
     description = forms.CharField(
         max_length=100,
@@ -213,7 +214,7 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
     vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
-        label='VLAN group'
+        label=_('VLAN group')
     )
     untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
@@ -221,7 +222,7 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
         query_params={
             'group_id': '$vlan_group',
         },
-        label='Untagged VLAN'
+        label=_('Untagged VLAN')
     )
     tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
@@ -229,12 +230,12 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
         query_params={
             'group_id': '$vlan_group',
         },
-        label='Tagged VLANs'
+        label=_('Tagged VLANs')
     )
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
 
     model = VMInterface

+ 17 - 16
netbox/virtualization/forms/bulk_import.py

@@ -1,5 +1,6 @@
 from dcim.choices import InterfaceModeChoices
 from dcim.models import Device, DeviceRole, Platform, Site
+from django.utils.translation import gettext as _
 from ipam.models import VRF
 from netbox.forms import NetBoxModelCSVForm
 from tenancy.models import Tenant
@@ -36,29 +37,29 @@ class ClusterCSVForm(NetBoxModelCSVForm):
     type = CSVModelChoiceField(
         queryset=ClusterType.objects.all(),
         to_field_name='name',
-        help_text='Type of cluster'
+        help_text=_('Type of cluster')
     )
     group = CSVModelChoiceField(
         queryset=ClusterGroup.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned cluster group'
+        help_text=_('Assigned cluster group')
     )
     status = CSVChoiceField(
         choices=ClusterStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned site'
+        help_text=_('Assigned site')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
 
     class Meta:
@@ -69,25 +70,25 @@ class ClusterCSVForm(NetBoxModelCSVForm):
 class VirtualMachineCSVForm(NetBoxModelCSVForm):
     status = CSVChoiceField(
         choices=VirtualMachineStatusChoices,
-        help_text='Operational status'
+        help_text=_('Operational status')
     )
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned site'
+        help_text=_('Assigned site')
     )
     cluster = CSVModelChoiceField(
         queryset=Cluster.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned cluster'
+        help_text=_('Assigned cluster')
     )
     device = CSVModelChoiceField(
         queryset=Device.objects.all(),
         to_field_name='name',
         required=False,
-        help_text='Assigned device within cluster'
+        help_text=_('Assigned device within cluster')
     )
     role = CSVModelChoiceField(
         queryset=DeviceRole.objects.filter(
@@ -95,19 +96,19 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
         ),
         required=False,
         to_field_name='name',
-        help_text='Functional role'
+        help_text=_('Functional role')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     platform = CSVModelChoiceField(
         queryset=Platform.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned platform'
+        help_text=_('Assigned platform')
     )
 
     class Meta:
@@ -127,24 +128,24 @@ class VMInterfaceCSVForm(NetBoxModelCSVForm):
         queryset=VMInterface.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent interface'
+        help_text=_('Parent interface')
     )
     bridge = CSVModelChoiceField(
         queryset=VMInterface.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Bridged interface'
+        help_text=_('Bridged interface')
     )
     mode = CSVChoiceField(
         choices=InterfaceModeChoices,
         required=False,
-        help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
+        help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
     )
     vrf = CSVModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
         to_field_name='rd',
-        help_text='Assigned VRF'
+        help_text=_('Assigned VRF')
     )
 
     class Meta:

+ 2 - 2
netbox/virtualization/forms/filtersets.py

@@ -160,11 +160,11 @@ class VirtualMachineFilterForm(
     )
     mac_address = forms.CharField(
         required=False,
-        label='MAC address'
+        label=_('MAC address')
     )
     has_primary_ip = forms.NullBooleanField(
         required=False,
-        label='Has a primary IP',
+        label=_('Has a primary IP'),
         widget=StaticSelect(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )

+ 10 - 9
netbox/virtualization/forms/model_forms.py

@@ -1,6 +1,7 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
+from django.utils.translation import gettext as _
 
 from dcim.forms.common import InterfaceCommonForm
 from dcim.forms.model_forms import INTERFACE_MODE_HELP_TEXT
@@ -204,7 +205,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
             'cluster_id': '$cluster',
             'site_id': '$site',
         },
-        help_text="Optionally pin this VM to a specific host device within the cluster"
+        help_text=_("Optionally pin this VM to a specific host device within the cluster")
     )
     role = DynamicModelChoiceField(
         queryset=DeviceRole.objects.all(),
@@ -240,8 +241,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
             'local_context_data',
         ]
         help_texts = {
-            'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
-                                  "config context",
+            'local_context_data': _("Local config context data overwrites all sources contexts in the final rendered "
+                                    "config context"),
         }
         widgets = {
             "status": StaticSelect(),
@@ -297,7 +298,7 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     parent = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         required=False,
-        label='Parent interface',
+        label=_('Parent interface'),
         query_params={
             'virtual_machine_id': '$virtual_machine',
         }
@@ -305,7 +306,7 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     bridge = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         required=False,
-        label='Bridged interface',
+        label=_('Bridged interface'),
         query_params={
             'virtual_machine_id': '$virtual_machine',
         }
@@ -313,12 +314,12 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
-        label='VLAN group'
+        label=_('VLAN group')
     )
     untagged_vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        label='Untagged VLAN',
+        label=_('Untagged VLAN'),
         query_params={
             'group_id': '$vlan_group',
             'available_on_virtualmachine': '$virtual_machine',
@@ -327,7 +328,7 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     tagged_vlans = DynamicModelMultipleChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        label='Tagged VLANs',
+        label=_('Tagged VLANs'),
         query_params={
             'group_id': '$vlan_group',
             'available_on_virtualmachine': '$virtual_machine',
@@ -336,7 +337,7 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
 
     fieldsets = (

+ 6 - 5
netbox/wireless/forms/bulk_edit.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from dcim.choices import LinkStatusChoices
 from ipam.models import VLAN
@@ -45,12 +46,12 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        label='VLAN'
+        label=_('VLAN')
     )
     ssid = forms.CharField(
         max_length=SSID_MAX_LENGTH,
         required=False,
-        label='SSID'
+        label=_('SSID')
     )
     tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
@@ -66,7 +67,7 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
     )
     auth_psk = forms.CharField(
         required=False,
-        label='Pre-shared key'
+        label=_('Pre-shared key')
     )
     description = forms.CharField(
         max_length=200,
@@ -91,7 +92,7 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
     ssid = forms.CharField(
         max_length=SSID_MAX_LENGTH,
         required=False,
-        label='SSID'
+        label=_('SSID')
     )
     status = forms.ChoiceField(
         choices=add_blank_choice(LinkStatusChoices),
@@ -111,7 +112,7 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
     )
     auth_psk = forms.CharField(
         required=False,
-        label='Pre-shared key'
+        label=_('Pre-shared key')
     )
     description = forms.CharField(
         max_length=200,

+ 11 - 10
netbox/wireless/forms/bulk_import.py

@@ -1,3 +1,4 @@
+from django.utils.translation import gettext as _
 from dcim.choices import LinkStatusChoices
 from dcim.models import Interface
 from ipam.models import VLAN
@@ -19,7 +20,7 @@ class WirelessLANGroupCSVForm(NetBoxModelCSVForm):
         queryset=WirelessLANGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Parent group'
+        help_text=_('Parent group')
     )
     slug = SlugField()
 
@@ -33,7 +34,7 @@ class WirelessLANCSVForm(NetBoxModelCSVForm):
         queryset=WirelessLANGroup.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned group'
+        help_text=_('Assigned group')
     )
     status = CSVChoiceField(
         choices=WirelessLANStatusChoices,
@@ -43,23 +44,23 @@ class WirelessLANCSVForm(NetBoxModelCSVForm):
         queryset=VLAN.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Bridged VLAN'
+        help_text=_('Bridged VLAN')
     )
     tenant = CSVModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     auth_type = CSVChoiceField(
         choices=WirelessAuthTypeChoices,
         required=False,
-        help_text='Authentication type'
+        help_text=_('Authentication type')
     )
     auth_cipher = CSVChoiceField(
         choices=WirelessAuthCipherChoices,
         required=False,
-        help_text='Authentication cipher'
+        help_text=_('Authentication cipher')
     )
 
     class Meta:
@@ -73,7 +74,7 @@ class WirelessLANCSVForm(NetBoxModelCSVForm):
 class WirelessLinkCSVForm(NetBoxModelCSVForm):
     status = CSVChoiceField(
         choices=LinkStatusChoices,
-        help_text='Connection status'
+        help_text=_('Connection status')
     )
     interface_a = CSVModelChoiceField(
         queryset=Interface.objects.all()
@@ -85,17 +86,17 @@ class WirelessLinkCSVForm(NetBoxModelCSVForm):
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='Assigned tenant'
+        help_text=_('Assigned tenant')
     )
     auth_type = CSVChoiceField(
         choices=WirelessAuthTypeChoices,
         required=False,
-        help_text='Authentication type'
+        help_text=_('Authentication type')
     )
     auth_cipher = CSVChoiceField(
         choices=WirelessAuthCipherChoices,
         required=False,
-        help_text='Authentication cipher'
+        help_text=_('Authentication cipher')
     )
 
     class Meta:

+ 2 - 2
netbox/wireless/forms/filtersets.py

@@ -35,7 +35,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     )
     ssid = forms.CharField(
         required=False,
-        label='SSID'
+        label=_('SSID')
     )
     group_id = DynamicModelMultipleChoiceField(
         queryset=WirelessLANGroup.objects.all(),
@@ -74,7 +74,7 @@ class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     )
     ssid = forms.CharField(
         required=False,
-        label='SSID'
+        label=_('SSID')
     )
     status = forms.ChoiceField(
         required=False,

+ 11 - 10
netbox/wireless/forms/model_forms.py

@@ -1,3 +1,4 @@
+from django.utils.translation import gettext as _
 from dcim.models import Device, Interface, Location, Region, Site, SiteGroup
 from ipam.models import VLAN, VLANGroup
 from netbox.forms import NetBoxModelForm
@@ -63,7 +64,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
     vlan_group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
-        label='VLAN group',
+        label=_('VLAN group'),
         null_option='None',
         query_params={
             'site': '$site'
@@ -75,7 +76,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        label='VLAN',
+        label=_('VLAN'),
         query_params={
             'site_id': '$site',
             'group_id': '$vlan_group',
@@ -107,7 +108,7 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
     site_a = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
-        label='Site',
+        label=_('Site'),
         initial_params={
             'devices': '$device_a',
         }
@@ -118,7 +119,7 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
             'site_id': '$site_a',
         },
         required=False,
-        label='Location',
+        label=_('Location'),
         initial_params={
             'devices': '$device_a',
         }
@@ -130,7 +131,7 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
             'location_id': '$location_a',
         },
         required=False,
-        label='Device',
+        label=_('Device'),
         initial_params={
             'interfaces': '$interface_a'
         }
@@ -142,12 +143,12 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
             'device_id': '$device_a',
         },
         disabled_indicator='_occupied',
-        label='Interface'
+        label=_('Interface')
     )
     site_b = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
-        label='Site',
+        label=_('Site'),
         initial_params={
             'devices': '$device_b',
         }
@@ -158,7 +159,7 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
             'site_id': '$site_b',
         },
         required=False,
-        label='Location',
+        label=_('Location'),
         initial_params={
             'devices': '$device_b',
         }
@@ -170,7 +171,7 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
             'location_id': '$location_b',
         },
         required=False,
-        label='Device',
+        label=_('Device'),
         initial_params={
             'interfaces': '$interface_b'
         }
@@ -182,7 +183,7 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
             'device_id': '$device_b',
         },
         disabled_indicator='_occupied',
-        label='Interface'
+        label=_('Interface')
     )
     comments = CommentField()
 

Некоторые файлы не были показаны из-за большого количества измененных файлов