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

Closes #13149: Wrap form field labels with gettext_lazy()

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Arthur Hanson 2 лет назад
Родитель
Сommit
b7a9649269
51 измененных файлов с 1350 добавлено и 589 удалено
  1. 22 16
      netbox/circuits/forms/bulk_edit.py
  2. 10 1
      netbox/circuits/forms/bulk_import.py
  3. 15 10
      netbox/circuits/forms/filtersets.py
  4. 14 7
      netbox/circuits/forms/model_forms.py
  5. 6 4
      netbox/core/forms/bulk_edit.py
  6. 17 5
      netbox/core/forms/filtersets.py
  7. 1 1
      netbox/core/forms/mixins.py
  8. 5 4
      netbox/core/forms/model_forms.py
  9. 3 1
      netbox/dcim/forms/bulk_create.py
  10. 166 48
      netbox/dcim/forms/bulk_edit.py
  11. 119 2
      netbox/dcim/forms/bulk_import.py
  12. 15 8
      netbox/dcim/forms/common.py
  13. 1 1
      netbox/dcim/forms/connections.py
  14. 194 129
      netbox/dcim/forms/filtersets.py
  15. 4 1
      netbox/dcim/forms/formsets.py
  16. 101 43
      netbox/dcim/forms/model_forms.py
  17. 19 7
      netbox/dcim/forms/object_create.py
  18. 9 1
      netbox/dcim/forms/object_import.py
  19. 33 1
      netbox/extras/forms/bulk_edit.py
  20. 11 1
      netbox/extras/forms/bulk_import.py
  21. 38 18
      netbox/extras/forms/filtersets.py
  22. 2 0
      netbox/extras/forms/misc.py
  23. 70 44
      netbox/extras/forms/model_forms.py
  24. 2 2
      netbox/extras/forms/reports.py
  25. 2 2
      netbox/extras/forms/scripts.py
  26. 1 1
      netbox/ipam/forms/bulk_create.py
  27. 69 39
      netbox/ipam/forms/bulk_edit.py
  28. 52 8
      netbox/ipam/forms/bulk_import.py
  29. 49 37
      netbox/ipam/forms/filtersets.py
  30. 59 34
      netbox/ipam/forms/model_forms.py
  31. 7 2
      netbox/netbox/forms/base.py
  32. 18 3
      netbox/tenancy/forms/bulk_edit.py
  33. 6 1
      netbox/tenancy/forms/bulk_import.py
  34. 3 2
      netbox/tenancy/forms/filtersets.py
  35. 3 1
      netbox/tenancy/forms/forms.py
  36. 13 5
      netbox/tenancy/forms/model_forms.py
  37. 0 1
      netbox/users/forms/model_forms.py
  38. 4 2
      netbox/utilities/forms/fields/array.py
  39. 7 6
      netbox/utilities/forms/fields/csv.py
  40. 5 5
      netbox/utilities/forms/fields/expandable.py
  41. 13 12
      netbox/utilities/forms/fields/fields.py
  42. 1 1
      netbox/virtualization/forms/bulk_create.py
  43. 31 11
      netbox/virtualization/forms/bulk_edit.py
  44. 18 1
      netbox/virtualization/forms/bulk_import.py
  45. 18 15
      netbox/virtualization/forms/filtersets.py
  46. 31 17
      netbox/virtualization/forms/model_forms.py
  47. 4 1
      netbox/virtualization/forms/object_create.py
  48. 18 9
      netbox/wireless/forms/bulk_edit.py
  49. 14 1
      netbox/wireless/forms/bulk_import.py
  50. 15 7
      netbox/wireless/forms/filtersets.py
  51. 12 10
      netbox/wireless/forms/model_forms.py

+ 22 - 16
netbox/circuits/forms/bulk_edit.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
 from circuits.models import *
@@ -26,12 +26,11 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label=_('Comments')
-    )
+    comments = CommentField()
 
     model = Provider
     fieldsets = (
@@ -44,16 +43,16 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
 
 class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
     provider = DynamicModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label=_('Comments')
-    )
+    comments = CommentField()
 
     model = ProviderAccount
     fieldsets = (
@@ -66,6 +65,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
 
 class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
     provider = DynamicModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all(),
         required=False
     )
@@ -75,12 +75,11 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Service ID')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label=_('Comments')
-    )
+    comments = CommentField()
 
     model = ProviderNetwork
     fieldsets = (
@@ -93,6 +92,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
 
 class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -106,14 +106,17 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
 
 class CircuitBulkEditForm(NetBoxModelBulkEditForm):
     type = DynamicModelChoiceField(
+        label=_('Type'),
         queryset=CircuitType.objects.all(),
         required=False
     )
     provider = DynamicModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all(),
         required=False
     )
     provider_account = DynamicModelChoiceField(
+        label=_('Provider account'),
         queryset=ProviderAccount.objects.all(),
         required=False,
         query_params={
@@ -121,19 +124,23 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
         }
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(CircuitStatusChoices),
         required=False,
         initial=''
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     install_date = forms.DateField(
+        label=_('Install date'),
         required=False,
         widget=DatePicker()
     )
     termination_date = forms.DateField(
+        label=_('Termination date'),
         required=False,
         widget=DatePicker()
     )
@@ -145,18 +152,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
         )
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=100,
         required=False
     )
-    comments = CommentField(
-        label=_('Comments')
-    )
+    comments = CommentField()
 
     model = Circuit
     fieldsets = (
-        ('Circuit', ('provider', 'type', 'status', 'description')),
-        ('Service Parameters', ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
-        ('Tenancy', ('tenant',)),
+        (_('Circuit'), ('provider', 'type', 'status', 'description')),
+        (_('Service Parameters'), ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
+        (_('Tenancy'), ('tenant',)),
     )
     nullable_fields = (
         'tenant', 'commit_rate', 'description', 'comments',

+ 10 - 1
netbox/circuits/forms/bulk_import.py

@@ -3,7 +3,7 @@ from django import forms
 from circuits.choices import CircuitStatusChoices
 from circuits.models import *
 from dcim.models import Site
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import Tenant
 from utilities.forms import BootstrapMixin
@@ -31,6 +31,7 @@ class ProviderImportForm(NetBoxModelImportForm):
 
 class ProviderAccountImportForm(NetBoxModelImportForm):
     provider = CSVModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all(),
         to_field_name='name',
         help_text=_('Assigned provider')
@@ -45,6 +46,7 @@ class ProviderAccountImportForm(NetBoxModelImportForm):
 
 class ProviderNetworkImportForm(NetBoxModelImportForm):
     provider = CSVModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all(),
         to_field_name='name',
         help_text=_('Assigned provider')
@@ -67,26 +69,31 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
 
 class CircuitImportForm(NetBoxModelImportForm):
     provider = CSVModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all(),
         to_field_name='name',
         help_text=_('Assigned provider')
     )
     provider_account = CSVModelChoiceField(
+        label=_('Provider account'),
         queryset=ProviderAccount.objects.all(),
         to_field_name='name',
         help_text=_('Assigned provider account'),
         required=False
     )
     type = CSVModelChoiceField(
+        label=_('Type'),
         queryset=CircuitType.objects.all(),
         to_field_name='name',
         help_text=_('Type of circuit')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=CircuitStatusChoices,
         help_text=_('Operational status')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -103,11 +110,13 @@ class CircuitImportForm(NetBoxModelImportForm):
 
 class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm):
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name',
         required=False
     )
     provider_network = CSVModelChoiceField(
+        label=_('Provider network'),
         queryset=ProviderNetwork.objects.all(),
         to_field_name='name',
         required=False

+ 15 - 10
netbox/circuits/forms/filtersets.py

@@ -23,9 +23,9 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Provider
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region_id', 'site_group_id', 'site_id')),
-        ('ASN', ('asn',)),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
+        (_('ASN'), ('asn',)),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -62,7 +62,7 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
     model = ProviderAccount
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('provider_id', 'account')),
+        (_('Attributes'), ('provider_id', 'account')),
     )
     provider_id = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
@@ -70,6 +70,7 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
         label=_('Provider')
     )
     account = forms.CharField(
+        label=_('Account'),
         required=False
     )
     tag = TagFilterField(model)
@@ -79,7 +80,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
     model = ProviderNetwork
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('provider_id', 'service_id')),
+        (_('Attributes'), ('provider_id', 'service_id')),
     )
     provider_id = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
@@ -87,6 +88,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
         label=_('Provider')
     )
     service_id = forms.CharField(
+        label=_('Service id'),
         max_length=100,
         required=False
     )
@@ -102,11 +104,11 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
     model = Circuit
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Provider', ('provider_id', 'provider_account_id', 'provider_network_id')),
-        ('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
-        ('Location', ('region_id', 'site_group_id', 'site_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
+        (_('Provider'), ('provider_id', 'provider_account_id', 'provider_network_id')),
+        (_('Attributes'), ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
     type_id = DynamicModelMultipleChoiceField(
         queryset=CircuitType.objects.all(),
@@ -135,6 +137,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
         label=_('Provider network')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=CircuitStatusChoices,
         required=False
     )
@@ -158,10 +161,12 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
         label=_('Site')
     )
     install_date = forms.DateField(
+        label=_('Install date'),
         required=False,
         widget=DatePicker
     )
     termination_date = forms.DateField(
+        label=_('Termination date'),
         required=False,
         widget=DatePicker
     )

+ 14 - 7
netbox/circuits/forms/model_forms.py

@@ -1,4 +1,4 @@
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
 from circuits.models import *
@@ -29,7 +29,7 @@ class ProviderForm(NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
+        (_('Provider'), ('name', 'slug', 'asns', 'description', 'tags')),
     )
 
     class Meta:
@@ -41,6 +41,7 @@ class ProviderForm(NetBoxModelForm):
 
 class ProviderAccountForm(NetBoxModelForm):
     provider = DynamicModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all()
     )
     comments = CommentField()
@@ -54,12 +55,13 @@ class ProviderAccountForm(NetBoxModelForm):
 
 class ProviderNetworkForm(NetBoxModelForm):
     provider = DynamicModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all()
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Provider Network', ('provider', 'name', 'service_id', 'description', 'tags')),
+        (_('Provider Network'), ('provider', 'name', 'service_id', 'description', 'tags')),
     )
 
     class Meta:
@@ -73,7 +75,7 @@ class CircuitTypeForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Circuit Type', (
+        (_('Circuit Type'), (
             'name', 'slug', 'description', 'tags',
         )),
     )
@@ -87,10 +89,12 @@ class CircuitTypeForm(NetBoxModelForm):
 
 class CircuitForm(TenancyForm, NetBoxModelForm):
     provider = DynamicModelChoiceField(
+        label=_('Provider'),
         queryset=Provider.objects.all(),
         selector=True
     )
     provider_account = DynamicModelChoiceField(
+        label=_('Provider account'),
         queryset=ProviderAccount.objects.all(),
         required=False,
         query_params={
@@ -103,9 +107,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Circuit', ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
-        ('Service Parameters', ('install_date', 'termination_date', 'commit_rate')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('Circuit'), ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
+        (_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -125,15 +129,18 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
 
 class CircuitTerminationForm(NetBoxModelForm):
     circuit = DynamicModelChoiceField(
+        label=_('Circuit'),
         queryset=Circuit.objects.all(),
         selector=True
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         selector=True
     )
     provider_network = DynamicModelChoiceField(
+        label=_('Provider network'),
         queryset=ProviderNetwork.objects.all(),
         required=False,
         selector=True

+ 6 - 4
netbox/core/forms/bulk_edit.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from core.choices import DataSourceTypeChoices
 from core.models import *
@@ -15,6 +15,7 @@ __all__ = (
 
 class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
     type = forms.ChoiceField(
+        label=_('Type'),
         choices=add_blank_choice(DataSourceTypeChoices),
         required=False,
         initial=''
@@ -25,16 +26,17 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Enforce unique space')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label=_('Comments')
-    )
+    comments = CommentField()
     parameters = forms.JSONField(
+        label=_('Parameters'),
         required=False
     )
     ignore_rules = forms.CharField(
+        label=_('Ignore rules'),
         required=False,
         widget=forms.Textarea()
     )

+ 17 - 5
netbox/core/forms/filtersets.py

@@ -1,7 +1,7 @@
 from django import forms
 from django.contrib.auth import get_user_model
 from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from core.choices import *
 from core.models import *
@@ -23,17 +23,20 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
     model = DataSource
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Data Source', ('type', 'status')),
+        (_('Data Source'), ('type', 'status')),
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=DataSourceTypeChoices,
         required=False
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=DataSourceStatusChoices,
         required=False
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -45,7 +48,7 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
     model = DataFile
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('File', ('source_id',)),
+        (_('File'), ('source_id',)),
     )
     source_id = DynamicModelMultipleChoiceField(
         queryset=DataSource.objects.all(),
@@ -57,8 +60,8 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
 class JobFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Attributes', ('object_type', 'status')),
-        ('Creation', (
+        (_('Attributes'), ('object_type', 'status')),
+        (_('Creation'), (
             'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
             'started__after', 'completed__before', 'completed__after', 'user',
         )),
@@ -69,38 +72,47 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
         required=False,
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=JobStatusChoices,
         required=False
     )
     created__after = forms.DateTimeField(
+        label=_('Created after'),
         required=False,
         widget=DateTimePicker()
     )
     created__before = forms.DateTimeField(
+        label=_('Created before'),
         required=False,
         widget=DateTimePicker()
     )
     scheduled__after = forms.DateTimeField(
+        label=_('Scheduled after'),
         required=False,
         widget=DateTimePicker()
     )
     scheduled__before = forms.DateTimeField(
+        label=_('Scheduled before'),
         required=False,
         widget=DateTimePicker()
     )
     started__after = forms.DateTimeField(
+        label=_('Started after'),
         required=False,
         widget=DateTimePicker()
     )
     started__before = forms.DateTimeField(
+        label=_('Started before'),
         required=False,
         widget=DateTimePicker()
     )
     completed__after = forms.DateTimeField(
+        label=_('Completed after'),
         required=False,
         widget=DateTimePicker()
     )
     completed__before = forms.DateTimeField(
+        label=_('Completed before'),
         required=False,
         widget=DateTimePicker()
     )

+ 1 - 1
netbox/core/forms/mixins.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from core.models import DataFile, DataSource
 from utilities.forms.fields import DynamicModelChoiceField

+ 5 - 4
netbox/core/forms/model_forms.py

@@ -1,6 +1,7 @@
 import copy
 
 from django import forms
+from django.utils.translation import gettext_lazy as _
 
 from core.forms.mixins import SyncedDataMixin
 from core.models import *
@@ -38,11 +39,11 @@ class DataSourceForm(NetBoxModelForm):
     @property
     def fieldsets(self):
         fieldsets = [
-            ('Source', ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')),
+            (_('Source'), ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')),
         ]
         if self.backend_fields:
             fieldsets.append(
-                ('Backend Parameters', self.backend_fields)
+                (_('Backend Parameters'), self.backend_fields)
             )
 
         return fieldsets
@@ -79,8 +80,8 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
     )
 
     fieldsets = (
-        ('File Upload', ('upload_file',)),
-        ('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
+        (_('File Upload'), ('upload_file',)),
+        (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
     )
 
     class Meta:

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

@@ -1,7 +1,7 @@
 from django import forms
 
 from dcim.models import *
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 from extras.forms import CustomFieldsMixin
 from extras.models import Tag
 from utilities.forms import BootstrapMixin, form_from_model
@@ -32,10 +32,12 @@ class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCre
         widget=forms.MultipleHiddenInput()
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=100,
         required=False
     )
     tags = DynamicModelMultipleChoiceField(
+        label=_('Tags'),
         queryset=Tag.objects.all(),
         required=False
     )

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


+ 119 - 2
netbox/dcim/forms/bulk_import.py

@@ -3,7 +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 django.utils.translation import gettext_lazy as _
 
 from dcim.choices import *
 from dcim.constants import *
@@ -56,6 +56,7 @@ __all__ = (
 
 class RegionImportForm(NetBoxModelImportForm):
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=Region.objects.all(),
         required=False,
         to_field_name='name',
@@ -69,6 +70,7 @@ class RegionImportForm(NetBoxModelImportForm):
 
 class SiteGroupImportForm(NetBoxModelImportForm):
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=SiteGroup.objects.all(),
         required=False,
         to_field_name='name',
@@ -82,22 +84,26 @@ class SiteGroupImportForm(NetBoxModelImportForm):
 
 class SiteImportForm(NetBoxModelImportForm):
     status = CSVChoiceField(
+        label=_('Status'),
         choices=SiteStatusChoices,
         help_text=_('Operational status')
     )
     region = CSVModelChoiceField(
+        label=_('Region'),
         queryset=Region.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned region')
     )
     group = CSVModelChoiceField(
+        label=_('Group'),
         queryset=SiteGroup.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned group')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -119,11 +125,13 @@ class SiteImportForm(NetBoxModelImportForm):
 
 class LocationImportForm(NetBoxModelImportForm):
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name',
         help_text=_('Assigned site')
     )
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=Location.objects.all(),
         required=False,
         to_field_name='name',
@@ -133,10 +141,12 @@ class LocationImportForm(NetBoxModelImportForm):
         }
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=LocationStatusChoices,
         help_text=_('Operational status')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -161,45 +171,54 @@ class RackRoleImportForm(NetBoxModelImportForm):
 
 class RackImportForm(NetBoxModelImportForm):
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name'
     )
     location = CSVModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         required=False,
         to_field_name='name'
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Name of assigned tenant')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=RackStatusChoices,
         help_text=_('Operational status')
     )
     role = CSVModelChoiceField(
+        label=_('Role'),
         queryset=RackRole.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Name of assigned role')
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=RackTypeChoices,
         required=False,
         help_text=_('Rack type')
     )
     width = forms.ChoiceField(
+        label=_('Width'),
         choices=RackWidthChoices,
         help_text=_('Rail-to-rail width (in inches)')
     )
     outer_unit = CSVChoiceField(
+        label=_('Outer unit'),
         choices=RackDimensionUnitChoices,
         required=False,
         help_text=_('Unit for outer dimensions')
     )
     weight_unit = CSVChoiceField(
+        label=_('Weight unit'),
         choices=WeightUnitChoices,
         required=False,
         help_text=_('Unit for rack weights')
@@ -225,27 +244,32 @@ class RackImportForm(NetBoxModelImportForm):
 
 class RackReservationImportForm(NetBoxModelImportForm):
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name',
         help_text=_('Parent site')
     )
     location = CSVModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_("Rack's location (if any)")
     )
     rack = CSVModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         to_field_name='name',
         help_text=_('Rack')
     )
     units = SimpleArrayField(
+        label=_('Units'),
         base_field=forms.IntegerField(),
         required=True,
         help_text=_('Comma-separated list of individual unit numbers')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -282,21 +306,25 @@ class ManufacturerImportForm(NetBoxModelImportForm):
 
 class DeviceTypeImportForm(NetBoxModelImportForm):
     manufacturer = forms.ModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         to_field_name='name',
         help_text=_('The manufacturer which produces this device type')
     )
     default_platform = forms.ModelChoiceField(
+        label=_('Default platform'),
         queryset=Platform.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('The default platform for devices of this type (optional)')
     )
     weight = forms.DecimalField(
+        label=_('Weight'),
         required=False,
         help_text=_('Device weight'),
     )
     weight_unit = CSVChoiceField(
+        label=_('Weight unit'),
         choices=WeightUnitChoices,
         required=False,
         help_text=_('Unit for device weight')
@@ -312,14 +340,17 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
 
 class ModuleTypeImportForm(NetBoxModelImportForm):
     manufacturer = forms.ModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         to_field_name='name'
     )
     weight = forms.DecimalField(
+        label=_('Weight'),
         required=False,
         help_text=_('Module weight'),
     )
     weight_unit = CSVChoiceField(
+        label=_('Weight unit'),
         choices=WeightUnitChoices,
         required=False,
         help_text=_('Unit for module weight')
@@ -332,6 +363,7 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
 
 class DeviceRoleImportForm(NetBoxModelImportForm):
     config_template = CSVModelChoiceField(
+        label=_('Config template'),
         queryset=ConfigTemplate.objects.all(),
         to_field_name='name',
         required=False,
@@ -350,12 +382,14 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
 class PlatformImportForm(NetBoxModelImportForm):
     slug = SlugField()
     manufacturer = CSVModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Limit platform assignments to this manufacturer')
     )
     config_template = CSVModelChoiceField(
+        label=_('Config template'),
         queryset=ConfigTemplate.objects.all(),
         to_field_name='name',
         required=False,
@@ -371,43 +405,51 @@ class PlatformImportForm(NetBoxModelImportForm):
 
 class BaseDeviceImportForm(NetBoxModelImportForm):
     device_role = CSVModelChoiceField(
+        label=_('Device role'),
         queryset=DeviceRole.objects.all(),
         to_field_name='name',
         help_text=_('Assigned role')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned tenant')
     )
     manufacturer = CSVModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         to_field_name='name',
         help_text=_('Device type manufacturer')
     )
     device_type = CSVModelChoiceField(
+        label=_('Device type'),
         queryset=DeviceType.objects.all(),
         to_field_name='model',
         help_text=_('Device type model')
     )
     platform = CSVModelChoiceField(
+        label=_('Platform'),
         queryset=Platform.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned platform')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=DeviceStatusChoices,
         help_text=_('Operational status')
     )
     virtual_chassis = CSVModelChoiceField(
+        label=_('Virtual chassis'),
         queryset=VirtualChassis.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Virtual chassis')
     )
     cluster = CSVModelChoiceField(
+        label=_('Cluster'),
         queryset=Cluster.objects.all(),
         to_field_name='name',
         required=False,
@@ -430,45 +472,53 @@ class BaseDeviceImportForm(NetBoxModelImportForm):
 
 class DeviceImportForm(BaseDeviceImportForm):
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name',
         help_text=_('Assigned site')
     )
     location = CSVModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_("Assigned location (if any)")
     )
     rack = CSVModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_("Assigned rack (if any)")
     )
     face = CSVChoiceField(
+        label=_('Face'),
         choices=DeviceFaceChoices,
         required=False,
         help_text=_('Mounted rack face')
     )
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=Device.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Parent device (for child devices)')
     )
     device_bay = CSVModelChoiceField(
+        label=_('Device bay'),
         queryset=DeviceBay.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Device bay in which this device is installed (for child devices)')
     )
     airflow = CSVChoiceField(
+        label=_('Airflow'),
         choices=DeviceAirflowChoices,
         required=False,
         help_text=_('Airflow direction')
     )
     config_template = CSVModelChoiceField(
+        label=_('Config template'),
         queryset=ConfigTemplate.objects.all(),
         to_field_name='name',
         required=False,
@@ -523,29 +573,35 @@ class DeviceImportForm(BaseDeviceImportForm):
 
 class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name',
         help_text=_('The device in which this module is installed')
     )
     module_bay = CSVModelChoiceField(
+        label=_('Module bay'),
         queryset=ModuleBay.objects.all(),
         to_field_name='name',
         help_text=_('The module bay in which this module is installed')
     )
     module_type = CSVModelChoiceField(
+        label=_('Module type'),
         queryset=ModuleType.objects.all(),
         to_field_name='model',
         help_text=_('The type of module')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=ModuleStatusChoices,
         help_text=_('Operational status')
     )
     replicate_components = forms.BooleanField(
+        label=_('Replicate components'),
         required=False,
         help_text=_('Automatically populate components associated with this module type (enabled by default)')
     )
     adopt_components = forms.BooleanField(
+        label=_('Adopt components'),
         required=False,
         help_text=_('Adopt already existing components')
     )
@@ -579,15 +635,18 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
 
 class ConsolePortImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=ConsolePortTypeChoices,
         required=False,
         help_text=_('Port type')
     )
     speed = CSVTypedChoiceField(
+        label=_('Speed'),
         choices=ConsolePortSpeedChoices,
         coerce=int,
         empty_value=None,
@@ -602,15 +661,18 @@ class ConsolePortImportForm(NetBoxModelImportForm):
 
 class ConsoleServerPortImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=ConsolePortTypeChoices,
         required=False,
         help_text=_('Port type')
     )
     speed = CSVTypedChoiceField(
+        label=_('Speed'),
         choices=ConsolePortSpeedChoices,
         coerce=int,
         empty_value=None,
@@ -625,10 +687,12 @@ class ConsoleServerPortImportForm(NetBoxModelImportForm):
 
 class PowerPortImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=PowerPortTypeChoices,
         required=False,
         help_text=_('Port type')
@@ -643,21 +707,25 @@ class PowerPortImportForm(NetBoxModelImportForm):
 
 class PowerOutletImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=PowerOutletTypeChoices,
         required=False,
         help_text=_('Outlet type')
     )
     power_port = CSVModelChoiceField(
+        label=_('Power port'),
         queryset=PowerPort.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Local power port which feeds this outlet')
     )
     feed_leg = CSVChoiceField(
+        label=_('Feed lag'),
         choices=PowerOutletFeedLegChoices,
         required=False,
         help_text=_('Electrical phase (for three-phase circuits)')
@@ -692,63 +760,75 @@ class PowerOutletImportForm(NetBoxModelImportForm):
 
 class InterfaceImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=Interface.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Parent interface')
     )
     bridge = CSVModelChoiceField(
+        label=_('Bridge'),
         queryset=Interface.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Bridged interface')
     )
     lag = CSVModelChoiceField(
+        label=_('Lag'),
         queryset=Interface.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Parent LAG interface')
     )
     vdcs = CSVModelMultipleChoiceField(
+        label=_('Vdcs'),
         queryset=VirtualDeviceContext.objects.all(),
         required=False,
         to_field_name='name',
-        help_text='VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")'
+        help_text=_('VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")')
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=InterfaceTypeChoices,
         help_text=_('Physical medium')
     )
     duplex = CSVChoiceField(
+        label=_('Duplex'),
         choices=InterfaceDuplexChoices,
         required=False
     )
     poe_mode = CSVChoiceField(
+        label=_('Poe mode'),
         choices=InterfacePoEModeChoices,
         required=False,
         help_text=_('PoE mode')
     )
     poe_type = CSVChoiceField(
+        label=_('Poe type'),
         choices=InterfacePoETypeChoices,
         required=False,
         help_text=_('PoE type')
     )
     mode = CSVChoiceField(
+        label=_('Mode'),
         choices=InterfaceModeChoices,
         required=False,
         help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
     )
     vrf = CSVModelChoiceField(
+        label=_('VRF'),
         queryset=VRF.objects.all(),
         required=False,
         to_field_name='rd',
         help_text=_('Assigned VRF')
     )
     rf_role = CSVChoiceField(
+        label=_('Rf role'),
         choices=WirelessRoleChoices,
         required=False,
         help_text=_('Wireless role (AP/station)')
@@ -792,15 +872,18 @@ class InterfaceImportForm(NetBoxModelImportForm):
 
 class FrontPortImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     rear_port = CSVModelChoiceField(
+        label=_('Rear port'),
         queryset=RearPort.objects.all(),
         to_field_name='name',
         help_text=_('Corresponding rear port')
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=PortTypeChoices,
         help_text=_('Physical medium classification')
     )
@@ -837,10 +920,12 @@ class FrontPortImportForm(NetBoxModelImportForm):
 
 class RearPortImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     type = CSVChoiceField(
+        label=_('Type'),
         help_text=_('Physical medium classification'),
         choices=PortTypeChoices,
     )
@@ -852,6 +937,7 @@ class RearPortImportForm(NetBoxModelImportForm):
 
 class ModuleBayImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
@@ -863,10 +949,12 @@ class ModuleBayImportForm(NetBoxModelImportForm):
 
 class DeviceBayImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     installed_device = CSVModelChoiceField(
+        label=_('Installed device'),
         queryset=Device.objects.all(),
         required=False,
         to_field_name='name',
@@ -909,32 +997,38 @@ class DeviceBayImportForm(NetBoxModelImportForm):
 
 class InventoryItemImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name'
     )
     role = CSVModelChoiceField(
+        label=_('Role'),
         queryset=InventoryItemRole.objects.all(),
         to_field_name='name',
         required=False
     )
     manufacturer = CSVModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         to_field_name='name',
         required=False
     )
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=Device.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Parent inventory item')
     )
     component_type = CSVContentTypeField(
+        label=_('Component type'),
         queryset=ContentType.objects.all(),
         limit_choices_to=MODULAR_COMPONENT_MODELS,
         required=False,
         help_text=_('Component Type')
     )
     component_name = forms.CharField(
+        label=_('Compnent name'),
         required=False,
         help_text=_('Component Name')
     )
@@ -1002,52 +1096,62 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
 class CableImportForm(NetBoxModelImportForm):
     # Termination A
     side_a_device = CSVModelChoiceField(
+        label=_('Side a device'),
         queryset=Device.objects.all(),
         to_field_name='name',
         help_text=_('Side A device')
     )
     side_a_type = CSVContentTypeField(
+        label=_('Side a type'),
         queryset=ContentType.objects.all(),
         limit_choices_to=CABLE_TERMINATION_MODELS,
         help_text=_('Side A type')
     )
     side_a_name = forms.CharField(
+        label=_('Side a name'),
         help_text=_('Side A component name')
     )
 
     # Termination B
     side_b_device = CSVModelChoiceField(
+        label=_('Side b device'),
         queryset=Device.objects.all(),
         to_field_name='name',
         help_text=_('Side B device')
     )
     side_b_type = CSVContentTypeField(
+        label=_('Side b type'),
         queryset=ContentType.objects.all(),
         limit_choices_to=CABLE_TERMINATION_MODELS,
         help_text=_('Side B type')
     )
     side_b_name = forms.CharField(
+        label=_('Side b name'),
         help_text=_('Side B component name')
     )
 
     # Cable attributes
     status = CSVChoiceField(
+        label=_('Status'),
         choices=LinkStatusChoices,
         required=False,
         help_text=_('Connection status')
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=CableTypeChoices,
         required=False,
         help_text=_('Physical medium classification')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned tenant')
     )
     length_unit = CSVChoiceField(
+        label=_('Length unit'),
         choices=CableLengthUnitChoices,
         required=False,
         help_text=_('Length unit')
@@ -1110,6 +1214,7 @@ class CableImportForm(NetBoxModelImportForm):
 
 class VirtualChassisImportForm(NetBoxModelImportForm):
     master = CSVModelChoiceField(
+        label=_('Master'),
         queryset=Device.objects.all(),
         to_field_name='name',
         required=False,
@@ -1127,11 +1232,13 @@ class VirtualChassisImportForm(NetBoxModelImportForm):
 
 class PowerPanelImportForm(NetBoxModelImportForm):
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name',
         help_text=_('Name of parent site')
     )
     location = CSVModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         required=False,
         to_field_name='name'
@@ -1153,22 +1260,26 @@ class PowerPanelImportForm(NetBoxModelImportForm):
 
 class PowerFeedImportForm(NetBoxModelImportForm):
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name',
         help_text=_('Assigned site')
     )
     power_panel = CSVModelChoiceField(
+        label=_('Power panel'),
         queryset=PowerPanel.objects.all(),
         to_field_name='name',
         help_text=_('Upstream power panel')
     )
     location = CSVModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_("Rack's location (if any)")
     )
     rack = CSVModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         to_field_name='name',
         required=False,
@@ -1181,18 +1292,22 @@ class PowerFeedImportForm(NetBoxModelImportForm):
         help_text=_('Assigned tenant')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=PowerFeedStatusChoices,
         help_text=_('Operational status')
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=PowerFeedTypeChoices,
         help_text=_('Primary or redundant')
     )
     supply = CSVChoiceField(
+        label=_('Supply'),
         choices=PowerFeedSupplyChoices,
         help_text=_('Supply type (AC/DC)')
     )
     phase = CSVChoiceField(
+        label=_('Phase'),
         choices=PowerFeedPhaseChoices,
         help_text=_('Single or three-phase')
     )
@@ -1228,11 +1343,13 @@ class PowerFeedImportForm(NetBoxModelImportForm):
 class VirtualDeviceContextImportForm(NetBoxModelImportForm):
 
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name',
         help_text='Assigned role'
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',

+ 15 - 8
netbox/dcim/forms/common.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import *
 from dcim.constants import *
@@ -47,7 +47,7 @@ class InterfaceCommonForm(forms.Form):
         # Untagged interfaces cannot be assigned tagged VLANs
         if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
             raise forms.ValidationError({
-                'mode': "An access interface cannot have tagged VLANs assigned."
+                'mode': _("An access interface cannot have tagged VLANs assigned.")
             })
 
         # Remove all tagged VLAN assignments from "tagged all" interfaces
@@ -61,8 +61,10 @@ class InterfaceCommonForm(forms.Form):
 
             if invalid_vlans:
                 raise forms.ValidationError({
-                    'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as "
-                                    f"the interface's parent device/VM, or they must be global"
+                    'tagged_vlans': _(
+                        "The tagged VLANs ({vlans}) must belong to the same site as the interface's parent device/VM, "
+                        "or they must be global"
+                    ).format(vlans=', '.join(invalid_vlans))
                 })
 
 
@@ -105,7 +107,7 @@ class ModuleCommonForm(forms.Form):
                 # Installing modules with placeholders require that the bay has a position value
                 if MODULE_TOKEN in template.name and not module_bay.position:
                     raise forms.ValidationError(
-                        "Cannot install module with placeholder values in a module bay with no position defined"
+                        _("Cannot install module with placeholder values in a module bay with no position defined.")
                     )
 
                 resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
@@ -114,12 +116,17 @@ class ModuleCommonForm(forms.Form):
                 # It is not possible to adopt components already belonging to a module
                 if adopt_components and existing_item and existing_item.module:
                     raise forms.ValidationError(
-                        f"Cannot adopt {template.component_model.__name__} '{resolved_name}' as it already belongs "
-                        f"to a module"
+                        _("Cannot adopt {name} '{resolved_name}' as it already belongs to a module").format(
+                            name=template.component_model.__name__,
+                            resolved_name=resolved_name
+                        )
                     )
 
                 # If we are not adopting components we error if the component exists
                 if not adopt_components and resolved_name in installed_components:
                     raise forms.ValidationError(
-                        f"{template.component_model.__name__} - {resolved_name} already exists"
+                        _("{name} - {resolved_name} already exists").format(
+                            name=template.component_model.__name__,
+                            resolved_name=resolved_name
+                        )
                     )

+ 1 - 1
netbox/dcim/forms/connections.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from circuits.models import Circuit, CircuitTermination
 from dcim.models import *

+ 194 - 129
netbox/dcim/forms/filtersets.py

@@ -1,6 +1,6 @@
 from django import forms
 from django.contrib.auth import get_user_model
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import *
 from dcim.constants import *
@@ -56,9 +56,11 @@ __all__ = (
 
 class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
     name = forms.CharField(
+        label=_('Name'),
         required=False
     )
     label = forms.CharField(
+        label=_('Label'),
         required=False
     )
     region_id = DynamicModelMultipleChoiceField(
@@ -130,7 +132,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Region
     fieldsets = (
         (None, ('q', 'filter_id', 'tag', 'parent_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group'))
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
     )
     parent_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -144,7 +146,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = SiteGroup
     fieldsets = (
         (None, ('q', 'filter_id', 'tag', 'parent_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group'))
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
     )
     parent_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
@@ -158,11 +160,12 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
     model = Site
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
+        (_('Attributes'), ('status', 'region_id', 'group_id', 'asn_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=SiteStatusChoices,
         required=False
     )
@@ -188,9 +191,9 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
     model = Location
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
+        (_('Attributes'), ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -221,6 +224,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
         label=_('Parent')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=LocationStatusChoices,
         required=False
     )
@@ -236,12 +240,12 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
     model = Rack
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
-        ('Function', ('status', 'role_id')),
-        ('Hardware', ('type', 'width', 'serial', 'asset_tag')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
-        ('Weight', ('weight', 'max_weight', 'weight_unit')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
+        (_('Function'), ('status', 'role_id')),
+        (_('Hardware'), ('type', 'width', 'serial', 'asset_tag')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        (_('Weight'), ('weight', 'max_weight', 'weight_unit')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -271,14 +275,17 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
         label=_('Location')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=RackStatusChoices,
         required=False
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=RackTypeChoices,
         required=False
     )
     width = forms.MultipleChoiceField(
+        label=_('Width'),
         choices=RackWidthChoices,
         required=False
     )
@@ -289,21 +296,26 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
         label=_('Role')
     )
     serial = forms.CharField(
+        label=_('Serial'),
         required=False
     )
     asset_tag = forms.CharField(
+        label=_('Asset tag'),
         required=False
     )
     tag = TagFilterField(model)
     weight = forms.DecimalField(
+        label=_('Weight'),
         required=False,
         min_value=1
     )
     max_weight = forms.IntegerField(
+        label=_('Max weight'),
         required=False,
         min_value=1
     )
     weight_unit = forms.ChoiceField(
+        label=_('Weight unit'),
         choices=add_blank_choice(WeightUnitChoices),
         required=False
     )
@@ -312,12 +324,12 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
 class RackElevationFilterForm(RackFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')),
-        ('Function', ('status', 'role_id')),
-        ('Hardware', ('type', 'width', 'serial', 'asset_tag')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
-        ('Weight', ('weight', 'max_weight', 'weight_unit')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')),
+        (_('Function'), ('status', 'role_id')),
+        (_('Hardware'), ('type', 'width', 'serial', 'asset_tag')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        (_('Weight'), ('weight', 'max_weight', 'weight_unit')),
     )
     id = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
@@ -334,9 +346,9 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = RackReservation
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('User', ('user_id',)),
-        ('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('User'), ('user_id',)),
+        (_('Rack'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -390,7 +402,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = Manufacturer
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group'))
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
     )
     tag = TagFilterField(model)
 
@@ -399,13 +411,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
     model = DeviceType
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Hardware', ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')),
-        ('Images', ('has_front_image', 'has_rear_image')),
-        ('Components', (
+        (_('Hardware'), ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')),
+        (_('Images'), ('has_front_image', 'has_rear_image')),
+        (_('Components'), (
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
             'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
         )),
-        ('Weight', ('weight', 'weight_unit')),
+        (_('Weight'), ('weight', 'weight_unit')),
     )
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
@@ -418,98 +430,103 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
         label=_('Default platform')
     )
     part_number = forms.CharField(
+        label=_('Part number'),
         required=False
     )
     subdevice_role = forms.MultipleChoiceField(
+        label=_('Subdevice role'),
         choices=add_blank_choice(SubdeviceRoleChoices),
         required=False
     )
     airflow = forms.MultipleChoiceField(
+        label=_('Airflow'),
         choices=add_blank_choice(DeviceAirflowChoices),
         required=False
     )
     has_front_image = forms.NullBooleanField(
         required=False,
-        label='Has a front image',
+        label=_('Has a front image'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     has_rear_image = forms.NullBooleanField(
         required=False,
-        label='Has a rear image',
+        label=_('Has a rear image'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     console_ports = forms.NullBooleanField(
         required=False,
-        label='Has console ports',
+        label=_('Has console ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     console_server_ports = forms.NullBooleanField(
         required=False,
-        label='Has console server ports',
+        label=_('Has console server ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_ports = forms.NullBooleanField(
         required=False,
-        label='Has power ports',
+        label=_('Has power ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_outlets = forms.NullBooleanField(
         required=False,
-        label='Has power outlets',
+        label=_('Has power outlets'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     interfaces = forms.NullBooleanField(
         required=False,
-        label='Has interfaces',
+        label=_('Has interfaces'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     pass_through_ports = forms.NullBooleanField(
         required=False,
-        label='Has pass-through ports',
+        label=_('Has pass-through ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     device_bays = forms.NullBooleanField(
         required=False,
-        label='Has device bays',
+        label=_('Has device bays'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     module_bays = forms.NullBooleanField(
         required=False,
-        label='Has module bays',
+        label=_('Has module bays'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     inventory_items = forms.NullBooleanField(
         required=False,
-        label='Has inventory items',
+        label=_('Has inventory items'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     tag = TagFilterField(model)
     weight = forms.DecimalField(
+        label=_('Weight'),
         required=False
     )
     weight_unit = forms.ChoiceField(
+        label=_('Weight unit'),
         choices=add_blank_choice(WeightUnitChoices),
         required=False
     )
@@ -519,12 +536,12 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
     model = ModuleType
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Hardware', ('manufacturer_id', 'part_number')),
-        ('Components', (
+        (_('Hardware'), ('manufacturer_id', 'part_number')),
+        (_('Components'), (
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
             'pass_through_ports',
         )),
-        ('Weight', ('weight', 'weight_unit')),
+        (_('Weight'), ('weight', 'weight_unit')),
     )
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
@@ -533,55 +550,58 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
         fetch_trigger='open'
     )
     part_number = forms.CharField(
+        label=_('Part number'),
         required=False
     )
     console_ports = forms.NullBooleanField(
         required=False,
-        label='Has console ports',
+        label=_('Has console ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     console_server_ports = forms.NullBooleanField(
         required=False,
-        label='Has console server ports',
+        label=_('Has console server ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_ports = forms.NullBooleanField(
         required=False,
-        label='Has power ports',
+        label=_('Has power ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_outlets = forms.NullBooleanField(
         required=False,
-        label='Has power outlets',
+        label=_('Has power outlets'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     interfaces = forms.NullBooleanField(
         required=False,
-        label='Has interfaces',
+        label=_('Has interfaces'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     pass_through_ports = forms.NullBooleanField(
         required=False,
-        label='Has pass-through ports',
+        label=_('Has pass-through ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     tag = TagFilterField(model)
     weight = forms.DecimalField(
+        label=_('Weight'),
         required=False
     )
     weight_unit = forms.ChoiceField(
+        label=_('Weight unit'),
         choices=add_blank_choice(WeightUnitChoices),
         required=False
     )
@@ -621,15 +641,17 @@ class DeviceFilterForm(
     model = Device
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
-        ('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
-        ('Components', (
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
+        (_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
+        (_('Components'), (
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
         )),
-        ('Miscellaneous', ('has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data'))
+        (_('Miscellaneous'), (
+            'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
+        ))
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -694,22 +716,26 @@ class DeviceFilterForm(
         label=_('Platform')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=DeviceStatusChoices,
         required=False
     )
     airflow = forms.MultipleChoiceField(
+        label=_('Airflow'),
         choices=add_blank_choice(DeviceAirflowChoices),
         required=False
     )
     serial = forms.CharField(
+        label=_('Serial'),
         required=False
     )
     asset_tag = forms.CharField(
+        label=_('Asset tag'),
         required=False
     )
     mac_address = forms.CharField(
         required=False,
-        label='MAC address'
+        label=_('MAC address')
     )
     config_template_id = DynamicModelMultipleChoiceField(
         queryset=ConfigTemplate.objects.all(),
@@ -718,7 +744,7 @@ class DeviceFilterForm(
     )
     has_primary_ip = forms.NullBooleanField(
         required=False,
-        label='Has a primary IP',
+        label=_('Has a primary IP'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
@@ -732,49 +758,49 @@ class DeviceFilterForm(
     )
     virtual_chassis_member = forms.NullBooleanField(
         required=False,
-        label='Virtual chassis member',
+        label=_('Virtual chassis member'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     console_ports = forms.NullBooleanField(
         required=False,
-        label='Has console ports',
+        label=_('Has console ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     console_server_ports = forms.NullBooleanField(
         required=False,
-        label='Has console server ports',
+        label=_('Has console server ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_ports = forms.NullBooleanField(
         required=False,
-        label='Has power ports',
+        label=_('Has power ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     power_outlets = forms.NullBooleanField(
         required=False,
-        label='Has power outlets',
+        label=_('Has power outlets'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     interfaces = forms.NullBooleanField(
         required=False,
-        label='Has interfaces',
+        label=_('Has interfaces'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     pass_through_ports = forms.NullBooleanField(
         required=False,
-        label='Has pass-through ports',
+        label=_('Has pass-through ports'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
@@ -789,8 +815,8 @@ class VirtualDeviceContextFilterForm(
     model = VirtualDeviceContext
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('device', 'status', 'has_primary_ip')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Attributes'), ('device', 'status', 'has_primary_ip')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     device = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
@@ -799,12 +825,13 @@ class VirtualDeviceContextFilterForm(
         fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         required=False,
         choices=add_blank_choice(VirtualDeviceContextStatusChoices)
     )
     has_primary_ip = forms.NullBooleanField(
         required=False,
-        label='Has a primary IP',
+        label=_('Has a primary IP'),
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
@@ -816,7 +843,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
     model = Module
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Hardware', ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')),
+        (_('Hardware'), ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')),
     )
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
@@ -834,13 +861,16 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
         fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=ModuleStatusChoices,
         required=False
     )
     serial = forms.CharField(
+        label=_('Serial'),
         required=False
     )
     asset_tag = forms.CharField(
+        label=_('Asset tag'),
         required=False
     )
     tag = TagFilterField(model)
@@ -850,8 +880,8 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = VirtualChassis
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region_id', 'site_group_id', 'site_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -879,9 +909,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = Cable
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('site_id', 'location_id', 'rack_id', 'device_id')),
-        ('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Location'), ('site_id', 'location_id', 'rack_id', 'device_id')),
+        (_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -927,20 +957,25 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('Device')
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=add_blank_choice(CableTypeChoices),
         required=False
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         required=False,
         choices=add_blank_choice(LinkStatusChoices)
     )
     color = ColorField(
+        label=_('Color'),
         required=False
     )
     length = forms.IntegerField(
+        label=_('Length'),
         required=False
     )
     length_unit = forms.ChoiceField(
+        label=_('Length unit'),
         choices=add_blank_choice(CableLengthUnitChoices),
         required=False
     )
@@ -951,8 +986,8 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     model = PowerPanel
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -989,9 +1024,9 @@ class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = PowerFeed
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Attributes'), ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -1030,28 +1065,35 @@ class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('Rack')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=PowerFeedStatusChoices,
         required=False
     )
     type = forms.ChoiceField(
+        label=_('Type'),
         choices=add_blank_choice(PowerFeedTypeChoices),
         required=False
     )
     supply = forms.ChoiceField(
+        label=_('Supply'),
         choices=add_blank_choice(PowerFeedSupplyChoices),
         required=False
     )
     phase = forms.ChoiceField(
+        label=_('Phase'),
         choices=add_blank_choice(PowerFeedPhaseChoices),
         required=False
     )
     voltage = forms.IntegerField(
+        label=_('Voltage'),
         required=False
     )
     amperage = forms.IntegerField(
+        label=_('Amperage'),
         required=False
     )
     max_utilization = forms.IntegerField(
+        label=_('Max utilization'),
         required=False
     )
     tag = TagFilterField(model)
@@ -1063,12 +1105,14 @@ class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 
 class CabledFilterForm(forms.Form):
     cabled = forms.NullBooleanField(
+        label=_('Cabled'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     occupied = forms.NullBooleanField(
+        label=_('Occupied'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -1078,6 +1122,7 @@ class CabledFilterForm(forms.Form):
 
 class PathEndpointFilterForm(CabledFilterForm):
     connected = forms.NullBooleanField(
+        label=_('Connected'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -1089,16 +1134,18 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = ConsolePort
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'type', 'speed')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        ('Connection', ('cabled', 'connected', 'occupied')),
+        (_('Attributes'), ('name', 'label', 'type', 'speed')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Connection'), ('cabled', 'connected', 'occupied')),
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=ConsolePortTypeChoices,
         required=False
     )
     speed = forms.MultipleChoiceField(
+        label=_('Speed'),
         choices=ConsolePortSpeedChoices,
         required=False
     )
@@ -1109,16 +1156,18 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
     model = ConsoleServerPort
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'type', 'speed')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        ('Connection', ('cabled', 'connected', 'occupied')),
+        (_('Attributes'), ('name', 'label', 'type', 'speed')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Connection'), ('cabled', 'connected', 'occupied')),
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=ConsolePortTypeChoices,
         required=False
     )
     speed = forms.MultipleChoiceField(
+        label=_('Speed'),
         choices=ConsolePortSpeedChoices,
         required=False
     )
@@ -1129,12 +1178,13 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = PowerPort
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'type')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        ('Connection', ('cabled', 'connected', 'occupied')),
+        (_('Attributes'), ('name', 'label', 'type')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Connection'), ('cabled', 'connected', 'occupied')),
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=PowerPortTypeChoices,
         required=False
     )
@@ -1145,12 +1195,13 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = PowerOutlet
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'type')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        ('Connection', ('cabled', 'connected', 'occupied')),
+        (_('Attributes'), ('name', 'label', 'type')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Connection'), ('cabled', 'connected', 'occupied')),
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=PowerOutletTypeChoices,
         required=False
     )
@@ -1161,13 +1212,13 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = Interface
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
-        ('Addressing', ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
-        ('PoE', ('poe_mode', 'poe_type')),
-        ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
-        ('Connection', ('cabled', 'connected', 'occupied')),
+        (_('Attributes'), ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
+        (_('Addressing'), ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
+        (_('PoE'), ('poe_mode', 'poe_type')),
+        (_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
+        (_('Connection'), ('cabled', 'connected', 'occupied')),
     )
     vdc_id = DynamicModelMultipleChoiceField(
         queryset=VirtualDeviceContext.objects.all(),
@@ -1178,30 +1229,36 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
         label=_('Virtual Device Context')
     )
     kind = forms.MultipleChoiceField(
+        label=_('Kind'),
         choices=InterfaceKindChoices,
         required=False
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=InterfaceTypeChoices,
         required=False
     )
     speed = forms.IntegerField(
+        label=_('Speed'),
         required=False,
         widget=NumberWithOptions(
             options=InterfaceSpeedChoices
         )
     )
     duplex = forms.MultipleChoiceField(
+        label=_('Duplex'),
         choices=InterfaceDuplexChoices,
         required=False
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     mgmt_only = forms.NullBooleanField(
+        label=_('Mgmt only'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -1209,50 +1266,50 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     )
     mac_address = forms.CharField(
         required=False,
-        label='MAC address'
+        label=_('MAC address')
     )
     wwn = forms.CharField(
         required=False,
-        label='WWN'
+        label=_('WWN')
     )
     poe_mode = forms.MultipleChoiceField(
         choices=InterfacePoEModeChoices,
         required=False,
-        label='PoE mode'
+        label=_('PoE mode')
     )
     poe_type = forms.MultipleChoiceField(
         choices=InterfacePoETypeChoices,
         required=False,
-        label='PoE type'
+        label=_('PoE type')
     )
     rf_role = forms.MultipleChoiceField(
         choices=WirelessRoleChoices,
         required=False,
-        label='Wireless role'
+        label=_('Wireless role')
     )
     rf_channel = forms.MultipleChoiceField(
         choices=WirelessChannelChoices,
         required=False,
-        label='Wireless channel'
+        label=_('Wireless channel')
     )
     rf_channel_frequency = forms.IntegerField(
         required=False,
-        label='Channel frequency (MHz)'
+        label=_('Channel frequency (MHz)')
     )
     rf_channel_width = forms.IntegerField(
         required=False,
-        label='Channel width (MHz)'
+        label=_('Channel width (MHz)')
     )
     tx_power = forms.IntegerField(
         required=False,
-        label='Transmit power (dBm)',
+        label=_('Transmit power (dBm)'),
         min_value=0,
         max_value=127
     )
     vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     l2vpn_id = DynamicModelMultipleChoiceField(
         queryset=L2VPN.objects.all(),
@@ -1265,17 +1322,19 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
 class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'type', 'color')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        ('Cable', ('cabled', 'occupied')),
+        (_('Attributes'), ('name', 'label', 'type', 'color')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Cable'), ('cabled', 'occupied')),
     )
     model = FrontPort
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=PortTypeChoices,
         required=False
     )
     color = ColorField(
+        label=_('Color'),
         required=False
     )
     tag = TagFilterField(model)
@@ -1285,16 +1344,18 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
     model = RearPort
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'type', 'color')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
-        ('Cable', ('cabled', 'occupied')),
+        (_('Attributes'), ('name', 'label', 'type', 'color')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Cable'), ('cabled', 'occupied')),
     )
     type = forms.MultipleChoiceField(
+        label=_('Type'),
         choices=PortTypeChoices,
         required=False
     )
     color = ColorField(
+        label=_('Color'),
         required=False
     )
     tag = TagFilterField(model)
@@ -1304,12 +1365,13 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
     model = ModuleBay
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'position')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Attributes'), ('name', 'label', 'position')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
     )
     tag = TagFilterField(model)
     position = forms.CharField(
+        label=_('Position'),
         required=False
     )
 
@@ -1318,9 +1380,9 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
     model = DeviceBay
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Attributes'), ('name', 'label')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
     )
     tag = TagFilterField(model)
 
@@ -1329,9 +1391,9 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
     model = InventoryItem
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
-        ('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
+        (_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
+        (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
     )
     role_id = DynamicModelMultipleChoiceField(
         queryset=InventoryItemRole.objects.all(),
@@ -1345,12 +1407,15 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
         label=_('Manufacturer')
     )
     serial = forms.CharField(
+        label=_('Serial'),
         required=False
     )
     asset_tag = forms.CharField(
+        label=_('Asset tag'),
         required=False
     )
     discovered = forms.NullBooleanField(
+        label=_('Discovered'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES

+ 4 - 1
netbox/dcim/forms/formsets.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext_lazy as _
 
 __all__ = (
     'BaseVCMemberFormSet',
@@ -16,6 +17,8 @@ class BaseVCMemberFormSet(forms.BaseModelFormSet):
             vc_position = form.cleaned_data.get('vc_position')
             if vc_position:
                 if vc_position in vc_position_list:
-                    error_msg = f"A virtual chassis member already exists in position {vc_position}."
+                    error_msg = _("A virtual chassis member already exists in position {vc_position}.").format(
+                        vc_position=vc_position
+                    )
                     form.add_error('vc_position', error_msg)
                 vc_position_list.append(vc_position)

+ 101 - 43
netbox/dcim/forms/model_forms.py

@@ -1,7 +1,7 @@
 from django import forms
 from django.contrib.auth import get_user_model
 from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 from timezone_field import TimeZoneFormField
 
 from dcim.choices import *
@@ -70,13 +70,14 @@ __all__ = (
 
 class RegionForm(NetBoxModelForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=Region.objects.all(),
         required=False
     )
     slug = SlugField()
 
     fieldsets = (
-        ('Region', (
+        (_('Region'), (
             'parent', 'name', 'slug', 'description', 'tags',
         )),
     )
@@ -90,13 +91,14 @@ class RegionForm(NetBoxModelForm):
 
 class SiteGroupForm(NetBoxModelForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=SiteGroup.objects.all(),
         required=False
     )
     slug = SlugField()
 
     fieldsets = (
-        ('Site Group', (
+        (_('Site Group'), (
             'parent', 'name', 'slug', 'description', 'tags',
         )),
     )
@@ -110,10 +112,12 @@ class SiteGroupForm(NetBoxModelForm):
 
 class SiteForm(TenancyForm, NetBoxModelForm):
     region = DynamicModelChoiceField(
+        label=_('Region'),
         queryset=Region.objects.all(),
         required=False
     )
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=SiteGroup.objects.all(),
         required=False
     )
@@ -124,17 +128,18 @@ class SiteForm(TenancyForm, NetBoxModelForm):
     )
     slug = SlugField()
     time_zone = TimeZoneFormField(
+        label=_('Time zone'),
         choices=add_blank_choice(TimeZoneFormField().choices),
         required=False
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Site', (
+        (_('Site'), (
             'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
         )),
-        ('Tenancy', ('tenant_group', 'tenant')),
-        ('Contact Info', ('physical_address', 'shipping_address', 'latitude', 'longitude')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
+        (_('Contact Info'), ('physical_address', 'shipping_address', 'latitude', 'longitude')),
     )
 
     class Meta:
@@ -159,10 +164,12 @@ class SiteForm(TenancyForm, NetBoxModelForm):
 
 class LocationForm(TenancyForm, NetBoxModelForm):
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         selector=True
     )
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=Location.objects.all(),
         required=False,
         query_params={
@@ -172,8 +179,8 @@ class LocationForm(TenancyForm, NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Location', ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -187,7 +194,7 @@ class RackRoleForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Rack Role', (
+        (_('Rack Role'), (
             'name', 'slug', 'color', 'description', 'tags',
         )),
     )
@@ -201,10 +208,12 @@ class RackRoleForm(NetBoxModelForm):
 
 class RackForm(TenancyForm, NetBoxModelForm):
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         selector=True
     )
     location = DynamicModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         required=False,
         query_params={
@@ -212,6 +221,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
         }
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=RackRole.objects.all(),
         required=False
     )
@@ -228,14 +238,17 @@ class RackForm(TenancyForm, NetBoxModelForm):
 
 class RackReservationForm(TenancyForm, NetBoxModelForm):
     rack = DynamicModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         selector=True
     )
     units = NumericArrayField(
+        label=_('Units'),
         base_field=forms.IntegerField(),
         help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
     )
     user = forms.ModelChoiceField(
+        label=_('User'),
         queryset=get_user_model().objects.order_by(
             'username'
         )
@@ -243,8 +256,8 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Reservation', ('rack', 'units', 'user', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('Reservation'), ('rack', 'units', 'user', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -258,7 +271,7 @@ class ManufacturerForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Manufacturer', (
+        (_('Manufacturer'), (
             'name', 'slug', 'description', 'tags',
         )),
     )
@@ -272,23 +285,26 @@ class ManufacturerForm(NetBoxModelForm):
 
 class DeviceTypeForm(NetBoxModelForm):
     manufacturer = DynamicModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all()
     )
     default_platform = DynamicModelChoiceField(
+        label=_('Default platform'),
         queryset=Platform.objects.all(),
         required=False
     )
     slug = SlugField(
+        label=_('Slug'),
         slug_source='model'
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Device Type', ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
-        ('Chassis', (
+        (_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
+        (_('Chassis'), (
             'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
         )),
-        ('Images', ('front_image', 'rear_image')),
+        (_('Images'), ('front_image', 'rear_image')),
     )
 
     class Meta:
@@ -310,13 +326,14 @@ class DeviceTypeForm(NetBoxModelForm):
 
 class ModuleTypeForm(NetBoxModelForm):
     manufacturer = DynamicModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all()
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Module Type', ('manufacturer', 'model', 'part_number', 'description', 'tags')),
-        ('Weight', ('weight', 'weight_unit'))
+        (_('Module Type'), ('manufacturer', 'model', 'part_number', 'description', 'tags')),
+        (_('Weight'), ('weight', 'weight_unit'))
     )
 
     class Meta:
@@ -328,13 +345,14 @@ class ModuleTypeForm(NetBoxModelForm):
 
 class DeviceRoleForm(NetBoxModelForm):
     config_template = DynamicModelChoiceField(
+        label=_('Config template'),
         queryset=ConfigTemplate.objects.all(),
         required=False
     )
     slug = SlugField()
 
     fieldsets = (
-        ('Device Role', (
+        (_('Device Role'), (
             'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
         )),
     )
@@ -348,19 +366,22 @@ class DeviceRoleForm(NetBoxModelForm):
 
 class PlatformForm(NetBoxModelForm):
     manufacturer = DynamicModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         required=False
     )
     config_template = DynamicModelChoiceField(
+        label=_('Config template'),
         queryset=ConfigTemplate.objects.all(),
         required=False
     )
     slug = SlugField(
+        label=_('Slug'),
         max_length=64
     )
 
     fieldsets = (
-        ('Platform', ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
+        (_('Platform'), ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
     )
 
     class Meta:
@@ -372,10 +393,12 @@ class PlatformForm(NetBoxModelForm):
 
 class DeviceForm(TenancyForm, NetBoxModelForm):
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         selector=True
     )
     location = DynamicModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         required=False,
         query_params={
@@ -386,6 +409,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         }
     )
     rack = DynamicModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         required=False,
         query_params={
@@ -394,6 +418,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         }
     )
     position = forms.DecimalField(
+        label=_('Position'),
         required=False,
         help_text=_("The lowest-numbered unit occupied by the device"),
         widget=APISelect(
@@ -405,17 +430,21 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         )
     )
     device_type = DynamicModelChoiceField(
+        label=_('Device type'),
         queryset=DeviceType.objects.all(),
         selector=True
     )
     device_role = DynamicModelChoiceField(
+        label=_('Device role'),
         queryset=DeviceRole.objects.all()
     )
     platform = DynamicModelChoiceField(
+        label=_('Platform'),
         queryset=Platform.objects.all(),
         required=False
     )
     cluster = DynamicModelChoiceField(
+        label=_('Cluster'),
         queryset=Cluster.objects.all(),
         required=False,
         selector=True
@@ -426,6 +455,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         label=''
     )
     virtual_chassis = DynamicModelChoiceField(
+        label=_('Virtual chassis'),
         queryset=VirtualChassis.objects.all(),
         required=False,
         selector=True
@@ -441,6 +471,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         help_text=_("The priority of the device in the virtual chassis")
     )
     config_template = DynamicModelChoiceField(
+        label=_('Config template'),
         queryset=ConfigTemplate.objects.all(),
         required=False
     )
@@ -518,36 +549,41 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
 
 class ModuleForm(ModuleCommonForm, NetBoxModelForm):
     device = DynamicModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         initial_params={
             'modulebays': '$module_bay'
         }
     )
     module_bay = DynamicModelChoiceField(
+        label=_('Module bay'),
         queryset=ModuleBay.objects.all(),
         query_params={
             'device_id': '$device'
         }
     )
     module_type = DynamicModelChoiceField(
+        label=_('Module type'),
         queryset=ModuleType.objects.all(),
         selector=True
     )
     comments = CommentField()
     replicate_components = forms.BooleanField(
+        label=_('Replicate components'),
         required=False,
         initial=True,
         help_text=_("Automatically populate components associated with this module type")
     )
     adopt_components = forms.BooleanField(
+        label=_('Adopt components'),
         required=False,
         initial=False,
         help_text=_("Adopt already existing components")
     )
 
     fieldsets = (
-        ('Module', ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
-        ('Hardware', (
+        (_('Module'), ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
+        (_('Hardware'), (
             'serial', 'asset_tag', 'replicate_components', 'adopt_components',
         )),
     )
@@ -581,17 +617,19 @@ class CableForm(TenancyForm, NetBoxModelForm):
         ]
         error_messages = {
             'length': {
-                'max_value': 'Maximum length is 32767 (any unit)'
+                'max_value': _('Maximum length is 32767 (any unit)')
             }
         }
 
 
 class PowerPanelForm(NetBoxModelForm):
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         selector=True
     )
     location = DynamicModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         required=False,
         query_params={
@@ -613,10 +651,12 @@ class PowerPanelForm(NetBoxModelForm):
 
 class PowerFeedForm(TenancyForm, NetBoxModelForm):
     power_panel = DynamicModelChoiceField(
+        label=_('Power panel'),
         queryset=PowerPanel.objects.all(),
         selector=True
     )
     rack = DynamicModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         required=False,
         selector=True
@@ -624,9 +664,9 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
-        ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
+        (_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -643,6 +683,7 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm):
 
 class VirtualChassisForm(NetBoxModelForm):
     master = forms.ModelChoiceField(
+        label=_('Master'),
         queryset=Device.objects.all(),
         required=False,
     )
@@ -706,6 +747,7 @@ class DeviceVCMembershipForm(forms.ModelForm):
 
 class VCMemberSelectForm(BootstrapMixin, forms.Form):
     device = DynamicModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         query_params={
             'virtual_chassis_id': 'null',
@@ -728,6 +770,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
 
 class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
     device_type = DynamicModelChoiceField(
+        label=_('Device type'),
         queryset=DeviceType.objects.all()
     )
 
@@ -741,10 +784,12 @@ class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
 
 class ModularComponentTemplateForm(ComponentTemplateForm):
     device_type = DynamicModelChoiceField(
+        label=_('Device type'),
         queryset=DeviceType.objects.all().all(),
         required=False
     )
     module_type = DynamicModelChoiceField(
+        label=_('Module type'),
         queryset=ModuleType.objects.all(),
         required=False
     )
@@ -797,6 +842,7 @@ class PowerPortTemplateForm(ModularComponentTemplateForm):
 
 class PowerOutletTemplateForm(ModularComponentTemplateForm):
     power_port = DynamicModelChoiceField(
+        label=_('Power port'),
         queryset=PowerPortTemplate.objects.all(),
         required=False,
         query_params={
@@ -817,6 +863,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
 
 class InterfaceTemplateForm(ModularComponentTemplateForm):
     bridge = DynamicModelChoiceField(
+        label=_('Bridge'),
         queryset=InterfaceTemplate.objects.all(),
         required=False,
         query_params={
@@ -827,8 +874,8 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
 
     fieldsets = (
         (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
-        ('PoE', ('poe_mode', 'poe_type')),
-        ('Wireless', ('rf_role',))
+        (_('PoE'), ('poe_mode', 'poe_type')),
+        (_('Wireless'), ('rf_role',)),
     )
 
     class Meta:
@@ -840,6 +887,7 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
 
 class FrontPortTemplateForm(ModularComponentTemplateForm):
     rear_port = DynamicModelChoiceField(
+        label=_('Rear port'),
         queryset=RearPortTemplate.objects.all(),
         required=False,
         query_params={
@@ -901,6 +949,7 @@ class DeviceBayTemplateForm(ComponentTemplateForm):
 
 class InventoryItemTemplateForm(ComponentTemplateForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=InventoryItemTemplate.objects.all(),
         required=False,
         query_params={
@@ -908,10 +957,12 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
         }
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=InventoryItemRole.objects.all(),
         required=False
     )
     manufacturer = DynamicModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         required=False
     )
@@ -947,6 +998,7 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
 
 class DeviceComponentForm(NetBoxModelForm):
     device = DynamicModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         selector=True
     )
@@ -961,6 +1013,7 @@ class DeviceComponentForm(NetBoxModelForm):
 
 class ModularDeviceComponentForm(DeviceComponentForm):
     module = DynamicModelChoiceField(
+        label=_('Module'),
         queryset=Module.objects.all(),
         required=False,
         query_params={
@@ -1017,6 +1070,7 @@ class PowerPortForm(ModularDeviceComponentForm):
 
 class PowerOutletForm(ModularDeviceComponentForm):
     power_port = DynamicModelChoiceField(
+        label=_('Power port'),
         queryset=PowerPort.objects.all(),
         required=False,
         query_params={
@@ -1043,7 +1097,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     vdcs = DynamicModelMultipleChoiceField(
         queryset=VirtualDeviceContext.objects.all(),
         required=False,
-        label='Virtual Device Contexts',
+        label=_('Virtual device contexts'),
         query_params={
             'device_id': '$device',
         }
@@ -1121,13 +1175,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
     )
 
     fieldsets = (
-        ('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
-        ('Addressing', ('vrf', 'mac_address', 'wwn')),
-        ('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
-        ('Related Interfaces', ('parent', 'bridge', 'lag')),
-        ('PoE', ('poe_mode', 'poe_type')),
-        ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
-        ('Wireless', (
+        (_('Interface'), ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
+        (_('Addressing'), ('vrf', 'mac_address', 'wwn')),
+        (_('Operation'), ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
+        (_('Related Interfaces'), ('parent', 'bridge', 'lag')),
+        (_('PoE'), ('poe_mode', 'poe_type')),
+        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
+        (_('Wireless'), (
             'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
         )),
     )
@@ -1233,6 +1287,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
 
 class InventoryItemForm(DeviceComponentForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=InventoryItem.objects.all(),
         required=False,
         query_params={
@@ -1240,10 +1295,12 @@ class InventoryItemForm(DeviceComponentForm):
         }
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=InventoryItemRole.objects.all(),
         required=False
     )
     manufacturer = DynamicModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         required=False
     )
@@ -1307,8 +1364,8 @@ class InventoryItemForm(DeviceComponentForm):
     )
 
     fieldsets = (
-        ('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
-        ('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
+        (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
+        (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')),
     )
 
     class Meta:
@@ -1359,7 +1416,7 @@ class InventoryItemForm(DeviceComponentForm):
             ) if self.cleaned_data[field]
         ]
         if len(selected_objects) > 1:
-            raise forms.ValidationError("An InventoryItem can only be assigned to a single component.")
+            raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
         elif selected_objects:
             self.instance.component = self.cleaned_data[selected_objects[0]]
         else:
@@ -1373,7 +1430,7 @@ class InventoryItemRoleForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Inventory Item Role', (
+        (_('Inventory Item Role'), (
             'name', 'slug', 'color', 'description', 'tags',
         )),
     )
@@ -1387,12 +1444,13 @@ class InventoryItemRoleForm(NetBoxModelForm):
 
 class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
     device = DynamicModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         selector=True
     )
     primary_ip4 = DynamicModelChoiceField(
         queryset=IPAddress.objects.all(),
-        label='Primary IPv4',
+        label=_('Primary IPv4'),
         required=False,
         query_params={
             'device_id': '$device',
@@ -1401,7 +1459,7 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
     )
     primary_ip6 = DynamicModelChoiceField(
         queryset=IPAddress.objects.all(),
-        label='Primary IPv6',
+        label=_('Primary IPv6'),
         required=False,
         query_params={
             'device_id': '$device',
@@ -1410,8 +1468,8 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
     )
 
     fieldsets = (
-        ('Virtual Device Context', ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant'))
+        (_('Virtual Device Context'), ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant'))
     )
 
     class Meta:

+ 19 - 7
netbox/dcim/forms/object_create.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.models import *
 from netbox.forms import NetBoxModelForm
@@ -38,8 +38,11 @@ class ComponentCreateForm(forms.Form):
     Subclass this form when facilitating the creation of one or more component or component template objects based on
     a name pattern.
     """
-    name = ExpandableNameField()
+    name = ExpandableNameField(
+        label=_('Name'),
+    )
     label = ExpandableNameField(
+        label=_('Label'),
         required=False,
         help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
     )
@@ -57,8 +60,9 @@ class ComponentCreateForm(forms.Form):
             value_count = len(self.cleaned_data[field_name])
             if self.cleaned_data[field_name] and value_count != pattern_count:
                 raise forms.ValidationError({
-                    field_name: f'The provided pattern specifies {value_count} values, but {pattern_count} are '
-                                f'expected.'
+                    field_name: _(
+                        "The provided pattern specifies {value_count} values, but {pattern_count} are expected."
+                    ).format(value_count=value_count, pattern_count=pattern_count)
                 }, code='label_pattern_mismatch')
 
 
@@ -222,12 +226,14 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
         super().__init__(*args, **kwargs)
 
         if 'module' in self.fields:
-            self.fields['name'].help_text += ' The string <code>{module}</code> will be replaced with the position ' \
-                                             'of the assigned module, if any'
+            self.fields['name'].help_text += _(
+                "The string <code>{module}</code> will be replaced with the position of the assigned module, if any."
+            )
 
 
 class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
     device = DynamicModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         selector=True,
         widget=APISelect(
@@ -329,6 +335,7 @@ class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm
 
 class VirtualChassisCreateForm(NetBoxModelForm):
     region = DynamicModelChoiceField(
+        label=_('Region'),
         queryset=Region.objects.all(),
         required=False,
         initial_params={
@@ -336,6 +343,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
         }
     )
     site_group = DynamicModelChoiceField(
+        label=_('Site group'),
         queryset=SiteGroup.objects.all(),
         required=False,
         initial_params={
@@ -343,6 +351,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
         }
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         query_params={
@@ -351,6 +360,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
         }
     )
     rack = DynamicModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         required=False,
         null_option='None',
@@ -359,6 +369,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
         }
     )
     members = DynamicModelMultipleChoiceField(
+        label=_('Members'),
         queryset=Device.objects.all(),
         required=False,
         query_params={
@@ -367,6 +378,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
         }
     )
     initial_position = forms.IntegerField(
+        label=_('Initial position'),
         initial=1,
         required=False,
         help_text=_('Position of the first member device. Increases by one for each additional member.')
@@ -383,7 +395,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
 
         if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
             raise forms.ValidationError({
-                'initial_position': "A position must be specified for the first VC member."
+                'initial_position': _("A position must be specified for the first VC member.")
             })
 
     def save(self, *args, **kwargs):

+ 9 - 1
netbox/dcim/forms/object_import.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
 from dcim.models import *
@@ -57,6 +57,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
 
 class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
     power_port = forms.ModelChoiceField(
+        label=_('Power port'),
         queryset=PowerPortTemplate.objects.all(),
         to_field_name='name',
         required=False
@@ -85,6 +86,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
 
 class InterfaceTemplateImportForm(ComponentTemplateImportForm):
     type = forms.ChoiceField(
+        label=_('Type'),
         choices=InterfaceTypeChoices.CHOICES
     )
     poe_mode = forms.ChoiceField(
@@ -113,9 +115,11 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
 
 class FrontPortTemplateImportForm(ComponentTemplateImportForm):
     type = forms.ChoiceField(
+        label=_('Type'),
         choices=PortTypeChoices.CHOICES
     )
     rear_port = forms.ModelChoiceField(
+        label=_('Rear port'),
         queryset=RearPortTemplate.objects.all(),
         to_field_name='name'
     )
@@ -143,6 +147,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
 
 class RearPortTemplateImportForm(ComponentTemplateImportForm):
     type = forms.ChoiceField(
+        label=_('Type'),
         choices=PortTypeChoices.CHOICES
     )
 
@@ -173,15 +178,18 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
 
 class InventoryItemTemplateImportForm(ComponentTemplateImportForm):
     parent = forms.ModelChoiceField(
+        label=_('Parent'),
         queryset=InventoryItemTemplate.objects.all(),
         required=False
     )
     role = forms.ModelChoiceField(
+        label=_('Role'),
         queryset=InventoryItemRole.objects.all(),
         to_field_name='name',
         required=False
     )
     manufacturer = forms.ModelChoiceField(
+        label=_('Manufacturer'),
         queryset=Manufacturer.objects.all(),
         to_field_name='name',
         required=False

+ 33 - 1
netbox/extras/forms/bulk_edit.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from extras.choices import *
 from extras.models import *
@@ -27,16 +27,20 @@ class CustomFieldBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     group_name = forms.CharField(
+        label=_('Group name'),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         required=False
     )
     required = forms.NullBooleanField(
+        label=_('Required'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     weight = forms.IntegerField(
+        label=_('Weight'),
         required=False
     )
     choice_set = DynamicModelChoiceField(
@@ -50,6 +54,7 @@ class CustomFieldBulkEditForm(BulkEditForm):
         initial=''
     )
     is_cloneable = forms.NullBooleanField(
+        label=_('Is cloneable'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
@@ -83,17 +88,21 @@ class CustomLinkBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     new_window = forms.NullBooleanField(
+        label=_('New window'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     weight = forms.IntegerField(
+        label=_('Weight'),
         required=False
     )
     button_class = forms.ChoiceField(
+        label=_('Button class'),
         choices=add_blank_choice(CustomLinkButtonClassChoices),
         required=False
     )
@@ -105,18 +114,22 @@ class ExportTemplateBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
     mime_type = forms.CharField(
+        label=_('MIME type'),
         max_length=50,
         required=False
     )
     file_extension = forms.CharField(
+        label=_('File extension'),
         max_length=15,
         required=False
     )
     as_attachment = forms.NullBooleanField(
+        label=_('As attachment'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
@@ -130,17 +143,21 @@ class SavedFilterBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
     weight = forms.IntegerField(
+        label=_('Weight'),
         required=False
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     shared = forms.NullBooleanField(
+        label=_('Shared'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
@@ -154,26 +171,32 @@ class WebhookBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     type_create = forms.NullBooleanField(
+        label=_('On create'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     type_update = forms.NullBooleanField(
+        label=_('On update'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     type_delete = forms.NullBooleanField(
+        label=_('On delete'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     type_job_start = forms.NullBooleanField(
+        label=_('On job start'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     type_job_end = forms.NullBooleanField(
+        label=_('On job end'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
@@ -192,6 +215,7 @@ class WebhookBulkEditForm(BulkEditForm):
         label=_('SSL verification')
     )
     secret = forms.CharField(
+        label=_('Secret'),
         required=False
     )
     ca_file_path = forms.CharField(
@@ -208,9 +232,11 @@ class TagBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     color = ColorField(
+        label=_('Color'),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -224,14 +250,17 @@ class ConfigContextBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     weight = forms.IntegerField(
+        label=_('Weight'),
         required=False,
         min_value=0
     )
     is_active = forms.NullBooleanField(
+        label=_('Is active'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
     description = forms.CharField(
+        label=_('Description'),
         required=False,
         max_length=100
     )
@@ -245,6 +274,7 @@ class ConfigTemplateBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -258,10 +288,12 @@ class JournalEntryBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
     )
     kind = forms.ChoiceField(
+        label=_('Kind'),
         choices=add_blank_choice(JournalEntryKindChoices),
         required=False
     )
     comments = forms.CharField(
+        label=_('Comments'),
         required=False,
         widget=forms.Textarea()
     )

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

@@ -2,7 +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 django.utils.translation import gettext_lazy as _
 
 from extras.choices import *
 from extras.models import *
@@ -28,27 +28,32 @@ __all__ = (
 
 class CustomFieldImportForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
         help_text=_("One or more assigned object types")
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=CustomFieldTypeChoices,
         help_text=_('Field data type (e.g. text, integer, etc.)')
     )
     object_type = CSVContentTypeField(
+        label=_('Object type'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
         required=False,
         help_text=_("Object type (for object or multi-object fields)")
     )
     choice_set = CSVModelChoiceField(
+        label=_('Choice set'),
         queryset=CustomFieldChoiceSet.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Choice set (for selection fields)')
     )
     ui_visibility = CSVChoiceField(
+        label=_('UI visibility'),
         choices=CustomFieldVisibilityChoices,
         help_text=_('How the custom field is displayed in the user interface')
     )
@@ -83,6 +88,7 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
 
 class CustomLinkImportForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_links'),
         help_text=_("One or more assigned object types")
@@ -98,6 +104,7 @@ class CustomLinkImportForm(CSVModelForm):
 
 class ExportTemplateImportForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('export_templates'),
         help_text=_("One or more assigned object types")
@@ -121,6 +128,7 @@ class ConfigTemplateImportForm(CSVModelForm):
 
 class SavedFilterImportForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         help_text=_("One or more assigned object types")
     )
@@ -134,6 +142,7 @@ class SavedFilterImportForm(CSVModelForm):
 
 class WebhookImportForm(CSVModelForm):
     content_types = CSVMultipleContentTypeField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('webhooks'),
         help_text=_("One or more assigned object types")
@@ -165,6 +174,7 @@ class JournalEntryImportForm(NetBoxModelImportForm):
         label=_('Assigned object type'),
     )
     kind = CSVChoiceField(
+        label=_('Kind'),
         choices=JournalEntryKindChoices,
         help_text=_('The classification of entry')
     )

+ 38 - 18
netbox/extras/forms/filtersets.py

@@ -1,7 +1,7 @@
 from django import forms
 from django.contrib.auth import get_user_model
 from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from core.models import DataFile, DataSource
 from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@@ -39,7 +39,7 @@ __all__ = (
 class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Attributes', (
+        (_('Attributes'), (
             'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visibility',
             'is_cloneable',
         )),
@@ -55,12 +55,15 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
         label=_('Field type')
     )
     group_name = forms.CharField(
+        label=_('Group name'),
         required=False
     )
     weight = forms.IntegerField(
+        label=_('Weight'),
         required=False
     )
     required = forms.NullBooleanField(
+        label=_('Required'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -77,6 +80,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
         label=_('UI visibility')
     )
     is_cloneable = forms.NullBooleanField(
+        label=_('Is cloneable'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -104,22 +108,26 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
         (_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')),
     )
     content_types = ContentTypeMultipleChoiceField(
+        label=_('Content types'),
         queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
         required=False
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     new_window = forms.NullBooleanField(
+        label=_('New window'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     weight = forms.IntegerField(
+        label=_('Weight'),
         required=False
     )
 
@@ -127,8 +135,8 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
 class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Data', ('data_source_id', 'data_file_id')),
-        ('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
+        (_('Data'), ('data_source_id', 'data_file_id')),
+        (_('Attributes'), ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
     )
     data_source_id = DynamicModelMultipleChoiceField(
         queryset=DataSource.objects.all(),
@@ -144,6 +152,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
         }
     )
     content_types = ContentTypeMultipleChoiceField(
+        label=_('Content types'),
         queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
         required=False
     )
@@ -152,9 +161,11 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
         label=_('MIME type')
     )
     file_extension = forms.CharField(
+        label=_('File extension'),
         required=False
     )
     as_attachment = forms.NullBooleanField(
+        label=_('As attachment'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -165,13 +176,15 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
 class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Attributes', ('content_type_id', 'name',)),
+        (_('Attributes'), ('content_type_id', 'name',)),
     )
     content_type_id = ContentTypeChoiceField(
+        label=_('Content type'),
         queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
         required=False
     )
     name = forms.CharField(
+        label=_('Name'),
         required=False
     )
 
@@ -179,25 +192,29 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
 class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Attributes', ('content_types', 'enabled', 'shared', 'weight')),
+        (_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')),
     )
     content_types = ContentTypeMultipleChoiceField(
+        label=_('Content types'),
         queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
         required=False
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     shared = forms.NullBooleanField(
+        label=_('Shared'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
     )
     weight = forms.IntegerField(
+        label=_('Weight'),
         required=False
     )
 
@@ -205,8 +222,8 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
 class WebhookFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Attributes', ('content_type_id', 'http_method', 'enabled')),
-        ('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
+        (_('Attributes'), ('content_type_id', 'http_method', 'enabled')),
+        (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
     )
     content_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
@@ -219,6 +236,7 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
         label=_('HTTP method')
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -278,11 +296,11 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
 class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag_id')),
-        ('Data', ('data_source_id', 'data_file_id')),
-        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
-        ('Device', ('device_type_id', 'platform_id', 'role_id')),
-        ('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id'))
+        (_('Data'), ('data_source_id', 'data_file_id')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
+        (_('Device'), ('device_type_id', 'platform_id', 'role_id')),
+        (_('Cluster'), ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id'))
     )
     data_source_id = DynamicModelMultipleChoiceField(
         queryset=DataSource.objects.all(),
@@ -368,7 +386,7 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
 class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Data', ('data_source_id', 'data_file_id')),
+        (_('Data'), ('data_source_id', 'data_file_id')),
     )
     data_source_id = DynamicModelMultipleChoiceField(
         queryset=DataSource.objects.all(),
@@ -400,8 +418,8 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
     model = JournalEntry
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Creation', ('created_before', 'created_after', 'created_by_id')),
-        ('Attributes', ('assigned_object_type_id', 'kind'))
+        (_('Creation'), ('created_before', 'created_after', 'created_by_id')),
+        (_('Attributes'), ('assigned_object_type_id', 'kind'))
     )
     created_after = forms.DateTimeField(
         required=False,
@@ -430,6 +448,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
         )
     )
     kind = forms.ChoiceField(
+        label=_('Kind'),
         choices=add_blank_choice(JournalEntryKindChoices),
         required=False
     )
@@ -440,8 +459,8 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
     model = ObjectChange
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Time', ('time_before', 'time_after')),
-        ('Attributes', ('action', 'user_id', 'changed_object_type_id')),
+        (_('Time'), ('time_before', 'time_after')),
+        (_('Attributes'), ('action', 'user_id', 'changed_object_type_id')),
     )
     time_after = forms.DateTimeField(
         required=False,
@@ -454,6 +473,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
         widget=DateTimePicker()
     )
     action = forms.ChoiceField(
+        label=_('Action'),
         choices=add_blank_choice(ObjectChangeActionChoices),
         required=False
     )

+ 2 - 0
netbox/extras/forms/misc.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext_lazy as _
 
 __all__ = (
     'RenderMarkdownForm',
@@ -10,5 +11,6 @@ class RenderMarkdownForm(forms.Form):
     Provides basic validation for markup to be rendered.
     """
     text = forms.CharField(
+        label=_('Text'),
         required=False
     )

+ 70 - 44
netbox/extras/forms/model_forms.py

@@ -4,7 +4,7 @@ from django import forms
 from django.conf import settings
 from django.db.models import Q
 from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from core.forms.mixins import SyncedDataMixin
 from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@@ -42,10 +42,12 @@ __all__ = (
 
 class CustomFieldForm(BootstrapMixin, forms.ModelForm):
     content_types = ContentTypeMultipleChoiceField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
     )
     object_type = ContentTypeChoiceField(
+        label=_('Object type'),
         queryset=ContentType.objects.all(),
         # TODO: Come up with a canonical way to register suitable models
         limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']),
@@ -58,12 +60,12 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
     )
 
     fieldsets = (
-        ('Custom Field', (
+        (_('Custom Field'), (
             'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description',
         )),
-        ('Behavior', ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
-        ('Values', ('default', 'choice_set')),
-        ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
+        (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
+        (_('Values'), ('default', 'choice_set')),
+        (_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')),
     )
 
     class Meta:
@@ -106,13 +108,14 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
 
 class CustomLinkForm(BootstrapMixin, forms.ModelForm):
     content_types = ContentTypeMultipleChoiceField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_links')
     )
 
     fieldsets = (
-        ('Custom Link', ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
-        ('Templates', ('link_text', 'link_url')),
+        (_('Custom Link'), ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
+        (_('Templates'), ('link_text', 'link_url')),
     )
 
     class Meta:
@@ -133,18 +136,20 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
 
 class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
     content_types = ContentTypeMultipleChoiceField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('export_templates')
     )
     template_code = forms.CharField(
+        label=_('Template code'),
         required=False,
         widget=forms.Textarea(attrs={'class': 'font-monospace'})
     )
 
     fieldsets = (
-        ('Export Template', ('name', 'content_types', 'description', 'template_code')),
-        ('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
-        ('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
+        (_('Export Template'), ('name', 'content_types', 'description', 'template_code')),
+        (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
+        (_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')),
     )
 
     class Meta:
@@ -165,7 +170,7 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
         super().clean()
 
         if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
-            raise forms.ValidationError("Must specify either local content or a data file")
+            raise forms.ValidationError(_("Must specify either local content or a data file"))
 
         return self.cleaned_data
 
@@ -173,13 +178,14 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
 class SavedFilterForm(BootstrapMixin, forms.ModelForm):
     slug = SlugField()
     content_types = ContentTypeMultipleChoiceField(
+        label=_('Content types'),
         queryset=ContentType.objects.all()
     )
     parameters = JSONField()
 
     fieldsets = (
-        ('Saved Filter', ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')),
-        ('Parameters', ('parameters',)),
+        (_('Saved Filter'), ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')),
+        (_('Parameters'), ('parameters',)),
     )
 
     class Meta:
@@ -198,6 +204,7 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
 
 class BookmarkForm(BootstrapMixin, forms.ModelForm):
     object_type = ContentTypeChoiceField(
+        label=_('Object type'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('bookmarks').get_query()
     )
@@ -209,29 +216,30 @@ class BookmarkForm(BootstrapMixin, forms.ModelForm):
 
 class WebhookForm(BootstrapMixin, forms.ModelForm):
     content_types = ContentTypeMultipleChoiceField(
+        label=_('Content types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('webhooks')
     )
 
     fieldsets = (
-        ('Webhook', ('name', 'content_types', 'enabled')),
-        ('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
-        ('HTTP Request', (
+        (_('Webhook'), ('name', 'content_types', 'enabled')),
+        (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
+        (_('HTTP Request'), (
             'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
         )),
-        ('Conditions', ('conditions',)),
-        ('SSL', ('ssl_verification', 'ca_file_path')),
+        (_('Conditions'), ('conditions',)),
+        (_('SSL'), ('ssl_verification', 'ca_file_path')),
     )
 
     class Meta:
         model = Webhook
         fields = '__all__'
         labels = {
-            'type_create': 'Creations',
-            'type_update': 'Updates',
-            'type_delete': 'Deletions',
-            'type_job_start': 'Job executions',
-            'type_job_end': 'Job terminations',
+            'type_create': _('Creations'),
+            'type_update': _('Updates'),
+            'type_delete': _('Deletions'),
+            'type_job_start': _('Job executions'),
+            'type_job_end': _('Job terminations'),
         }
         widgets = {
             'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
@@ -243,6 +251,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
 class TagForm(BootstrapMixin, forms.ModelForm):
     slug = SlugField()
     object_types = ContentTypeMultipleChoiceField(
+        label=_('Object types'),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('tags'),
         required=False
@@ -261,65 +270,79 @@ class TagForm(BootstrapMixin, forms.ModelForm):
 
 class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
     regions = DynamicModelMultipleChoiceField(
+        label=_('Regions'),
         queryset=Region.objects.all(),
         required=False
     )
     site_groups = DynamicModelMultipleChoiceField(
+        label=_('Site groups'),
         queryset=SiteGroup.objects.all(),
         required=False
     )
     sites = DynamicModelMultipleChoiceField(
+        label=_('Sites'),
         queryset=Site.objects.all(),
         required=False
     )
     locations = DynamicModelMultipleChoiceField(
+        label=_('Locations'),
         queryset=Location.objects.all(),
         required=False
     )
     device_types = DynamicModelMultipleChoiceField(
+        label=_('Device types'),
         queryset=DeviceType.objects.all(),
         required=False
     )
     roles = DynamicModelMultipleChoiceField(
+        label=_('Roles'),
         queryset=DeviceRole.objects.all(),
         required=False
     )
     platforms = DynamicModelMultipleChoiceField(
+        label=_('Platforms'),
         queryset=Platform.objects.all(),
         required=False
     )
     cluster_types = DynamicModelMultipleChoiceField(
+        label=_('Cluster types'),
         queryset=ClusterType.objects.all(),
         required=False
     )
     cluster_groups = DynamicModelMultipleChoiceField(
+        label=_('Cluster groups'),
         queryset=ClusterGroup.objects.all(),
         required=False
     )
     clusters = DynamicModelMultipleChoiceField(
+        label=_('Clusters'),
         queryset=Cluster.objects.all(),
         required=False
     )
     tenant_groups = DynamicModelMultipleChoiceField(
+        label=_('Tenat groups'),
         queryset=TenantGroup.objects.all(),
         required=False
     )
     tenants = DynamicModelMultipleChoiceField(
+        label=_('Tenants'),
         queryset=Tenant.objects.all(),
         required=False
     )
     tags = DynamicModelMultipleChoiceField(
+        label=_('Tags'),
         queryset=Tag.objects.all(),
         required=False
     )
     data = JSONField(
+        label=_('Data'),
         required=False
     )
 
     fieldsets = (
-        ('Config Context', ('name', 'weight', 'description', 'data', 'is_active')),
-        ('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
-        ('Assignment', (
+        (_('Config Context'), ('name', 'weight', 'description', 'data', 'is_active')),
+        (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
+        (_('Assignment'), (
             'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
             'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
         )),
@@ -351,25 +374,27 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
         super().clean()
 
         if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'):
-            raise forms.ValidationError("Must specify either local data or a data file")
+            raise forms.ValidationError(_("Must specify either local data or a data file"))
 
         return self.cleaned_data
 
 
 class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
     tags = DynamicModelMultipleChoiceField(
+        label=_('Tags'),
         queryset=Tag.objects.all(),
         required=False
     )
     template_code = forms.CharField(
+        label=_('Template code'),
         required=False,
         widget=forms.Textarea(attrs={'class': 'font-monospace'})
     )
 
     fieldsets = (
-        ('Config Template', ('name', 'description', 'environment_params', 'tags')),
-        ('Content', ('template_code',)),
-        ('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
+        (_('Config Template'), ('name', 'description', 'environment_params', 'tags')),
+        (_('Content'), ('template_code',)),
+        (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
     )
 
     class Meta:
@@ -393,7 +418,7 @@ class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
         super().clean()
 
         if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
-            raise forms.ValidationError("Must specify either local content or a data file")
+            raise forms.ValidationError(_("Must specify either local content or a data file"))
 
         return self.cleaned_data
 
@@ -409,6 +434,7 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
 
 class JournalEntryForm(NetBoxModelForm):
     kind = forms.ChoiceField(
+        label=_('Kind'),
         choices=add_blank_choice(JournalEntryKindChoices),
         required=False
     )
@@ -451,16 +477,16 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe
     """
 
     fieldsets = (
-        ('Rack Elevations', ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
-        ('Power', ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
-        ('IPAM', ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
-        ('Security', ('ALLOWED_URL_SCHEMES',)),
-        ('Banners', ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
-        ('Pagination', ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
-        ('Validation', ('CUSTOM_VALIDATORS',)),
-        ('User Preferences', ('DEFAULT_USER_PREFERENCES',)),
-        ('Miscellaneous', ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')),
-        ('Config Revision', ('comment',))
+        (_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
+        (_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
+        (_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
+        (_('Security'), ('ALLOWED_URL_SCHEMES',)),
+        (_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
+        (_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
+        (_('Validation'), ('CUSTOM_VALIDATORS',)),
+        (_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
+        (_('Miscellaneous'), ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')),
+        (_('Config Revision'), ('comment',))
     )
 
     class Meta:
@@ -487,11 +513,11 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe
                 help_text = self.fields[param.name].help_text
                 if help_text:
                     help_text += '<br />'  # Line break
-                help_text += f'Current value: <strong>{value}</strong>'
+                help_text += _('Current value: <strong>{value}</strong>').format(value=value)
                 if is_static:
-                    help_text += ' (defined statically)'
+                    help_text += _(' (defined statically)')
                 elif value == param.default:
-                    help_text += ' (default)'
+                    help_text += _(' (default)')
                 self.fields[param.name].help_text = help_text
                 self.fields[param.name].initial = value
             if is_static:

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

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from extras.choices import DurationChoices
 from utilities.forms import BootstrapMixin
@@ -33,7 +33,7 @@ class ReportForm(BootstrapMixin, forms.Form):
 
         # Annotate the current system time for reference
         now = local_now().strftime('%Y-%m-%d %H:%M:%S')
-        self.fields['schedule_at'].help_text += f' (current time: <strong>{now}</strong>)'
+        self.fields['schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
 
         # Remove scheduling fields if scheduling is disabled
         if not scheduling_enabled:

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

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from extras.choices import DurationChoices
 from utilities.forms import BootstrapMixin
@@ -39,7 +39,7 @@ class ScriptForm(BootstrapMixin, forms.Form):
 
         # Annotate the current system time for reference
         now = local_now().strftime('%Y-%m-%d %H:%M:%S')
-        self.fields['_schedule_at'].help_text += f' (current time: <strong>{now}</strong>)'
+        self.fields['_schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
 
         # Remove scheduling fields if scheduling is disabled
         if not scheduling_enabled:

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

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from utilities.forms import BootstrapMixin
 from utilities.forms.fields import ExpandableIPAddressField

+ 69 - 39
netbox/ipam/forms/bulk_edit.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.models import Region, Site, SiteGroup
 from ipam.choices import *
@@ -37,6 +37,7 @@ __all__ = (
 
 class VRFBulkEditForm(NetBoxModelBulkEditForm):
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
@@ -46,12 +47,11 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Enforce unique space')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = VRF
     fieldsets = (
@@ -62,16 +62,16 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
 
 class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = RouteTarget
     fieldsets = (
@@ -82,10 +82,12 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
 
 class RIRBulkEditForm(NetBoxModelBulkEditForm):
     is_private = forms.NullBooleanField(
+        label=_('Is private'),
         required=False,
         widget=BulkEditNullBooleanSelect
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -104,10 +106,12 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
         label=_('RIR')
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -121,6 +125,7 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
 
 class ASNBulkEditForm(NetBoxModelBulkEditForm):
     sites = DynamicModelMultipleChoiceField(
+        label=_('Sites'),
         queryset=Site.objects.all(),
         required=False
     )
@@ -130,16 +135,16 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
         label=_('RIR')
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = ASN
     fieldsets = (
@@ -155,19 +160,20 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
         label=_('RIR')
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     date_added = forms.DateField(
+        label=_('Date added'),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = Aggregate
     fieldsets = (
@@ -178,9 +184,11 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
 
 class RoleBulkEditForm(NetBoxModelBulkEditForm):
     weight = forms.IntegerField(
+        label=_('Weight'),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -194,14 +202,17 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
 
 class PrefixBulkEditForm(NetBoxModelBulkEditForm):
     region = DynamicModelChoiceField(
+        label=_('Region'),
         queryset=Region.objects.all(),
         required=False
     )
     site_group = DynamicModelChoiceField(
+        label=_('Site group'),
         queryset=SiteGroup.objects.all(),
         required=False
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         query_params={
@@ -215,19 +226,23 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
         label=_('VRF')
     )
     prefix_length = forms.IntegerField(
+        label=_('Prefix length'),
         min_value=PREFIX_LENGTH_MIN,
         max_value=PREFIX_LENGTH_MAX,
         required=False
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(PrefixStatusChoices),
         required=False
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False
     )
@@ -242,18 +257,17 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Treat as 100% utilized')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = Prefix
     fieldsets = (
         (None, ('tenant', 'status', 'role', 'description')),
-        ('Site', ('region', 'site_group', 'site')),
-        ('Addressing', ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')),
+        (_('Site'), ('region', 'site_group', 'site')),
+        (_('Addressing'), ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')),
     )
     nullable_fields = (
         'site', 'vrf', 'tenant', 'role', 'description', 'comments',
@@ -267,14 +281,17 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
         label=_('VRF')
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(IPRangeStatusChoices),
         required=False
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False
     )
@@ -284,12 +301,11 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Treat as 100% utilized')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = IPRange
     fieldsets = (
@@ -307,19 +323,23 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
         label=_('VRF')
     )
     mask_length = forms.IntegerField(
+        label=_('Mask length'),
         min_value=IPADDRESS_MASK_LENGTH_MIN,
         max_value=IPADDRESS_MASK_LENGTH_MAX,
         required=False
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(IPAddressStatusChoices),
         required=False
     )
     role = forms.ChoiceField(
+        label=_('Role'),
         choices=add_blank_choice(IPAddressRoleChoices),
         required=False
     )
@@ -329,17 +349,16 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
         label=_('DNS name')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = IPAddress
     fieldsets = (
         (None, ('status', 'role', 'tenant', 'description')),
-        ('Addressing', ('vrf', 'mask_length', 'dns_name')),
+        (_('Addressing'), ('vrf', 'mask_length', 'dns_name')),
     )
     nullable_fields = (
         'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
@@ -348,6 +367,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
 
 class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
     protocol = forms.ChoiceField(
+        label=_('Protocol'),
         choices=add_blank_choice(FHRPGroupProtocolChoices),
         required=False
     )
@@ -367,27 +387,28 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Authentication key')
     )
     name = forms.CharField(
+        label=_('Name'),
         max_length=100,
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = FHRPGroup
     fieldsets = (
         (None, ('protocol', 'group_id', 'name', 'description')),
-        ('Authentication', ('auth_type', 'auth_key')),
+        (_('Authentication'), ('auth_type', 'auth_key')),
     )
     nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
 
 
 class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False
     )
@@ -404,6 +425,7 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Maximum child VLAN VID')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -417,14 +439,17 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 class VLANBulkEditForm(NetBoxModelBulkEditForm):
     region = DynamicModelChoiceField(
+        label=_('Region'),
         queryset=Region.objects.all(),
         required=False
     )
     site_group = DynamicModelChoiceField(
+        label=_('Site group'),
         queryset=SiteGroup.objects.all(),
         required=False
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         query_params={
@@ -433,6 +458,7 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
         }
     )
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=VLANGroup.objects.all(),
         required=False,
         query_params={
@@ -440,29 +466,31 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
         }
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(VLANStatusChoices),
         required=False
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = VLAN
     fieldsets = (
         (None, ('status', 'role', 'tenant', 'description')),
-        ('Site & Group', ('region', 'site_group', 'site', 'group')),
+        (_('Site & Group'), ('region', 'site_group', 'site', 'group')),
     )
     nullable_fields = (
         'site', 'group', 'tenant', 'role', 'description', 'comments',
@@ -471,10 +499,12 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
 
 class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
     protocol = forms.ChoiceField(
+        label=_('Protocol'),
         choices=add_blank_choice(ServiceProtocolChoices),
         required=False
     )
     ports = NumericArrayField(
+        label=_('Ports'),
         base_field=forms.IntegerField(
             min_value=SERVICE_PORT_MIN,
             max_value=SERVICE_PORT_MAX
@@ -482,12 +512,11 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = ServiceTemplate
     fieldsets = (
@@ -502,20 +531,21 @@ class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
 
 class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
     type = forms.ChoiceField(
+        label=_('Type'),
         choices=add_blank_choice(L2VPNTypeChoices),
         required=False
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = L2VPN
     fieldsets = (

+ 52 - 8
netbox/ipam/forms/bulk_import.py

@@ -2,7 +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 django.utils.translation import gettext_lazy as _
 
 from dcim.models import Device, Interface, Site
 from ipam.choices import *
@@ -36,6 +36,7 @@ __all__ = (
 
 class VRFImportForm(NetBoxModelImportForm):
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -49,6 +50,7 @@ class VRFImportForm(NetBoxModelImportForm):
 
 class RouteTargetImportForm(NetBoxModelImportForm):
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -70,11 +72,13 @@ class RIRImportForm(NetBoxModelImportForm):
 
 class AggregateImportForm(NetBoxModelImportForm):
     rir = CSVModelChoiceField(
+        label=_('RIR'),
         queryset=RIR.objects.all(),
         to_field_name='name',
         help_text=_('Assigned RIR')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -88,11 +92,13 @@ class AggregateImportForm(NetBoxModelImportForm):
 
 class ASNRangeImportForm(NetBoxModelImportForm):
     rir = CSVModelChoiceField(
+        label=_('RIR'),
         queryset=RIR.objects.all(),
         to_field_name='name',
         help_text=_('Assigned RIR')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -106,11 +112,13 @@ class ASNRangeImportForm(NetBoxModelImportForm):
 
 class ASNImportForm(NetBoxModelImportForm):
     rir = CSVModelChoiceField(
+        label=_('RIR'),
         queryset=RIR.objects.all(),
         to_field_name='name',
         help_text=_('Assigned RIR')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
@@ -132,40 +140,47 @@ class RoleImportForm(NetBoxModelImportForm):
 
 class PrefixImportForm(NetBoxModelImportForm):
     vrf = CSVModelChoiceField(
+        label=_('VRF'),
         queryset=VRF.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned VRF')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned tenant')
     )
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned site')
     )
     vlan_group = CSVModelChoiceField(
+        label=_('VLAN group'),
         queryset=VLANGroup.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_("VLAN's group (if any)")
     )
     vlan = CSVModelChoiceField(
+        label=_('VLAN'),
         queryset=VLAN.objects.all(),
         required=False,
         to_field_name='vid',
         help_text=_("Assigned VLAN")
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=PrefixStatusChoices,
         help_text=_('Operational status')
     )
     role = CSVModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False,
         to_field_name='name',
@@ -211,22 +226,26 @@ class PrefixImportForm(NetBoxModelImportForm):
 
 class IPRangeImportForm(NetBoxModelImportForm):
     vrf = CSVModelChoiceField(
+        label=_('VRF'),
         queryset=VRF.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned VRF')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned tenant')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=IPRangeStatusChoices,
         help_text=_('Operational status')
     )
     role = CSVModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False,
         to_field_name='name',
@@ -243,45 +262,53 @@ class IPRangeImportForm(NetBoxModelImportForm):
 
 class IPAddressImportForm(NetBoxModelImportForm):
     vrf = CSVModelChoiceField(
+        label=_('VRF'),
         queryset=VRF.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned VRF')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned tenant')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=IPAddressStatusChoices,
         help_text=_('Operational status')
     )
     role = CSVChoiceField(
+        label=_('Role'),
         choices=IPAddressRoleChoices,
         required=False,
         help_text=_('Functional role')
     )
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Parent device of assigned interface (if any)')
     )
     virtual_machine = CSVModelChoiceField(
+        label=_('Virtual machine'),
         queryset=VirtualMachine.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Parent VM of assigned interface (if any)')
     )
     interface = CSVModelChoiceField(
+        label=_('Interface'),
         queryset=Interface.objects.none(),  # Can also refer to VMInterface
         required=False,
         to_field_name='name',
         help_text=_('Assigned interface')
     )
     is_primary = forms.BooleanField(
+        label=_('Is primary'),
         help_text=_('Make this the primary IP for the assigned device'),
         required=False
     )
@@ -321,11 +348,11 @@ class IPAddressImportForm(NetBoxModelImportForm):
         # Validate is_primary
         if is_primary and not device and not virtual_machine:
             raise forms.ValidationError({
-                "is_primary": "No device or virtual machine specified; cannot set as primary IP"
+                "is_primary": _("No device or virtual machine specified; cannot set as primary IP")
             })
         if is_primary and not interface:
             raise forms.ValidationError({
-                "is_primary": "No interface specified; cannot set as primary IP"
+                "is_primary": _("No interface specified; cannot set as primary IP")
             })
 
     def save(self, *args, **kwargs):
@@ -350,9 +377,11 @@ class IPAddressImportForm(NetBoxModelImportForm):
 
 class FHRPGroupImportForm(NetBoxModelImportForm):
     protocol = CSVChoiceField(
+        label=_('Protocol'),
         choices=FHRPGroupProtocolChoices
     )
     auth_type = CSVChoiceField(
+        label=_('Auth type'),
         choices=FHRPGroupAuthTypeChoices,
         required=False
     )
@@ -373,13 +402,13 @@ class VLANGroupImportForm(NetBoxModelImportForm):
         min_value=VLAN_VID_MIN,
         max_value=VLAN_VID_MAX,
         required=False,
-        label=f'Minimum child VLAN VID (default: {VLAN_VID_MIN})'
+        label=_('Minimum child VLAN VID (default: {minimum})').format(minimum=VLAN_VID_MIN)
     )
     max_vid = forms.IntegerField(
         min_value=VLAN_VID_MIN,
         max_value=VLAN_VID_MAX,
         required=False,
-        label=f'Maximum child VLAN VID (default: {VLAN_VID_MIN})'
+        label=_('Maximum child VLAN VID (default: {maximum})').format(maximum=VLAN_VID_MIN)
     )
 
     class Meta:
@@ -392,28 +421,33 @@ class VLANGroupImportForm(NetBoxModelImportForm):
 
 class VLANImportForm(NetBoxModelImportForm):
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned site')
     )
     group = CSVModelChoiceField(
+        label=_('Group'),
         queryset=VLANGroup.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned VLAN group')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned tenant')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=VLANStatusChoices,
         help_text=_('Operational status')
     )
     role = CSVModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False,
         to_field_name='name',
@@ -427,6 +461,7 @@ class VLANImportForm(NetBoxModelImportForm):
 
 class ServiceTemplateImportForm(NetBoxModelImportForm):
     protocol = CSVChoiceField(
+        label=_('Protocol'),
         choices=ServiceProtocolChoices,
         help_text=_('IP protocol')
     )
@@ -438,18 +473,21 @@ class ServiceTemplateImportForm(NetBoxModelImportForm):
 
 class ServiceImportForm(NetBoxModelImportForm):
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Required if not assigned to a VM')
     )
     virtual_machine = CSVModelChoiceField(
+        label=_('Virtual machine'),
         queryset=VirtualMachine.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Required if not assigned to a device')
     )
     protocol = CSVChoiceField(
+        label=_('Protocol'),
         choices=ServiceProtocolChoices,
         help_text=_('IP protocol')
     )
@@ -461,11 +499,13 @@ class ServiceImportForm(NetBoxModelImportForm):
 
 class L2VPNImportForm(NetBoxModelImportForm):
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
     )
     type = CSVChoiceField(
+        label=_('Type'),
         choices=L2VPNTypeChoices,
         help_text=_('L2VPN type')
     )
@@ -484,24 +524,28 @@ class L2VPNTerminationImportForm(NetBoxModelImportForm):
         label=_('L2VPN'),
     )
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Parent device (for interface)')
     )
     virtual_machine = CSVModelChoiceField(
+        label=_('Virtual machine'),
         queryset=VirtualMachine.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Parent virtual machine (for interface)')
     )
     interface = CSVModelChoiceField(
+        label=_('Interface'),
         queryset=Interface.objects.none(),  # Can also refer to VMInterface
         required=False,
         to_field_name='name',
         help_text=_('Assigned interface (device or VM)')
     )
     vlan = CSVModelChoiceField(
+        label=_('VLAN'),
         queryset=VLAN.objects.all(),
         required=False,
         to_field_name='name',
@@ -531,10 +575,10 @@ class L2VPNTerminationImportForm(NetBoxModelImportForm):
         super().clean()
 
         if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
-            raise ValidationError('Cannot import device and VM interface terminations simultaneously.')
+            raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
         if not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
-            raise ValidationError('Each termination must specify either an interface or a VLAN.')
+            raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
         if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
-            raise ValidationError('Cannot assign both an interface and a VLAN.')
+            raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
 
         self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')

+ 49 - 37
netbox/ipam/forms/filtersets.py

@@ -1,6 +1,6 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
 from ipam.choices import *
@@ -47,8 +47,8 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = VRF
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Route Targets', ('import_target_id', 'export_target_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Route Targets'), ('import_target_id', 'export_target_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     import_target_id = DynamicModelMultipleChoiceField(
         queryset=RouteTarget.objects.all(),
@@ -67,8 +67,8 @@ class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = RouteTarget
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('VRF', ('importing_vrf_id', 'exporting_vrf_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('VRF'), ('importing_vrf_id', 'exporting_vrf_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     importing_vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
@@ -99,8 +99,8 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = Aggregate
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('family', 'rir_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Attributes'), ('family', 'rir_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     family = forms.ChoiceField(
         required=False,
@@ -119,8 +119,8 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = ASNRange
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Range', ('rir_id', 'start', 'end')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Range'), ('rir_id', 'start', 'end')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     rir_id = DynamicModelMultipleChoiceField(
         queryset=RIR.objects.all(),
@@ -128,9 +128,11 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('RIR')
     )
     start = forms.IntegerField(
+        label=_('Start'),
         required=False
     )
     end = forms.IntegerField(
+        label=_('End'),
         required=False
     )
     tag = TagFilterField(model)
@@ -140,8 +142,8 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = ASN
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Assignment', ('rir_id', 'site_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Assignment'), ('rir_id', 'site_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     rir_id = DynamicModelMultipleChoiceField(
         queryset=RIR.objects.all(),
@@ -165,10 +167,10 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = Prefix
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Addressing', ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')),
-        ('VRF', ('vrf_id', 'present_in_vrf_id')),
-        ('Location', ('region_id', 'site_group_id', 'site_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Addressing'), ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')),
+        (_('VRF'), ('vrf_id', 'present_in_vrf_id')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     mask_length__lte = forms.IntegerField(
         widget=forms.HiddenInput()
@@ -204,6 +206,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('Present in VRF')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=PrefixStatusChoices,
         required=False
     )
@@ -253,8 +256,8 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = IPRange
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attriubtes', ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Attriubtes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     family = forms.ChoiceField(
         required=False,
@@ -268,6 +271,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         null_option='Global'
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=IPRangeStatusChoices,
         required=False
     )
@@ -291,10 +295,10 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = IPAddress
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
-        ('VRF', ('vrf_id', 'present_in_vrf_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Device/VM', ('device_id', 'virtual_machine_id')),
+        (_('Attributes'), ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
+        (_('VRF'), ('vrf_id', 'present_in_vrf_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Device/VM'), ('device_id', 'virtual_machine_id')),
     )
     parent = forms.CharField(
         required=False,
@@ -337,10 +341,12 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('Assigned VM'),
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=IPAddressStatusChoices,
         required=False
     )
     role = forms.MultipleChoiceField(
+        label=_('Role'),
         choices=IPAddressRoleChoices,
         required=False
     )
@@ -358,29 +364,31 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
     model = FHRPGroup
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('name', 'protocol', 'group_id')),
-        ('Authentication', ('auth_type', 'auth_key')),
+        (_('Attributes'), ('name', 'protocol', 'group_id')),
+        (_('Authentication'), ('auth_type', 'auth_key')),
     )
     name = forms.CharField(
+        label=_('Name'),
         required=False
     )
     protocol = forms.MultipleChoiceField(
+        label=_('Protocol'),
         choices=FHRPGroupProtocolChoices,
         required=False
     )
     group_id = forms.IntegerField(
         min_value=0,
         required=False,
-        label='Group ID'
+        label=_('Group ID')
     )
     auth_type = forms.MultipleChoiceField(
         choices=FHRPGroupAuthTypeChoices,
         required=False,
-        label='Authentication type'
+        label=_('Authentication type')
     )
     auth_key = forms.CharField(
         required=False,
-        label='Authentication key'
+        label=_('Authentication key')
     )
     tag = TagFilterField(model)
 
@@ -388,8 +396,8 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
 class VLANGroupFilterForm(NetBoxModelFilterSetForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region', 'sitegroup', 'site', 'location', 'rack')),
-        ('VLAN ID', ('min_vid', 'max_vid')),
+        (_('Location'), ('region', 'sitegroup', 'site', 'location', 'rack')),
+        (_('VLAN ID'), ('min_vid', 'max_vid')),
     )
     model = VLANGroup
     region = DynamicModelMultipleChoiceField(
@@ -436,9 +444,9 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = VLAN
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Location', ('region_id', 'site_group_id', 'site_id')),
-        ('Attributes', ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
+        (_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
@@ -469,6 +477,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('VLAN group')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=VLANStatusChoices,
         required=False
     )
@@ -480,7 +489,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     )
     vid = forms.IntegerField(
         required=False,
-        label='VLAN ID'
+        label=_('VLAN ID')
     )
     l2vpn_id = DynamicModelMultipleChoiceField(
         queryset=L2VPN.objects.all(),
@@ -494,13 +503,15 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
     model = ServiceTemplate
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('protocol', 'port')),
+        (_('Attributes'), ('protocol', 'port')),
     )
     protocol = forms.ChoiceField(
+        label=_('Protocol'),
         choices=add_blank_choice(ServiceProtocolChoices),
         required=False
     )
     port = forms.IntegerField(
+        label=_('Port'),
         required=False,
     )
     tag = TagFilterField(model)
@@ -515,10 +526,11 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = L2VPN
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('type', 'import_target_id', 'export_target_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
+        (_('Attributes'), ('type', 'import_target_id', 'export_target_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
     type = forms.ChoiceField(
+        label=_('Type'),
         choices=add_blank_choice(L2VPNTypeChoices),
         required=False
     )
@@ -539,14 +551,14 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
     model = L2VPNTermination
     fieldsets = (
         (None, ('filter_id', 'l2vpn_id',)),
-        ('Assigned Object', (
+        (_('Assigned Object'), (
             'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
         )),
     )
     l2vpn_id = DynamicModelChoiceField(
         queryset=L2VPN.objects.all(),
         required=False,
-        label='L2VPN'
+        label=_('L2VPN')
     )
     assigned_object_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),

+ 59 - 34
netbox/ipam/forms/model_forms.py

@@ -1,7 +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 django.utils.translation import gettext_lazy as _
 
 from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
 from ipam.choices import *
@@ -46,19 +46,21 @@ __all__ = (
 
 class VRFForm(TenancyForm, NetBoxModelForm):
     import_targets = DynamicModelMultipleChoiceField(
+        label=_('Import targets'),
         queryset=RouteTarget.objects.all(),
         required=False
     )
     export_targets = DynamicModelMultipleChoiceField(
+        label=_('Export targets'),
         queryset=RouteTarget.objects.all(),
         required=False
     )
     comments = CommentField()
 
     fieldsets = (
-        ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')),
-        ('Route Targets', ('import_targets', 'export_targets')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('VRF'), ('name', 'rd', 'enforce_unique', 'description', 'tags')),
+        (_('Route Targets'), ('import_targets', 'export_targets')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -90,7 +92,7 @@ class RIRForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('RIR', (
+        (_('RIR'), (
             'name', 'slug', 'is_private', 'description', 'tags',
         )),
     )
@@ -110,8 +112,8 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('Aggregate'), ('prefix', 'rir', 'date_added', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -131,8 +133,8 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm):
     )
     slug = SlugField()
     fieldsets = (
-        ('ASN Range', ('name', 'slug', 'rir', 'start', 'end', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('ASN Range'), ('name', 'slug', 'rir', 'start', 'end', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -155,8 +157,8 @@ class ASNForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('ASN', ('asn', 'rir', 'sites', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('ASN'), ('asn', 'rir', 'sites', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -184,7 +186,7 @@ class RoleForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Role', (
+        (_('Role'), (
             'name', 'slug', 'weight', 'description', 'tags',
         )),
     )
@@ -203,6 +205,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
         label=_('VRF')
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         selector=True,
@@ -215,15 +218,16 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
         label=_('VLAN'),
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
-        ('Site/VLAN Assignment', ('site', 'vlan')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('Prefix'), ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
+        (_('Site/VLAN Assignment'), ('site', 'vlan')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -241,14 +245,15 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
         label=_('VRF')
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False
     )
     comments = CommentField()
 
     fieldsets = (
-        ('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('IP Range'), ('vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -261,6 +266,7 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
 
 class IPAddressForm(TenancyForm, NetBoxModelForm):
     interface = DynamicModelChoiceField(
+        label=_('Interface'),
         queryset=Interface.objects.all(),
         required=False,
         selector=True,
@@ -341,13 +347,13 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
         ]
         if len(selected_objects) > 1:
             raise forms.ValidationError({
-                selected_objects[1]: "An IP address can only be assigned to a single object."
+                selected_objects[1]: _("An IP address can only be assigned to a single object.")
             })
         elif selected_objects:
             assigned_object = self.cleaned_data[selected_objects[0]]
             if self.instance.pk and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
                 raise ValidationError(
-                    "Cannot reassign IP address while it is designated as the primary IP for the parent object"
+                    _("Cannot reassign IP address while it is designated as the primary IP for the parent object")
                 )
             self.instance.assigned_object = assigned_object
         else:
@@ -357,19 +363,21 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
         interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
         if self.cleaned_data.get('primary_for_parent') and not interface:
             self.add_error(
-                'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs."
+                'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.")
             )
 
         # Do not allow assigning a network ID or broadcast address to an interface.
         if interface and (address := self.cleaned_data.get('address')):
             if address.ip == address.network:
-                msg = f"{address} is a network ID, which may not be assigned to an interface."
+                msg = _("{address} is a network ID, which may not be assigned to an interface.").format(address=address)
                 if address.version == 4 and address.prefixlen not in (31, 32):
                     raise ValidationError(msg)
                 if address.version == 6 and address.prefixlen not in (127, 128):
                     raise ValidationError(msg)
             if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32):
-                msg = f"{address} is a broadcast address, which may not be assigned to an interface."
+                msg = _("{address} is a broadcast address, which may not be assigned to an interface.").format(
+                    address=address
+                )
                 raise ValidationError(msg)
 
     def save(self, *args, **kwargs):
@@ -442,9 +450,9 @@ class FHRPGroupForm(NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')),
-        ('Authentication', ('auth_type', 'auth_key')),
-        ('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status'))
+        (_('FHRP Group'), ('protocol', 'group_id', 'name', 'description', 'tags')),
+        (_('Authentication'), ('auth_type', 'auth_key')),
+        (_('Virtual IP Address'), ('ip_vrf', 'ip_address', 'ip_status'))
     )
 
     class Meta:
@@ -497,6 +505,7 @@ class FHRPGroupForm(NetBoxModelForm):
 
 class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=FHRPGroup.objects.all()
     )
 
@@ -514,10 +523,12 @@ class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
 
 class VLANGroupForm(NetBoxModelForm):
     scope_type = ContentTypeChoiceField(
+        label=_('Scope type'),
         queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
         required=False
     )
     region = DynamicModelChoiceField(
+        label=_('Region'),
         queryset=Region.objects.all(),
         required=False,
         initial_params={
@@ -533,6 +544,7 @@ class VLANGroupForm(NetBoxModelForm):
         label=_('Site group')
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         initial_params={
@@ -544,6 +556,7 @@ class VLANGroupForm(NetBoxModelForm):
         }
     )
     location = DynamicModelChoiceField(
+        label=_('Location'),
         queryset=Location.objects.all(),
         required=False,
         initial_params={
@@ -554,6 +567,7 @@ class VLANGroupForm(NetBoxModelForm):
         }
     )
     rack = DynamicModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         required=False,
         query_params={
@@ -570,6 +584,7 @@ class VLANGroupForm(NetBoxModelForm):
         label=_('Cluster group')
     )
     cluster = DynamicModelChoiceField(
+        label=_('Cluster'),
         queryset=Cluster.objects.all(),
         required=False,
         query_params={
@@ -579,9 +594,9 @@ class VLANGroupForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('VLAN Group', ('name', 'slug', 'description', 'tags')),
-        ('Child VLANs', ('min_vid', 'max_vid')),
-        ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
+        (_('VLAN Group'), ('name', 'slug', 'description', 'tags')),
+        (_('Child VLANs'), ('min_vid', 'max_vid')),
+        (_('Scope'), ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
     )
 
     class Meta:
@@ -621,12 +636,14 @@ class VLANForm(TenancyForm, NetBoxModelForm):
         label=_('VLAN Group')
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         null_option='None',
         selector=True
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=Role.objects.all(),
         required=False
     )
@@ -642,6 +659,7 @@ class VLANForm(TenancyForm, NetBoxModelForm):
 
 class ServiceTemplateForm(NetBoxModelForm):
     ports = NumericArrayField(
+        label=_('Ports'),
         base_field=forms.IntegerField(
             min_value=SERVICE_PORT_MIN,
             max_value=SERVICE_PORT_MAX
@@ -651,7 +669,7 @@ class ServiceTemplateForm(NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Service Template', (
+        (_('Service Template'), (
             'name', 'protocol', 'ports', 'description', 'tags',
         )),
     )
@@ -663,16 +681,19 @@ class ServiceTemplateForm(NetBoxModelForm):
 
 class ServiceForm(NetBoxModelForm):
     device = DynamicModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         required=False,
         selector=True
     )
     virtual_machine = DynamicModelChoiceField(
+        label=_('Virtual machine'),
         queryset=VirtualMachine.objects.all(),
         required=False,
         selector=True
     )
     ports = NumericArrayField(
+        label=_('Ports'),
         base_field=forms.IntegerField(
             min_value=SERVICE_PORT_MIN,
             max_value=SERVICE_PORT_MAX
@@ -699,6 +720,7 @@ class ServiceForm(NetBoxModelForm):
 
 class ServiceCreateForm(ServiceForm):
     service_template = DynamicModelChoiceField(
+        label=_('Service template'),
         queryset=ServiceTemplate.objects.all(),
         required=False
     )
@@ -739,19 +761,21 @@ class ServiceCreateForm(ServiceForm):
 class L2VPNForm(TenancyForm, NetBoxModelForm):
     slug = SlugField()
     import_targets = DynamicModelMultipleChoiceField(
+        label=_('Import targets'),
         queryset=RouteTarget.objects.all(),
         required=False
     )
     export_targets = DynamicModelMultipleChoiceField(
+        label=_('Export targets'),
         queryset=RouteTarget.objects.all(),
         required=False
     )
     comments = CommentField()
 
     fieldsets = (
-        ('L2VPN', ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
-        ('Route Targets', ('import_targets', 'export_targets')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
+        (_('Route Targets'), ('import_targets', 'export_targets')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -777,6 +801,7 @@ class L2VPNTerminationForm(NetBoxModelForm):
         label=_('VLAN')
     )
     interface = DynamicModelChoiceField(
+        label=_('Interface'),
         queryset=Interface.objects.all(),
         required=False,
         selector=True
@@ -815,8 +840,8 @@ class L2VPNTerminationForm(NetBoxModelForm):
         vlan = self.cleaned_data.get('vlan')
 
         if not (interface or vminterface or vlan):
-            raise ValidationError('A termination must specify an interface or VLAN.')
+            raise ValidationError(_('A termination must specify an interface or VLAN.'))
         if len([x for x in (interface, vminterface, vlan) if x]) > 1:
-            raise ValidationError('A termination can only have one terminating object (an interface or VLAN).')
+            raise ValidationError(_('A termination can only have one terminating object (an interface or VLAN).'))
 
         self.instance.assigned_object = interface or vminterface or vlan

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

@@ -1,7 +1,7 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
 from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin
@@ -28,7 +28,8 @@ class NetBoxModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm):
     fieldsets = ()
     tags = DynamicModelMultipleChoiceField(
         queryset=Tag.objects.all(),
-        required=False
+        required=False,
+        label=_('Tags'),
     )
 
     def __init__(self, *args, **kwargs):
@@ -73,10 +74,12 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
     Base form for creating a NetBox objects from CSV data. Used for bulk importing.
     """
     id = forms.IntegerField(
+        label=_('Id'),
         required=False,
         help_text='Numeric ID of an existing object to update (if not creating a new object)'
     )
     tags = CSVModelMultipleChoiceField(
+        label=_('Tags'),
         queryset=Tag.objects.all(),
         required=False,
         to_field_name='slug',
@@ -109,10 +112,12 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
         widget=forms.MultipleHiddenInput
     )
     add_tags = DynamicModelMultipleChoiceField(
+        label=_('Add tags'),
         queryset=Tag.objects.all(),
         required=False
     )
     remove_tags = DynamicModelMultipleChoiceField(
+        label=_('Remove tags'),
         queryset=Tag.objects.all(),
         required=False
     )

+ 18 - 3
netbox/tenancy/forms/bulk_edit.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext_lazy as _
 
 from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.choices import ContactPriorityChoices
@@ -22,10 +23,12 @@ __all__ = (
 
 class TenantGroupBulkEditForm(NetBoxModelBulkEditForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=TenantGroup.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -36,6 +39,7 @@ class TenantGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 class TenantBulkEditForm(NetBoxModelBulkEditForm):
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=TenantGroup.objects.all(),
         required=False
     )
@@ -53,10 +57,12 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm):
 
 class ContactGroupBulkEditForm(NetBoxModelBulkEditForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=ContactGroup.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Desciption'),
         max_length=200,
         required=False
     )
@@ -70,6 +76,7 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 class ContactRoleBulkEditForm(NetBoxModelBulkEditForm):
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -83,34 +90,39 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm):
 
 class ContactBulkEditForm(NetBoxModelBulkEditForm):
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=ContactGroup.objects.all(),
         required=False
     )
     title = forms.CharField(
+        label=_('Title'),
         max_length=100,
         required=False
     )
     phone = forms.CharField(
+        label=_('Phone'),
         max_length=50,
         required=False
     )
     email = forms.EmailField(
+        label=_('Email'),
         required=False
     )
     address = forms.CharField(
+        label=_('Address'),
         max_length=200,
         required=False
     )
     link = forms.URLField(
+        label=_('Link'),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = Contact
     fieldsets = (
@@ -121,14 +133,17 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm):
 
 class ContactAssignmentBulkEditForm(NetBoxModelBulkEditForm):
     contact = DynamicModelChoiceField(
+        label=_('Contact'),
         queryset=Contact.objects.all(),
         required=False
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=ContactRole.objects.all(),
         required=False
     )
     priority = forms.ChoiceField(
+        label=_('Priority'),
         choices=add_blank_choice(ContactPriorityChoices),
         required=False
     )

+ 6 - 1
netbox/tenancy/forms/bulk_import.py

@@ -1,5 +1,6 @@
 from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
+
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import *
 from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, SlugField
@@ -20,6 +21,7 @@ __all__ = (
 
 class TenantGroupImportForm(NetBoxModelImportForm):
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=TenantGroup.objects.all(),
         required=False,
         to_field_name='name',
@@ -35,6 +37,7 @@ class TenantGroupImportForm(NetBoxModelImportForm):
 class TenantImportForm(NetBoxModelImportForm):
     slug = SlugField()
     group = CSVModelChoiceField(
+        label=_('Group'),
         queryset=TenantGroup.objects.all(),
         required=False,
         to_field_name='name',
@@ -52,6 +55,7 @@ class TenantImportForm(NetBoxModelImportForm):
 
 class ContactGroupImportForm(NetBoxModelImportForm):
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=ContactGroup.objects.all(),
         required=False,
         to_field_name='name',
@@ -74,6 +78,7 @@ class ContactRoleImportForm(NetBoxModelImportForm):
 
 class ContactImportForm(NetBoxModelImportForm):
     group = CSVModelChoiceField(
+        label=_('Group'),
         queryset=ContactGroup.objects.all(),
         required=False,
         to_field_name='name',

+ 3 - 2
netbox/tenancy/forms/filtersets.py

@@ -1,6 +1,6 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from extras.utils import FeatureQuery
 from netbox.forms import NetBoxModelFilterSetForm
@@ -84,7 +84,7 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm):
     model = ContactAssignment
     fieldsets = (
         (None, ('q', 'filter_id')),
-        ('Assignment', ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')),
+        (_('Assignment'), ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')),
     )
     content_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.all(),
@@ -108,6 +108,7 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm):
         label=_('Role')
     )
     priority = forms.MultipleChoiceField(
+        label=_('Priority'),
         choices=ContactPriorityChoices,
         required=False
     )

+ 3 - 1
netbox/tenancy/forms/forms.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from tenancy.models import *
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
@@ -13,6 +13,7 @@ __all__ = (
 
 class TenancyForm(forms.Form):
     tenant_group = DynamicModelChoiceField(
+        label=_('Tenant group'),
         queryset=TenantGroup.objects.all(),
         required=False,
         null_option='None',
@@ -21,6 +22,7 @@ class TenancyForm(forms.Form):
         }
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         query_params={

+ 13 - 5
netbox/tenancy/forms/model_forms.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext_lazy as _
 
 from netbox.forms import NetBoxModelForm
 from tenancy.models import *
@@ -21,13 +22,14 @@ __all__ = (
 
 class TenantGroupForm(NetBoxModelForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=TenantGroup.objects.all(),
         required=False
     )
     slug = SlugField()
 
     fieldsets = (
-        ('Tenant Group', (
+        (_('Tenant Group'), (
             'parent', 'name', 'slug', 'description', 'tags',
         )),
     )
@@ -42,13 +44,14 @@ class TenantGroupForm(NetBoxModelForm):
 class TenantForm(NetBoxModelForm):
     slug = SlugField()
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=TenantGroup.objects.all(),
         required=False
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Tenant', ('name', 'slug', 'group', 'description', 'tags')),
+        (_('Tenant'), ('name', 'slug', 'group', 'description', 'tags')),
     )
 
     class Meta:
@@ -64,13 +67,14 @@ class TenantForm(NetBoxModelForm):
 
 class ContactGroupForm(NetBoxModelForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=ContactGroup.objects.all(),
         required=False
     )
     slug = SlugField()
 
     fieldsets = (
-        ('Contact Group', (
+        (_('Contact Group'), (
             'parent', 'name', 'slug', 'description', 'tags',
         )),
     )
@@ -84,7 +88,7 @@ class ContactRoleForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Contact Role', (
+        (_('Contact Role'), (
             'name', 'slug', 'description', 'tags',
         )),
     )
@@ -96,13 +100,14 @@ class ContactRoleForm(NetBoxModelForm):
 
 class ContactForm(NetBoxModelForm):
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=ContactGroup.objects.all(),
         required=False
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Contact', ('group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags')),
+        (_('Contact'), ('group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'tags')),
     )
 
     class Meta:
@@ -117,6 +122,7 @@ class ContactForm(NetBoxModelForm):
 
 class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=ContactGroup.objects.all(),
         required=False,
         initial_params={
@@ -124,12 +130,14 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
         }
     )
     contact = DynamicModelChoiceField(
+        label=_('Contact'),
         queryset=Contact.objects.all(),
         query_params={
             'group_id': '$group'
         }
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=ContactRole.objects.all()
     )
 

+ 0 - 1
netbox/users/forms/model_forms.py

@@ -66,7 +66,6 @@ class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMe
     )
     # List of clearable preferences
     pk = forms.MultipleChoiceField(
-        label=_('Pk'),
         choices=[],
         required=False
     )

+ 4 - 2
netbox/utilities/forms/fields/array.py

@@ -1,5 +1,6 @@
 from django import forms
 from django.contrib.postgres.forms import SimpleArrayField
+from django.utils.translation import gettext_lazy as _
 
 from ..utils import parse_numeric_range
 
@@ -12,8 +13,9 @@ class NumericArrayField(SimpleArrayField):
 
     def clean(self, value):
         if value and not self.to_python(value):
-            raise forms.ValidationError(f'Invalid list ({value}). '
-                                        f'Must be numeric and ranges must be in ascending order')
+            raise forms.ValidationError(
+                _("Invalid list ({value}). Must be numeric and ranges must be in ascending order.").format(value=value)
+            )
         return super().clean(value)
 
     def to_python(self, value):

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

@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext_lazy as _
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
 from django.db.models import Q
@@ -40,7 +41,7 @@ class CSVMultipleChoiceField(CSVChoicesMixin, forms.MultipleChoiceField):
         if not value:
             return []
         if not isinstance(value, str):
-            raise forms.ValidationError(f"Invalid value for a multiple choice field: {value}")
+            raise forms.ValidationError(_("Invalid value for a multiple choice field: {value}").format(value=value))
         return value.split(',')
 
 
@@ -53,7 +54,7 @@ class CSVModelChoiceField(forms.ModelChoiceField):
     Extends Django's `ModelChoiceField` to provide additional validation for CSV values.
     """
     default_error_messages = {
-        'invalid_choice': 'Object not found: %(value)s',
+        'invalid_choice': _('Object not found: %(value)s'),
     }
 
     def to_python(self, value):
@@ -61,7 +62,7 @@ class CSVModelChoiceField(forms.ModelChoiceField):
             return super().to_python(value)
         except MultipleObjectsReturned:
             raise forms.ValidationError(
-                f'"{value}" is not a unique value for this field; multiple objects were found'
+                _('"{value}" is not a unique value for this field; multiple objects were found').format(value=value)
             )
 
 
@@ -70,7 +71,7 @@ class CSVModelMultipleChoiceField(forms.ModelMultipleChoiceField):
     Extends Django's `ModelMultipleChoiceField` to support comma-separated values.
     """
     default_error_messages = {
-        'invalid_choice': 'Object not found: %(value)s',
+        'invalid_choice': _('Object not found: %(value)s'),
     }
 
     def clean(self, value):
@@ -93,11 +94,11 @@ class CSVContentTypeField(CSVModelChoiceField):
         try:
             app_label, model = value.split('.')
         except ValueError:
-            raise forms.ValidationError(f'Object type must be specified as "<app>.<model>"')
+            raise forms.ValidationError(_('Object type must be specified as "<app>.<model>"'))
         try:
             return self.queryset.get(app_label=app_label, model=model)
         except ObjectDoesNotExist:
-            raise forms.ValidationError(f'Invalid object type')
+            raise forms.ValidationError(_('Invalid object type'))
 
 
 class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField):

+ 5 - 5
netbox/utilities/forms/fields/expandable.py

@@ -1,7 +1,7 @@
 import re
 
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from utilities.forms.constants import *
 from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipaddress_pattern
@@ -21,10 +21,10 @@ class ExpandableNameField(forms.CharField):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         if not self.help_text:
-            self.help_text = """
-                Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
-                are not supported (example: <code>[ge,xe]-0/0/[0-9]</code>).
-                """
+            self.help_text = _(
+                "Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are "
+                "not supported (example: <code>[ge,xe]-0/0/[0-9]</code>)."
+            )
 
     def to_python(self, value):
         if not value:

+ 13 - 12
netbox/utilities/forms/fields/fields.py

@@ -4,7 +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 django.utils.translation import gettext_lazy as _
 from netaddr import AddrFormatError, EUI
 
 from utilities.forms import widgets
@@ -26,14 +26,14 @@ class CommentField(forms.CharField):
     A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`.
     """
     widget = widgets.MarkdownWidget
-    help_text = f"""
-        <i class="mdi mdi-information-outline"></i>
-        <a href="{static('docs/reference/markdown/')}" target="_blank" tabindex="-1">
-        Markdown</a> syntax is supported
-    """
+    label = _('Comments')
+    help_text = _(
+        '<i class="mdi mdi-information-outline"></i> '
+        '<a href="{url}" target="_blank" tabindex="-1">Markdown</a> syntax is supported'
+    ).format(url=static('docs/reference/markdown/'))
 
-    def __init__(self, *, help_text=help_text, required=False, **kwargs):
-        super().__init__(help_text=help_text, required=required, **kwargs)
+    def __init__(self, *, label=label, help_text=help_text, required=False, **kwargs):
+        super().__init__(label=label, help_text=help_text, required=required, **kwargs)
 
 
 class SlugField(forms.SlugField):
@@ -44,10 +44,11 @@ class SlugField(forms.SlugField):
         slug_source: Name of the form field from which the slug value will be derived
     """
     widget = widgets.SlugWidget
+    label = _('Slug')
     help_text = _("URL-friendly unique shorthand")
 
-    def __init__(self, *, slug_source='name', help_text=help_text, **kwargs):
-        super().__init__(help_text=help_text, **kwargs)
+    def __init__(self, *, slug_source='name', label=label, help_text=help_text, **kwargs):
+        super().__init__(label=label, help_text=help_text, **kwargs)
 
         self.widget.attrs['slug-source'] = slug_source
 
@@ -77,7 +78,7 @@ class TagFilterField(forms.MultipleChoiceField):
             ]
 
         # Choices are fetched each time the form is initialized
-        super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs)
+        super().__init__(label=_('Tags'), choices=get_choices, required=False, *args, **kwargs)
 
 
 class LaxURLField(forms.URLField):
@@ -113,7 +114,7 @@ class MACAddressField(forms.Field):
     """
     widget = forms.CharField
     default_error_messages = {
-        'invalid': 'MAC address must be in EUI-48 format',
+        'invalid': _('MAC address must be in EUI-48 format'),
     }
 
     def to_python(self, value):

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

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from utilities.forms import BootstrapMixin, form_from_model
 from utilities.forms.fields import ExpandableNameField

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

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import InterfaceModeChoices
 from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
@@ -25,6 +25,7 @@ __all__ = (
 
 class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm):
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -38,6 +39,7 @@ class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm):
 
 class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm):
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -51,31 +53,38 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 class ClusterBulkEditForm(NetBoxModelBulkEditForm):
     type = DynamicModelChoiceField(
+        label=_('Type'),
         queryset=ClusterType.objects.all(),
         required=False
     )
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=ClusterGroup.objects.all(),
         required=False
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(ClusterStatusChoices),
         required=False,
         initial=''
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     region = DynamicModelChoiceField(
+        label=_('Region'),
         queryset=Region.objects.all(),
         required=False,
     )
     site_group = DynamicModelChoiceField(
+        label=_('Site group'),
         queryset=SiteGroup.objects.all(),
         required=False,
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         query_params={
@@ -84,17 +93,16 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
         }
     )
     description = forms.CharField(
+        label=_('Site'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label=_('Comments')
-    )
+    comments = CommentField()
 
     model = Cluster
     fieldsets = (
         (None, ('type', 'group', 'status', 'tenant', 'description')),
-        ('Site', ('region', 'site_group', 'site')),
+        (_('Site'), ('region', 'site_group', 'site')),
     )
     nullable_fields = (
         'group', 'site', 'tenant', 'description', 'comments',
@@ -103,15 +111,18 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
 
 class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(VirtualMachineStatusChoices),
         required=False,
         initial='',
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False
     )
     cluster = DynamicModelChoiceField(
+        label=_('Cluster'),
         queryset=Cluster.objects.all(),
         required=False,
         query_params={
@@ -119,6 +130,7 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
         }
     )
     device = DynamicModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         required=False,
         query_params={
@@ -126,6 +138,7 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
         }
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=DeviceRole.objects.filter(
             vm_role=True
         ),
@@ -135,10 +148,12 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
         }
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     platform = DynamicModelChoiceField(
+        label=_('Platform'),
         queryset=Platform.objects.all(),
         required=False
     )
@@ -155,17 +170,16 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Disk (GB)')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label=_('Comments')
-    )
+    comments = CommentField()
 
     model = VirtualMachine
     fieldsets = (
         (None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description')),
-        ('Resources', ('vcpus', 'memory', 'disk'))
+        (_('Resources'), ('vcpus', 'memory', 'disk'))
     )
     nullable_fields = (
         'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',
@@ -174,20 +188,24 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
 
 class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
     virtual_machine = forms.ModelChoiceField(
+        label=_('Virtual machine'),
         queryset=VirtualMachine.objects.all(),
         required=False,
         disabled=True,
         widget=forms.HiddenInput()
     )
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=VMInterface.objects.all(),
         required=False
     )
     bridge = DynamicModelChoiceField(
+        label=_('Bridge'),
         queryset=VMInterface.objects.all(),
         required=False
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=BulkEditNullBooleanSelect()
     )
@@ -198,10 +216,12 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
         label=_('MTU')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=100,
         required=False
     )
     mode = forms.ChoiceField(
+        label=_('Mode'),
         choices=add_blank_choice(InterfaceModeChoices),
         required=False
     )
@@ -235,8 +255,8 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
     model = VMInterface
     fieldsets = (
         (None, ('mtu', 'enabled', 'vrf', 'description')),
-        ('Related Interfaces', ('parent', 'bridge')),
-        ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
+        (_('Related Interfaces'), ('parent', 'bridge')),
+        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
     )
     nullable_fields = (
         'parent', 'bridge', 'mtu', 'vrf', 'description',

+ 18 - 1
netbox/virtualization/forms/bulk_import.py

@@ -1,4 +1,4 @@
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import InterfaceModeChoices
 from dcim.models import Device, DeviceRole, Platform, Site
@@ -36,27 +36,32 @@ class ClusterGroupImportForm(NetBoxModelImportForm):
 
 class ClusterImportForm(NetBoxModelImportForm):
     type = CSVModelChoiceField(
+        label=_('Type'),
         queryset=ClusterType.objects.all(),
         to_field_name='name',
         help_text=_('Type of cluster')
     )
     group = CSVModelChoiceField(
+        label=_('Group'),
         queryset=ClusterGroup.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned cluster group')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=ClusterStatusChoices,
         help_text=_('Operational status')
     )
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned site')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         to_field_name='name',
         required=False,
@@ -70,28 +75,33 @@ class ClusterImportForm(NetBoxModelImportForm):
 
 class VirtualMachineImportForm(NetBoxModelImportForm):
     status = CSVChoiceField(
+        label=_('Status'),
         choices=VirtualMachineStatusChoices,
         help_text=_('Operational status')
     )
     site = CSVModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned site')
     )
     cluster = CSVModelChoiceField(
+        label=_('Cluster'),
         queryset=Cluster.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned cluster')
     )
     device = CSVModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         to_field_name='name',
         required=False,
         help_text=_('Assigned device within cluster')
     )
     role = CSVModelChoiceField(
+        label=_('Role'),
         queryset=DeviceRole.objects.filter(
             vm_role=True
         ),
@@ -100,12 +110,14 @@ class VirtualMachineImportForm(NetBoxModelImportForm):
         help_text=_('Functional role')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned tenant')
     )
     platform = CSVModelChoiceField(
+        label=_('Platform'),
         queryset=Platform.objects.all(),
         required=False,
         to_field_name='name',
@@ -122,27 +134,32 @@ class VirtualMachineImportForm(NetBoxModelImportForm):
 
 class VMInterfaceImportForm(NetBoxModelImportForm):
     virtual_machine = CSVModelChoiceField(
+        label=_('Virtual machine'),
         queryset=VirtualMachine.objects.all(),
         to_field_name='name'
     )
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=VMInterface.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Parent interface')
     )
     bridge = CSVModelChoiceField(
+        label=_('Bridge'),
         queryset=VMInterface.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Bridged interface')
     )
     mode = CSVChoiceField(
+        label=_('Mode'),
         choices=InterfaceModeChoices,
         required=False,
         help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
     )
     vrf = CSVModelChoiceField(
+        label=_('VRF'),
         queryset=VRF.objects.all(),
         required=False,
         to_field_name='rd',

+ 18 - 15
netbox/virtualization/forms/filtersets.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
 from extras.forms import LocalConfigContextFilterForm
@@ -30,7 +30,7 @@ class ClusterGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
 
 
@@ -38,10 +38,10 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
     model = Cluster
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('group_id', 'type_id', 'status')),
-        ('Location', ('region_id', 'site_group_id', 'site_id')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
+        (_('Attributes'), ('group_id', 'type_id', 'status')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
     type_id = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
@@ -54,6 +54,7 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
         label=_('Region')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=ClusterStatusChoices,
         required=False
     )
@@ -90,11 +91,11 @@ class VirtualMachineFilterForm(
     model = VirtualMachine
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
-        ('Location', ('region_id', 'site_group_id', 'site_id')),
-        ('Attributes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Contacts', ('contact', 'contact_role', 'contact_group')),
+        (_('Cluster'), ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
+        (_('Location'), ('region_id', 'site_group_id', 'site_id')),
+        (_('Attributes'), ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
     cluster_group_id = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
@@ -148,6 +149,7 @@ class VirtualMachineFilterForm(
         label=_('Role')
     )
     status = forms.MultipleChoiceField(
+        label=_('Status'),
         choices=VirtualMachineStatusChoices,
         required=False
     )
@@ -175,8 +177,8 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
     model = VMInterface
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Virtual Machine', ('cluster_id', 'virtual_machine_id')),
-        ('Attributes', ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')),
+        (_('Virtual Machine'), ('cluster_id', 'virtual_machine_id')),
+        (_('Attributes'), ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')),
     )
     cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
@@ -192,6 +194,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
         label=_('Virtual machine')
     )
     enabled = forms.NullBooleanField(
+        label=_('Enabled'),
         required=False,
         widget=forms.Select(
             choices=BOOLEAN_WITH_BLANK_CHOICES
@@ -199,12 +202,12 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
     )
     mac_address = forms.CharField(
         required=False,
-        label='MAC address'
+        label=_('MAC address')
     )
     vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label='VRF'
+        label=_('VRF')
     )
     l2vpn_id = DynamicModelMultipleChoiceField(
         queryset=L2VPN.objects.all(),

+ 31 - 17
netbox/virtualization/forms/model_forms.py

@@ -1,7 +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 django.utils.translation import gettext_lazy as _
 
 from dcim.forms.common import InterfaceCommonForm
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
@@ -30,7 +30,7 @@ class ClusterTypeForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Cluster Type', (
+        (_('Cluster Type'), (
             'name', 'slug', 'description', 'tags',
         )),
     )
@@ -46,7 +46,7 @@ class ClusterGroupForm(NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Cluster Group', (
+        (_('Cluster Group'), (
             'name', 'slug', 'description', 'tags',
         )),
     )
@@ -60,13 +60,16 @@ class ClusterGroupForm(NetBoxModelForm):
 
 class ClusterForm(TenancyForm, NetBoxModelForm):
     type = DynamicModelChoiceField(
+        label=_('Type'),
         queryset=ClusterType.objects.all()
     )
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=ClusterGroup.objects.all(),
         required=False
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         selector=True
@@ -74,8 +77,8 @@ class ClusterForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Cluster', ('name', 'type', 'group', 'site', 'status', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
+        (_('Cluster'), ('name', 'type', 'group', 'site', 'status', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
     )
 
     class Meta:
@@ -87,16 +90,19 @@ class ClusterForm(TenancyForm, NetBoxModelForm):
 
 class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
     region = DynamicModelChoiceField(
+        label=_('Region'),
         queryset=Region.objects.all(),
         required=False,
         null_option='None'
     )
     site_group = DynamicModelChoiceField(
+        label=_('Site group'),
         queryset=SiteGroup.objects.all(),
         required=False,
         null_option='None'
     )
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False,
         query_params={
@@ -105,6 +111,7 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
         }
     )
     rack = DynamicModelChoiceField(
+        label=_('Rack'),
         queryset=Rack.objects.all(),
         required=False,
         null_option='None',
@@ -113,6 +120,7 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
         }
     )
     devices = DynamicModelMultipleChoiceField(
+        label=_('Devices'),
         queryset=Device.objects.all(),
         query_params={
             'site_id': '$site',
@@ -142,7 +150,7 @@ class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
             for device in self.cleaned_data.get('devices', []):
                 if device.site != self.cluster.site:
                     raise ValidationError({
-                        'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
+                        'devices': _("{} belongs to a different site ({}) than the cluster ({})").format(
                             device, device.site, self.cluster.site
                         )
                     })
@@ -157,10 +165,12 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
 
 class VirtualMachineForm(TenancyForm, NetBoxModelForm):
     site = DynamicModelChoiceField(
+        label=_('Site'),
         queryset=Site.objects.all(),
         required=False
     )
     cluster = DynamicModelChoiceField(
+        label=_('Cluster'),
         queryset=Cluster.objects.all(),
         required=False,
         selector=True,
@@ -169,6 +179,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
         }
     )
     device = DynamicModelChoiceField(
+        label=_('Device'),
         queryset=Device.objects.all(),
         required=False,
         query_params={
@@ -178,6 +189,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
         help_text=_("Optionally pin this VM to a specific host device within the cluster")
     )
     role = DynamicModelChoiceField(
+        label=_('Role'),
         queryset=DeviceRole.objects.all(),
         required=False,
         query_params={
@@ -185,6 +197,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
         }
     )
     platform = DynamicModelChoiceField(
+        label=_('Platform'),
         queryset=Platform.objects.all(),
         required=False
     )
@@ -195,12 +208,12 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Virtual Machine', ('name', 'role', 'status', 'description', 'tags')),
-        ('Site/Cluster', ('site', 'cluster', 'device')),
-        ('Tenancy', ('tenant_group', 'tenant')),
-        ('Management', ('platform', 'primary_ip4', 'primary_ip6')),
-        ('Resources', ('vcpus', 'memory', 'disk')),
-        ('Config Context', ('local_context_data',)),
+        (_('Virtual Machine'), ('name', 'role', 'status', 'description', 'tags')),
+        (_('Site/Cluster'), ('site', 'cluster', 'device')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
+        (_('Management'), ('platform', 'primary_ip4', 'primary_ip6')),
+        (_('Resources'), ('vcpus', 'memory', 'disk')),
+        (_('Config Context'), ('local_context_data',)),
     )
 
     class Meta:
@@ -253,6 +266,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
 
 class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     virtual_machine = DynamicModelChoiceField(
+        label=_('Virtual machine'),
         queryset=VirtualMachine.objects.all(),
         selector=True
     )
@@ -302,11 +316,11 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     )
 
     fieldsets = (
-        ('Interface', ('virtual_machine', 'name', 'description', 'tags')),
-        ('Addressing', ('vrf', 'mac_address')),
-        ('Operation', ('mtu', 'enabled')),
-        ('Related Interfaces', ('parent', 'bridge')),
-        ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
+        (_('Interface'), ('virtual_machine', 'name', 'description', 'tags')),
+        (_('Addressing'), ('vrf', 'mac_address')),
+        (_('Operation'), ('mtu', 'enabled')),
+        (_('Related Interfaces'), ('parent', 'bridge')),
+        (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
     )
 
     class Meta:

+ 4 - 1
netbox/virtualization/forms/object_create.py

@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_lazy as _
 from utilities.forms.fields import ExpandableNameField
 from .model_forms import VMInterfaceForm
 
@@ -7,7 +8,9 @@ __all__ = (
 
 
 class VMInterfaceCreateForm(VMInterfaceForm):
-    name = ExpandableNameField()
+    name = ExpandableNameField(
+        label=_('Name'),
+    )
     replication_fields = ('name',)
 
     class Meta(VMInterfaceForm.Meta):

+ 18 - 9
netbox/wireless/forms/bulk_edit.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import LinkStatusChoices
 from ipam.models import VLAN
@@ -20,10 +20,12 @@ __all__ = (
 
 class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=WirelessLANGroup.objects.all(),
         required=False
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
@@ -37,10 +39,12 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm):
 
 class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(WirelessLANStatusChoices),
         required=False
     )
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=WirelessLANGroup.objects.all(),
         required=False
     )
@@ -55,14 +59,17 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
         label=_('SSID')
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     auth_type = forms.ChoiceField(
+        label=_('Authentication type'),
         choices=add_blank_choice(WirelessAuthTypeChoices),
         required=False
     )
     auth_cipher = forms.ChoiceField(
+        label=_('Authentication cipher'),
         choices=add_blank_choice(WirelessAuthCipherChoices),
         required=False
     )
@@ -71,17 +78,16 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Pre-shared key')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = WirelessLAN
     fieldsets = (
         (None, ('group', 'ssid', 'status', 'vlan', 'tenant', 'description')),
-        ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
+        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
     )
     nullable_fields = (
         'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',
@@ -95,18 +101,22 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
         label=_('SSID')
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         choices=add_blank_choice(LinkStatusChoices),
         required=False
     )
     tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False
     )
     auth_type = forms.ChoiceField(
+        label=_('Authentication type'),
         choices=add_blank_choice(WirelessAuthTypeChoices),
         required=False
     )
     auth_cipher = forms.ChoiceField(
+        label=_('Authentication cipher'),
         choices=add_blank_choice(WirelessAuthCipherChoices),
         required=False
     )
@@ -115,17 +125,16 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
         label=_('Pre-shared key')
     )
     description = forms.CharField(
+        label=_('Description'),
         max_length=200,
         required=False
     )
-    comments = CommentField(
-        label='Comments'
-    )
+    comments = CommentField()
 
     model = WirelessLink
     fieldsets = (
         (None, ('ssid', 'status', 'tenant', 'description')),
-        ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk'))
+        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk'))
     )
     nullable_fields = (
         'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',

+ 14 - 1
netbox/wireless/forms/bulk_import.py

@@ -1,4 +1,4 @@
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import LinkStatusChoices
 from dcim.models import Interface
@@ -18,6 +18,7 @@ __all__ = (
 
 class WirelessLANGroupImportForm(NetBoxModelImportForm):
     parent = CSVModelChoiceField(
+        label=_('Parent'),
         queryset=WirelessLANGroup.objects.all(),
         required=False,
         to_field_name='name',
@@ -32,33 +33,39 @@ class WirelessLANGroupImportForm(NetBoxModelImportForm):
 
 class WirelessLANImportForm(NetBoxModelImportForm):
     group = CSVModelChoiceField(
+        label=_('Group'),
         queryset=WirelessLANGroup.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned group')
     )
     status = CSVChoiceField(
+        label=_('Status'),
         choices=WirelessLANStatusChoices,
         help_text='Operational status'
     )
     vlan = CSVModelChoiceField(
+        label=_('VLAN'),
         queryset=VLAN.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Bridged VLAN')
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned tenant')
     )
     auth_type = CSVChoiceField(
+        label=_('Authentication type'),
         choices=WirelessAuthTypeChoices,
         required=False,
         help_text=_('Authentication type')
     )
     auth_cipher = CSVChoiceField(
+        label=_('Authentication cipher'),
         choices=WirelessAuthCipherChoices,
         required=False,
         help_text=_('Authentication cipher')
@@ -74,27 +81,33 @@ class WirelessLANImportForm(NetBoxModelImportForm):
 
 class WirelessLinkImportForm(NetBoxModelImportForm):
     status = CSVChoiceField(
+        label=_('Status'),
         choices=LinkStatusChoices,
         help_text=_('Connection status')
     )
     interface_a = CSVModelChoiceField(
+        label=_('Interface A'),
         queryset=Interface.objects.all()
     )
     interface_b = CSVModelChoiceField(
+        label=_('Interface B'),
         queryset=Interface.objects.all()
     )
     tenant = CSVModelChoiceField(
+        label=_('Tenant'),
         queryset=Tenant.objects.all(),
         required=False,
         to_field_name='name',
         help_text=_('Assigned tenant')
     )
     auth_type = CSVChoiceField(
+        label=_('Authentication type'),
         choices=WirelessAuthTypeChoices,
         required=False,
         help_text=_('Authentication type')
     )
     auth_cipher = CSVChoiceField(
+        label=_('Authentication cipher'),
         choices=WirelessAuthCipherChoices,
         required=False,
         help_text=_('Authentication cipher')

+ 15 - 7
netbox/wireless/forms/filtersets.py

@@ -1,5 +1,5 @@
 from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import LinkStatusChoices
 from netbox.forms import NetBoxModelFilterSetForm
@@ -30,9 +30,9 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = WirelessLAN
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('ssid', 'group_id', 'status')),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
+        (_('Attributes'), ('ssid', 'group_id', 'status')),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
     )
     ssid = forms.CharField(
         required=False,
@@ -45,18 +45,22 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('Group')
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         required=False,
         choices=add_blank_choice(WirelessLANStatusChoices)
     )
     auth_type = forms.ChoiceField(
+        label=_('Authentication type'),
         required=False,
         choices=add_blank_choice(WirelessAuthTypeChoices)
     )
     auth_cipher = forms.ChoiceField(
+        label=_('Authentication cipher'),
         required=False,
         choices=add_blank_choice(WirelessAuthCipherChoices)
     )
     auth_psk = forms.CharField(
+        label=_('Pre-shared key'),
         required=False
     )
     tag = TagFilterField(model)
@@ -66,27 +70,31 @@ class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = WirelessLink
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
-        ('Attributes', ('ssid', 'status',)),
-        ('Tenant', ('tenant_group_id', 'tenant_id')),
-        ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
+        (_('Attributes'), ('ssid', 'status',)),
+        (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
     )
     ssid = forms.CharField(
         required=False,
         label=_('SSID')
     )
     status = forms.ChoiceField(
+        label=_('Status'),
         required=False,
         choices=add_blank_choice(LinkStatusChoices)
     )
     auth_type = forms.ChoiceField(
+        label=_('Authentication type'),
         required=False,
         choices=add_blank_choice(WirelessAuthTypeChoices)
     )
     auth_cipher = forms.ChoiceField(
+        label=_('Authentication cipher'),
         required=False,
         choices=add_blank_choice(WirelessAuthCipherChoices)
     )
     auth_psk = forms.CharField(
+        label=_('Pre-shared key'),
         required=False
     )
     tag = TagFilterField(model)

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

@@ -1,5 +1,5 @@
 from django.forms import PasswordInput
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
 
 from dcim.models import Device, Interface, Location, Site
 from ipam.models import VLAN
@@ -17,13 +17,14 @@ __all__ = (
 
 class WirelessLANGroupForm(NetBoxModelForm):
     parent = DynamicModelChoiceField(
+        label=_('Parent'),
         queryset=WirelessLANGroup.objects.all(),
         required=False
     )
     slug = SlugField()
 
     fieldsets = (
-        ('Wireless LAN Group', (
+        (_('Wireless LAN Group'), (
             'parent', 'name', 'slug', 'description', 'tags',
         )),
     )
@@ -37,6 +38,7 @@ class WirelessLANGroupForm(NetBoxModelForm):
 
 class WirelessLANForm(TenancyForm, NetBoxModelForm):
     group = DynamicModelChoiceField(
+        label=_('Group'),
         queryset=WirelessLANGroup.objects.all(),
         required=False
     )
@@ -49,9 +51,9 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Wireless LAN', ('ssid', 'group', 'vlan', 'status', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
-        ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
+        (_('Wireless LAN'), ('ssid', 'group', 'vlan', 'status', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
+        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
     )
 
     class Meta:
@@ -152,11 +154,11 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Side A', ('site_a', 'location_a', 'device_a', 'interface_a')),
-        ('Side B', ('site_b', 'location_b', 'device_b', 'interface_b')),
-        ('Link', ('status', 'ssid', 'description', 'tags')),
-        ('Tenancy', ('tenant_group', 'tenant')),
-        ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
+        (_('Side A'), ('site_a', 'location_a', 'device_a', 'interface_a')),
+        (_('Side B'), ('site_b', 'location_b', 'device_b', 'interface_b')),
+        (_('Link'), ('status', 'ssid', 'description', 'tags')),
+        (_('Tenancy'), ('tenant_group', 'tenant')),
+        (_('Authentication'), ('auth_type', 'auth_cipher', 'auth_psk')),
     )
 
     class Meta:

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