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

Closes #13352: Translation support for model verbose names (#13354)

* Update verbose_name & verbose_name_plural Meta attributes on all models

* Alter makemigrations to ignore verbose_name & verbose_name_plural changes
Jeremy Stretch 2 лет назад
Родитель
Сommit
caedc8dbe3
36 измененных файлов с 288 добавлено и 28 удалено
  1. 9 0
      netbox/circuits/models/circuits.py
  2. 6 0
      netbox/circuits/models/providers.py
  3. 6 0
      netbox/core/management/commands/makemigrations.py
  4. 6 0
      netbox/core/models/data.py
  5. 2 0
      netbox/core/models/files.py
  6. 2 0
      netbox/core/models/jobs.py
  7. 8 0
      netbox/dcim/models/cables.py
  8. 36 0
      netbox/dcim/models/device_component_templates.py
  9. 39 0
      netbox/dcim/models/device_components.py
  10. 27 1
      netbox/dcim/models/devices.py
  11. 4 0
      netbox/dcim/models/power.py
  12. 9 0
      netbox/dcim/models/racks.py
  13. 8 0
      netbox/dcim/models/sites.py
  14. 2 0
      netbox/extras/models/change_logging.py
  15. 4 0
      netbox/extras/models/configs.py
  16. 4 0
      netbox/extras/models/customfields.py
  17. 2 1
      netbox/extras/models/dashboard.py
  18. 16 1
      netbox/extras/models/models.py
  19. 3 0
      netbox/extras/models/reports.py
  20. 3 0
      netbox/extras/models/scripts.py
  21. 2 0
      netbox/extras/models/search.py
  22. 4 0
      netbox/extras/models/staging.py
  23. 4 0
      netbox/extras/models/tags.py
  24. 4 4
      netbox/ipam/models/asns.py
  25. 3 1
      netbox/ipam/models/fhrp.py
  26. 10 5
      netbox/ipam/models/ip.py
  27. 4 2
      netbox/ipam/models/l2vpn.py
  28. 4 0
      netbox/ipam/models/services.py
  29. 4 4
      netbox/ipam/models/vlans.py
  30. 4 2
      netbox/ipam/models/vrfs.py
  31. 11 0
      netbox/tenancy/models/contacts.py
  32. 4 0
      netbox/tenancy/models/tenants.py
  33. 12 4
      netbox/users/models.py
  34. 12 0
      netbox/virtualization/models/clusters.py
  35. 4 1
      netbox/virtualization/models/virtualmachines.py
  36. 6 2
      netbox/wireless/models.py

+ 9 - 0
netbox/circuits/models/circuits.py

@@ -25,6 +25,11 @@ class CircuitType(OrganizationalModel):
     def get_absolute_url(self):
         return reverse('circuits:circuittype', args=[self.pk])
 
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('circuit type')
+        verbose_name_plural = _('circuit types')
+
 
 class Circuit(PrimaryModel):
     """
@@ -131,6 +136,8 @@ class Circuit(PrimaryModel):
                 name='%(app_label)s_%(class)s_unique_provideraccount_cid'
             ),
         )
+        verbose_name = _('circuit')
+        verbose_name_plural = _('circuits')
 
     def __str__(self):
         return self.cid
@@ -217,6 +224,8 @@ class CircuitTermination(
                 name='%(app_label)s_%(class)s_unique_circuit_term_side'
             ),
         )
+        verbose_name = _('circuit termination')
+        verbose_name_plural = _('circuit terminations')
 
     def __str__(self):
         return f'{self.circuit}: Termination {self.term_side}'

+ 6 - 0
netbox/circuits/models/providers.py

@@ -44,6 +44,8 @@ class Provider(PrimaryModel):
 
     class Meta:
         ordering = ['name']
+        verbose_name = _('provider')
+        verbose_name_plural = _('providers')
 
     def __str__(self):
         return self.name
@@ -91,6 +93,8 @@ class ProviderAccount(PrimaryModel):
                 condition=~Q(name="")
             ),
         )
+        verbose_name = _('provider account')
+        verbose_name_plural = _('provider accounts')
 
     def __str__(self):
         if self.name:
@@ -129,6 +133,8 @@ class ProviderNetwork(PrimaryModel):
                 name='%(app_label)s_%(class)s_unique_provider_name'
             ),
         )
+        verbose_name = _('provider network')
+        verbose_name_plural = _('provider networks')
 
     def __str__(self):
         return self.name

+ 6 - 0
netbox/core/management/commands/makemigrations.py

@@ -3,9 +3,15 @@ from django.conf import settings
 from django.core.management.base import CommandError
 from django.core.management.commands.makemigrations import Command as _Command
 from django.db import models
+from django.db.migrations.operations import AlterModelOptions
 
 from utilities.migration import custom_deconstruct
 
+# Monkey patch AlterModelOptions to ignore verbose name attributes
+AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name')
+AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name_plural')
+
+# Set our custom deconstructor for fields
 models.Field.deconstruct = custom_deconstruct
 
 

+ 6 - 0
netbox/core/models/data.py

@@ -83,6 +83,8 @@ class DataSource(JobsMixin, PrimaryModel):
 
     class Meta:
         ordering = ('name',)
+        verbose_name = _('data source')
+        verbose_name_plural = _('data sources')
 
     def __str__(self):
         return f'{self.name}'
@@ -300,6 +302,8 @@ class DataFile(models.Model):
         indexes = [
             models.Index(fields=('source', 'path'), name='core_datafile_source_path'),
         ]
+        verbose_name = _('data file')
+        verbose_name_plural = _('data files')
 
     def __str__(self):
         return self.path
@@ -383,3 +387,5 @@ class AutoSyncRecord(models.Model):
         indexes = (
             models.Index(fields=('object_type', 'object_id')),
         )
+        verbose_name = _('auto sync record')
+        verbose_name_plural = _('auto sync records')

+ 2 - 0
netbox/core/models/files.py

@@ -56,6 +56,8 @@ class ManagedFile(SyncedDataMixin, models.Model):
         indexes = [
             models.Index(fields=('file_root', 'file_path'), name='core_managedfile_root_path'),
         ]
+        verbose_name = _('managed file')
+        verbose_name_plural = _('managed files')
 
     def __str__(self):
         return self.name

+ 2 - 0
netbox/core/models/jobs.py

@@ -101,6 +101,8 @@ class Job(models.Model):
 
     class Meta:
         ordering = ['-created']
+        verbose_name = _('job')
+        verbose_name_plural = _('jobs')
 
     def __str__(self):
         return str(self.job_id)

+ 8 - 0
netbox/dcim/models/cables.py

@@ -91,6 +91,8 @@ class Cable(PrimaryModel):
 
     class Meta:
         ordering = ('pk',)
+        verbose_name = _('cable')
+        verbose_name_plural = _('cables')
 
     def __init__(self, *args, a_terminations=None, b_terminations=None, **kwargs):
         super().__init__(*args, **kwargs)
@@ -292,6 +294,8 @@ class CableTermination(ChangeLoggedModel):
                 name='%(app_label)s_%(class)s_unique_termination'
             ),
         )
+        verbose_name = _('cable termination')
+        verbose_name_plural = _('cable terminations')
 
     def __str__(self):
         return f'Cable {self.cable} to {self.termination}'
@@ -427,6 +431,10 @@ class CablePath(models.Model):
     )
     _nodes = PathField()
 
+    class Meta:
+        verbose_name = _('cable path')
+        verbose_name_plural = _('cable paths')
+
     def __str__(self):
         return f"Path #{self.pk}: {len(self.path)} hops"
 

+ 36 - 0
netbox/dcim/models/device_component_templates.py

@@ -183,6 +183,10 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
 
     component_model = ConsolePort
 
+    class Meta(ModularComponentTemplateModel.Meta):
+        verbose_name = _('console port template')
+        verbose_name_plural = _('console port templates')
+
     def instantiate(self, **kwargs):
         return self.component_model(
             name=self.resolve_name(kwargs.get('module')),
@@ -213,6 +217,10 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
 
     component_model = ConsoleServerPort
 
+    class Meta(ModularComponentTemplateModel.Meta):
+        verbose_name = _('console server port template')
+        verbose_name_plural = _('console server port templates')
+
     def instantiate(self, **kwargs):
         return self.component_model(
             name=self.resolve_name(kwargs.get('module')),
@@ -258,6 +266,10 @@ class PowerPortTemplate(ModularComponentTemplateModel):
 
     component_model = PowerPort
 
+    class Meta(ModularComponentTemplateModel.Meta):
+        verbose_name = _('power port template')
+        verbose_name_plural = _('power port templates')
+
     def instantiate(self, **kwargs):
         return self.component_model(
             name=self.resolve_name(kwargs.get('module')),
@@ -316,6 +328,10 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
 
     component_model = PowerOutlet
 
+    class Meta(ModularComponentTemplateModel.Meta):
+        verbose_name = _('power outlet template')
+        verbose_name_plural = _('power outlet templates')
+
     def clean(self):
         super().clean()
 
@@ -410,6 +426,10 @@ class InterfaceTemplate(ModularComponentTemplateModel):
 
     component_model = Interface
 
+    class Meta(ModularComponentTemplateModel.Meta):
+        verbose_name = _('interface template')
+        verbose_name_plural = _('interface templates')
+
     def clean(self):
         super().clean()
 
@@ -503,6 +523,8 @@ class FrontPortTemplate(ModularComponentTemplateModel):
                 name='%(app_label)s_%(class)s_unique_rear_port_position'
             ),
         )
+        verbose_name = _('front port template')
+        verbose_name_plural = _('front port templates')
 
     def clean(self):
         super().clean()
@@ -579,6 +601,10 @@ class RearPortTemplate(ModularComponentTemplateModel):
 
     component_model = RearPort
 
+    class Meta(ModularComponentTemplateModel.Meta):
+        verbose_name = _('rear port template')
+        verbose_name_plural = _('rear port templates')
+
     def instantiate(self, **kwargs):
         return self.component_model(
             name=self.resolve_name(kwargs.get('module')),
@@ -614,6 +640,10 @@ class ModuleBayTemplate(ComponentTemplateModel):
 
     component_model = ModuleBay
 
+    class Meta(ComponentTemplateModel.Meta):
+        verbose_name = _('module bay template')
+        verbose_name_plural = _('module bay templates')
+
     def instantiate(self, device):
         return self.component_model(
             device=device,
@@ -638,6 +668,10 @@ class DeviceBayTemplate(ComponentTemplateModel):
     """
     component_model = DeviceBay
 
+    class Meta(ComponentTemplateModel.Meta):
+        verbose_name = _('device bay template')
+        verbose_name_plural = _('device bay templates')
+
     def instantiate(self, device):
         return self.component_model(
             device=device,
@@ -720,6 +754,8 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
                 name='%(app_label)s_%(class)s_unique_device_type_parent_name'
             ),
         )
+        verbose_name = _('inventory item template')
+        verbose_name_plural = _('inventory item templates')
 
     def instantiate(self, **kwargs):
         parent = InventoryItem.objects.get(name=self.parent.name, **kwargs) if self.parent else None

+ 39 - 0
netbox/dcim/models/device_components.py

@@ -298,6 +298,10 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
 
     clone_fields = ('device', 'module', 'type', 'speed')
 
+    class Meta(ModularComponentModel.Meta):
+        verbose_name = _('console port')
+        verbose_name_plural = _('console ports')
+
     def get_absolute_url(self):
         return reverse('dcim:consoleport', kwargs={'pk': self.pk})
 
@@ -323,6 +327,10 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint,
 
     clone_fields = ('device', 'module', 'type', 'speed')
 
+    class Meta(ModularComponentModel.Meta):
+        verbose_name = _('console server port')
+        verbose_name_plural = _('console server ports')
+
     def get_absolute_url(self):
         return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
 
@@ -359,6 +367,10 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
 
     clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
 
+    class Meta(ModularComponentModel.Meta):
+        verbose_name = _('power port')
+        verbose_name_plural = _('power ports')
+
     def get_absolute_url(self):
         return reverse('dcim:powerport', kwargs={'pk': self.pk})
 
@@ -473,6 +485,10 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
 
     clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
 
+    class Meta(ModularComponentModel.Meta):
+        verbose_name = _('power outlet')
+        verbose_name_plural = _('power outlets')
+
     def get_absolute_url(self):
         return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
 
@@ -718,6 +734,8 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
 
     class Meta(ModularComponentModel.Meta):
         ordering = ('device', CollateAsChar('_name'))
+        verbose_name = _('interface')
+        verbose_name_plural = _('interfaces')
 
     def get_absolute_url(self):
         return reverse('dcim:interface', kwargs={'pk': self.pk})
@@ -977,6 +995,8 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
                 name='%(app_label)s_%(class)s_unique_rear_port_position'
             ),
         )
+        verbose_name = _('front port')
+        verbose_name_plural = _('front ports')
 
     def get_absolute_url(self):
         return reverse('dcim:frontport', kwargs={'pk': self.pk})
@@ -1032,6 +1052,10 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
     )
     clone_fields = ('device', 'type', 'color', 'positions')
 
+    class Meta(ModularComponentModel.Meta):
+        verbose_name = _('rear port')
+        verbose_name_plural = _('rear ports')
+
     def get_absolute_url(self):
         return reverse('dcim:rearport', kwargs={'pk': self.pk})
 
@@ -1066,6 +1090,10 @@ class ModuleBay(ComponentModel, TrackingModelMixin):
 
     clone_fields = ('device',)
 
+    class Meta(ComponentModel.Meta):
+        verbose_name = _('module bay')
+        verbose_name_plural = _('module bays')
+
     def get_absolute_url(self):
         return reverse('dcim:modulebay', kwargs={'pk': self.pk})
 
@@ -1084,6 +1112,10 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
 
     clone_fields = ('device',)
 
+    class Meta(ComponentModel.Meta):
+        verbose_name = _('device bay')
+        verbose_name_plural = _('device bays')
+
     def get_absolute_url(self):
         return reverse('dcim:devicebay', kwargs={'pk': self.pk})
 
@@ -1125,6 +1157,11 @@ class InventoryItemRole(OrganizationalModel):
         default=ColorChoices.COLOR_GREY
     )
 
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('inventory item role')
+        verbose_name_plural = _('inventory item roles')
+
     def get_absolute_url(self):
         return reverse('dcim:inventoryitemrole', args=[self.pk])
 
@@ -1209,6 +1246,8 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
                 name='%(app_label)s_%(class)s_unique_device_parent_name'
             ),
         )
+        verbose_name = _('inventory item')
+        verbose_name_plural = _('inventory items')
 
     def get_absolute_url(self):
         return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})

+ 27 - 1
netbox/dcim/models/devices.py

@@ -53,6 +53,11 @@ class Manufacturer(OrganizationalModel):
         to='tenancy.ContactAssignment'
     )
 
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('manufacturer')
+        verbose_name_plural = _('manufacturers')
+
     def get_absolute_url(self):
         return reverse('dcim:manufacturer', args=[self.pk])
 
@@ -199,6 +204,8 @@ class DeviceType(PrimaryModel, WeightMixin):
                 name='%(app_label)s_%(class)s_unique_manufacturer_slug'
             ),
         )
+        verbose_name = _('device type')
+        verbose_name_plural = _('device types')
 
     def __str__(self):
         return self.model
@@ -400,6 +407,8 @@ class ModuleType(PrimaryModel, WeightMixin):
                 name='%(app_label)s_%(class)s_unique_manufacturer_model'
             ),
         )
+        verbose_name = _('module type')
+        verbose_name_plural = _('module types')
 
     def __str__(self):
         return self.model
@@ -477,6 +486,11 @@ class DeviceRole(OrganizationalModel):
         null=True
     )
 
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('device role')
+        verbose_name_plural = _('device roles')
+
     def get_absolute_url(self):
         return reverse('dcim:devicerole', args=[self.pk])
 
@@ -502,6 +516,11 @@ class Platform(OrganizationalModel):
         null=True
     )
 
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('platform')
+        verbose_name_plural = _('platforms')
+
     def get_absolute_url(self):
         return reverse('dcim:platform', args=[self.pk])
 
@@ -789,6 +808,8 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
                 name='%(app_label)s_%(class)s_unique_virtual_chassis_vc_position'
             ),
         )
+        verbose_name = _('device')
+        verbose_name_plural = _('devices')
 
     def __str__(self):
         if self.name and self.asset_tag:
@@ -1182,6 +1203,8 @@ class Module(PrimaryModel, ConfigContextModel):
 
     class Meta:
         ordering = ('module_bay',)
+        verbose_name = _('module')
+        verbose_name_plural = _('modules')
 
     def __str__(self):
         return f'{self.module_bay.name}: {self.module_type} ({self.pk})'
@@ -1314,7 +1337,8 @@ class VirtualChassis(PrimaryModel):
 
     class Meta:
         ordering = ['name']
-        verbose_name_plural = 'virtual chassis'
+        verbose_name = _('virtual chassis')
+        verbose_name_plural = _('virtual chassis')
 
     def __str__(self):
         return self.name
@@ -1415,6 +1439,8 @@ class VirtualDeviceContext(PrimaryModel):
                 name='%(app_label)s_%(class)s_device_name'
             ),
         )
+        verbose_name = _('virtual device context')
+        verbose_name_plural = _('virtual device contexts')
 
     def __str__(self):
         return self.name

+ 4 - 0
netbox/dcim/models/power.py

@@ -60,6 +60,8 @@ class PowerPanel(PrimaryModel):
                 name='%(app_label)s_%(class)s_unique_site_name'
             ),
         )
+        verbose_name = _('power panel')
+        verbose_name_plural = _('power panels')
 
     def __str__(self):
         return self.name
@@ -166,6 +168,8 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
                 name='%(app_label)s_%(class)s_unique_power_panel_name'
             ),
         )
+        verbose_name = _('power feed')
+        verbose_name_plural = _('power feeds')
 
     def __str__(self):
         return self.name

+ 9 - 0
netbox/dcim/models/racks.py

@@ -43,6 +43,11 @@ class RackRole(OrganizationalModel):
         default=ColorChoices.COLOR_GREY
     )
 
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('rack role')
+        verbose_name_plural = _('rack roles')
+
     def get_absolute_url(self):
         return reverse('dcim:rackrole', args=[self.pk])
 
@@ -216,6 +221,8 @@ class Rack(PrimaryModel, WeightMixin):
                 name='%(app_label)s_%(class)s_unique_location_facility_id'
             ),
         )
+        verbose_name = _('rack')
+        verbose_name_plural = _('racks')
 
     def __str__(self):
         if self.facility_id:
@@ -538,6 +545,8 @@ class RackReservation(PrimaryModel):
 
     class Meta:
         ordering = ['created', 'pk']
+        verbose_name = _('rack reservation')
+        verbose_name_plural = _('rack reservations')
 
     def __str__(self):
         return "Reservation for rack {}".format(self.rack)

+ 8 - 0
netbox/dcim/models/sites.py

@@ -62,6 +62,8 @@ class Region(NestedGroupModel):
                 violation_error_message=_("A top-level region with this slug already exists.")
             ),
         )
+        verbose_name = _('region')
+        verbose_name_plural = _('regions')
 
     def get_absolute_url(self):
         return reverse('dcim:region', args=[self.pk])
@@ -117,6 +119,8 @@ class SiteGroup(NestedGroupModel):
                 violation_error_message=_("A top-level site group with this slug already exists.")
             ),
         )
+        verbose_name = _('site group')
+        verbose_name_plural = _('site groups')
 
     def get_absolute_url(self):
         return reverse('dcim:sitegroup', args=[self.pk])
@@ -244,6 +248,8 @@ class Site(PrimaryModel):
 
     class Meta:
         ordering = ('_name',)
+        verbose_name = _('site')
+        verbose_name_plural = _('sites')
 
     def __str__(self):
         return self.name
@@ -326,6 +332,8 @@ class Location(NestedGroupModel):
                 violation_error_message=_("A location with this slug already exists within the specified site.")
             ),
         )
+        verbose_name = _('location')
+        verbose_name_plural = _('locations')
 
     def get_absolute_url(self):
         return reverse('dcim:location', args=[self.pk])

+ 2 - 0
netbox/extras/models/change_logging.py

@@ -93,6 +93,8 @@ class ObjectChange(models.Model):
 
     class Meta:
         ordering = ['-time']
+        verbose_name = _('object change')
+        verbose_name_plural = _('object changes')
 
     def __str__(self):
         return '{} {} {} by {}'.format(

+ 4 - 0
netbox/extras/models/configs.py

@@ -125,6 +125,8 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
 
     class Meta:
         ordering = ['weight', 'name']
+        verbose_name = _('config context')
+        verbose_name_plural = _('config contexts')
 
     def __str__(self):
         return self.name
@@ -233,6 +235,8 @@ class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLog
 
     class Meta:
         ordering = ('name',)
+        verbose_name = _('config template')
+        verbose_name_plural = _('config templates')
 
     def __str__(self):
         return self.name

+ 4 - 0
netbox/extras/models/customfields.py

@@ -202,6 +202,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
 
     class Meta:
         ordering = ['group_name', 'weight', 'name']
+        verbose_name = _('custom field')
+        verbose_name_plural = _('custom fields')
 
     def __str__(self):
         return self.label or self.name.replace('_', ' ').capitalize()
@@ -710,6 +712,8 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
 
     class Meta:
         ordering = ('name',)
+        verbose_name = _('custom field choice set')
+        verbose_name_plural = _('custom field choice sets')
 
     def __str__(self):
         return self.name

+ 2 - 1
netbox/extras/models/dashboard.py

@@ -25,7 +25,8 @@ class Dashboard(models.Model):
     )
 
     class Meta:
-        pass
+        verbose_name = _('dashboard')
+        verbose_name_plural = _('dashboards')
 
     def get_widget(self, id):
         """

+ 16 - 1
netbox/extras/models/models.py

@@ -165,6 +165,8 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
                 name='%(app_label)s_%(class)s_unique_payload_url_types'
             ),
         )
+        verbose_name = _('webhook')
+        verbose_name_plural = _('webhooks')
 
     def __str__(self):
         return self.name
@@ -284,6 +286,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
 
     class Meta:
         ordering = ['group_name', 'weight', 'name']
+        verbose_name = _('custom link')
+        verbose_name_plural = _('custom links')
 
     def __str__(self):
         return self.name
@@ -371,6 +375,8 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
 
     class Meta:
         ordering = ('name',)
+        verbose_name = _('export template')
+        verbose_name_plural = _('export templates')
 
     def __str__(self):
         return self.name
@@ -482,6 +488,8 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
 
     class Meta:
         ordering = ('weight', 'name')
+        verbose_name = _('saved filter')
+        verbose_name_plural = _('saved filters')
 
     def __str__(self):
         return self.name
@@ -544,6 +552,8 @@ class ImageAttachment(ChangeLoggedModel):
 
     class Meta:
         ordering = ('name', 'pk')  # name may be non-unique
+        verbose_name = _('image attachment')
+        verbose_name_plural = _('image attachments')
 
     def __str__(self):
         if self.name:
@@ -622,7 +632,8 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
 
     class Meta:
         ordering = ('-created',)
-        verbose_name_plural = 'journal entries'
+        verbose_name = _('journal entry')
+        verbose_name_plural = _('journal entries')
 
     def __str__(self):
         created = timezone.localtime(self.created)
@@ -677,6 +688,8 @@ class Bookmark(models.Model):
                 name='%(app_label)s_%(class)s_unique_per_object_and_user'
             ),
         )
+        verbose_name = _('bookmark')
+        verbose_name_plural = _('bookmarks')
 
     def __str__(self):
         if self.object:
@@ -707,6 +720,8 @@ class ConfigRevision(models.Model):
 
     class Meta:
         ordering = ['-created']
+        verbose_name = _('config revision')
+        verbose_name_plural = _('config revisions')
 
     def __str__(self):
         return f'Config revision #{self.pk} ({self.created})'

+ 3 - 0
netbox/extras/models/reports.py

@@ -4,6 +4,7 @@ from functools import cached_property
 
 from django.db import models
 from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
 
 from core.choices import ManagedFileRootPathChoices
 from core.models import ManagedFile
@@ -42,6 +43,8 @@ class ReportModule(PythonModuleMixin, JobsMixin, ManagedFile):
 
     class Meta:
         proxy = True
+        verbose_name = _('report module')
+        verbose_name_plural = _('report modules')
 
     def get_absolute_url(self):
         return reverse('extras:report_list')

+ 3 - 0
netbox/extras/models/scripts.py

@@ -4,6 +4,7 @@ from functools import cached_property
 
 from django.db import models
 from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
 
 from core.choices import ManagedFileRootPathChoices
 from core.models import ManagedFile
@@ -42,6 +43,8 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
 
     class Meta:
         proxy = True
+        verbose_name = _('script module')
+        verbose_name_plural = _('script modules')
 
     def get_absolute_url(self):
         return reverse('extras:script_list')

+ 2 - 0
netbox/extras/models/search.py

@@ -51,6 +51,8 @@ class CachedValue(models.Model):
 
     class Meta:
         ordering = ('weight', 'object_type', 'object_id')
+        verbose_name = _('cached value')
+        verbose_name_plural = _('cached values')
 
     def __str__(self):
         return f'{self.object_type} {self.object_id}: {self.field}={self.value}'

+ 4 - 0
netbox/extras/models/staging.py

@@ -41,6 +41,8 @@ class Branch(ChangeLoggedModel):
 
     class Meta:
         ordering = ('name',)
+        verbose_name = _('branch')
+        verbose_name_plural = _('branches')
 
     def __str__(self):
         return f'{self.name} ({self.pk})'
@@ -89,6 +91,8 @@ class StagedChange(ChangeLoggedModel):
 
     class Meta:
         ordering = ('pk',)
+        verbose_name = _('staged change')
+        verbose_name_plural = _('staged changes')
 
     def __str__(self):
         action = self.get_action_display()

+ 4 - 0
netbox/extras/models/tags.py

@@ -50,6 +50,8 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
 
     class Meta:
         ordering = ['name']
+        verbose_name = _('tag')
+        verbose_name_plural = _('tags')
 
     def get_absolute_url(self):
         return reverse('extras:tag', args=[self.pk])
@@ -75,3 +77,5 @@ class TaggedItem(GenericTaggedItemBase):
 
     class Meta:
         indexes = [models.Index(fields=["content_type", "object_id"])]
+        verbose_name = _('tagged item')
+        verbose_name_plural = _('tagged items')

+ 4 - 4
netbox/ipam/models/asns.py

@@ -48,8 +48,8 @@ class ASNRange(OrganizationalModel):
 
     class Meta:
         ordering = ('name',)
-        verbose_name = 'ASN range'
-        verbose_name_plural = 'ASN ranges'
+        verbose_name = _('ASN range')
+        verbose_name_plural = _('ASN ranges')
 
     def __str__(self):
         return f'{self.name} ({self.range_as_string()})'
@@ -122,8 +122,8 @@ class ASN(PrimaryModel):
 
     class Meta:
         ordering = ['asn']
-        verbose_name = 'ASN'
-        verbose_name_plural = 'ASNs'
+        verbose_name = _('ASN')
+        verbose_name_plural = _('ASNs')
 
     def __str__(self):
         return f'AS{self.asn_with_asdot}'

+ 3 - 1
netbox/ipam/models/fhrp.py

@@ -54,7 +54,8 @@ class FHRPGroup(PrimaryModel):
 
     class Meta:
         ordering = ['protocol', 'group_id', 'pk']
-        verbose_name = 'FHRP group'
+        verbose_name = _('FHRP group')
+        verbose_name_plural = _('FHRP groups')
 
     def __str__(self):
         name = ''
@@ -108,6 +109,7 @@ class FHRPGroupAssignment(ChangeLoggedModel):
             ),
         )
         verbose_name = _('FHRP group assignment')
+        verbose_name_plural = _('FHRP group assignments')
 
     def __str__(self):
         return f'{self.interface}: {self.group} ({self.priority})'

+ 10 - 5
netbox/ipam/models/ip.py

@@ -111,6 +111,8 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
 
     class Meta:
         ordering = ('prefix', 'pk')  # prefix may be non-unique
+        verbose_name = _('aggregate')
+        verbose_name_plural = _('aggregates')
 
     def __str__(self):
         return str(self.prefix)
@@ -188,6 +190,8 @@ class Role(OrganizationalModel):
 
     class Meta:
         ordering = ('weight', 'name')
+        verbose_name = _('role')
+        verbose_name_plural = _('roles')
 
     def __str__(self):
         return self.name
@@ -279,7 +283,8 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
 
     class Meta:
         ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk')  # (vrf, prefix) may be non-unique
-        verbose_name_plural = 'prefixes'
+        verbose_name = _('prefix')
+        verbose_name_plural = _('prefixes')
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
@@ -532,8 +537,8 @@ class IPRange(PrimaryModel):
 
     class Meta:
         ordering = (F('vrf').asc(nulls_first=True), 'start_address', 'pk')  # (vrf, start_address) may be non-unique
-        verbose_name = 'IP range'
-        verbose_name_plural = 'IP ranges'
+        verbose_name = _('IP range')
+        verbose_name_plural = _('IP ranges')
 
     def __str__(self):
         return self.name
@@ -783,8 +788,8 @@ class IPAddress(PrimaryModel):
         indexes = [
             models.Index(Cast(Host('address'), output_field=IPAddressField()), name='ipam_ipaddress_host'),
         ]
-        verbose_name = 'IP address'
-        verbose_name_plural = 'IP addresses'
+        verbose_name = _('IP address')
+        verbose_name_plural = _('IP addresses')
 
     def __str__(self):
         return str(self.address)

+ 4 - 2
netbox/ipam/models/l2vpn.py

@@ -62,7 +62,8 @@ class L2VPN(PrimaryModel):
 
     class Meta:
         ordering = ('name', 'identifier')
-        verbose_name = 'L2VPN'
+        verbose_name = _('L2VPN')
+        verbose_name_plural = _('L2VPNs')
 
     def __str__(self):
         if self.identifier:
@@ -105,13 +106,14 @@ class L2VPNTermination(NetBoxModel):
 
     class Meta:
         ordering = ('l2vpn',)
-        verbose_name = 'L2VPN termination'
         constraints = (
             models.UniqueConstraint(
                 fields=('assigned_object_type', 'assigned_object_id'),
                 name='ipam_l2vpntermination_assigned_object'
             ),
         )
+        verbose_name = _('L2VPN termination')
+        verbose_name_plural = _('L2VPN terminations')
 
     def __str__(self):
         if self.pk is not None:

+ 4 - 0
netbox/ipam/models/services.py

@@ -56,6 +56,8 @@ class ServiceTemplate(ServiceBase, PrimaryModel):
 
     class Meta:
         ordering = ('name',)
+        verbose_name = _('service template')
+        verbose_name_plural = _('service templates')
 
     def get_absolute_url(self):
         return reverse('ipam:servicetemplate', args=[self.pk])
@@ -97,6 +99,8 @@ class Service(ServiceBase, PrimaryModel):
 
     class Meta:
         ordering = ('protocol', 'ports', 'pk')  # (protocol, port) may be non-unique
+        verbose_name = _('service')
+        verbose_name_plural = _('services')
 
     def get_absolute_url(self):
         return reverse('ipam:service', args=[self.pk])

+ 4 - 4
netbox/ipam/models/vlans.py

@@ -79,8 +79,8 @@ class VLANGroup(OrganizationalModel):
                 name='%(app_label)s_%(class)s_unique_scope_slug'
             ),
         )
-        verbose_name = 'VLAN group'
-        verbose_name_plural = 'VLAN groups'
+        verbose_name = _('VLAN group')
+        verbose_name_plural = _('VLAN groups')
 
     def get_absolute_url(self):
         return reverse('ipam:vlangroup', args=[self.pk])
@@ -204,8 +204,8 @@ class VLAN(PrimaryModel):
                 name='%(app_label)s_%(class)s_unique_group_name'
             ),
         )
-        verbose_name = 'VLAN'
-        verbose_name_plural = 'VLANs'
+        verbose_name = _('VLAN')
+        verbose_name_plural = _('VLANs')
 
     def __str__(self):
         return f'{self.name} ({self.vid})'

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

@@ -59,8 +59,8 @@ class VRF(PrimaryModel):
 
     class Meta:
         ordering = ('name', 'rd', 'pk')  # (name, rd) may be non-unique
-        verbose_name = 'VRF'
-        verbose_name_plural = 'VRFs'
+        verbose_name = _('VRF')
+        verbose_name_plural = _('VRFs')
 
     def __str__(self):
         if self.rd:
@@ -91,6 +91,8 @@ class RouteTarget(PrimaryModel):
 
     class Meta:
         ordering = ['name']
+        verbose_name = _('route target')
+        verbose_name_plural = _('route targets')
 
     def __str__(self):
         return self.name

+ 11 - 0
netbox/tenancy/models/contacts.py

@@ -28,6 +28,8 @@ class ContactGroup(NestedGroupModel):
                 name='%(app_label)s_%(class)s_unique_parent_name'
             ),
         )
+        verbose_name = _('contact group')
+        verbose_name_plural = _('contact groups')
 
     def get_absolute_url(self):
         return reverse('tenancy:contactgroup', args=[self.pk])
@@ -40,6 +42,11 @@ class ContactRole(OrganizationalModel):
     def get_absolute_url(self):
         return reverse('tenancy:contactrole', args=[self.pk])
 
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('contact role')
+        verbose_name_plural = _('contact roles')
+
 
 class Contact(PrimaryModel):
     """
@@ -92,6 +99,8 @@ class Contact(PrimaryModel):
                 name='%(app_label)s_%(class)s_unique_group_name'
             ),
         )
+        verbose_name = _('contact')
+        verbose_name_plural = _('contacts')
 
     def __str__(self):
         return self.name
@@ -137,6 +146,8 @@ class ContactAssignment(ChangeLoggedModel, TagsMixin):
                 name='%(app_label)s_%(class)s_unique_object_contact_role'
             ),
         )
+        verbose_name = _('contact assignment')
+        verbose_name_plural = _('contact assignments')
 
     def __str__(self):
         if self.priority:

+ 4 - 0
netbox/tenancy/models/tenants.py

@@ -29,6 +29,8 @@ class TenantGroup(NestedGroupModel):
 
     class Meta:
         ordering = ['name']
+        verbose_name = _('tenant group')
+        verbose_name_plural = _('tenant groups')
 
     def get_absolute_url(self):
         return reverse('tenancy:tenantgroup', args=[self.pk])
@@ -88,6 +90,8 @@ class Tenant(PrimaryModel):
                 condition=Q(group__isnull=True)
             ),
         )
+        verbose_name = _('tenant')
+        verbose_name_plural = _('tenants')
 
     def __str__(self):
         return self.name

+ 12 - 4
netbox/users/models.py

@@ -49,9 +49,10 @@ class NetBoxUser(User):
     objects = NetBoxUserManager()
 
     class Meta:
-        verbose_name = 'User'
         proxy = True
         ordering = ('username',)
+        verbose_name = _('user')
+        verbose_name_plural = _('users')
 
     def get_absolute_url(self):
         return reverse('users:netboxuser', args=[self.pk])
@@ -72,9 +73,10 @@ class NetBoxGroup(Group):
     objects = NetBoxGroupManager()
 
     class Meta:
-        verbose_name = 'Group'
         proxy = True
         ordering = ('name',)
+        verbose_name = _('group')
+        verbose_name_plural = _('groups')
 
     def get_absolute_url(self):
         return reverse('users:netboxgroup', args=[self.pk])
@@ -99,7 +101,8 @@ class UserConfig(models.Model):
 
     class Meta:
         ordering = ['user']
-        verbose_name = verbose_name_plural = _('User Preferences')
+        verbose_name = _('user preferences')
+        verbose_name_plural = _('user preferences')
 
     def get(self, path, default=None):
         """
@@ -281,6 +284,10 @@ class Token(models.Model):
 
     objects = RestrictedQuerySet.as_manager()
 
+    class Meta:
+        verbose_name = _('token')
+        verbose_name_plural = _('tokens')
+
     def __str__(self):
         return self.key if settings.ALLOW_TOKEN_RETRIEVAL else self.partial
 
@@ -373,7 +380,8 @@ class ObjectPermission(models.Model):
 
     class Meta:
         ordering = ['name']
-        verbose_name = _("permission")
+        verbose_name = _('permission')
+        verbose_name_plural = _('permissions')
 
     def __str__(self):
         return self.name

+ 12 - 0
netbox/virtualization/models/clusters.py

@@ -19,6 +19,11 @@ class ClusterType(OrganizationalModel):
     """
     A type of Cluster.
     """
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('cluster type')
+        verbose_name_plural = _('cluster types')
+
     def get_absolute_url(self):
         return reverse('virtualization:clustertype', args=[self.pk])
 
@@ -38,6 +43,11 @@ class ClusterGroup(OrganizationalModel):
         to='tenancy.ContactAssignment'
     )
 
+    class Meta:
+        ordering = ('name',)
+        verbose_name = _('cluster group')
+        verbose_name_plural = _('cluster groups')
+
     def get_absolute_url(self):
         return reverse('virtualization:clustergroup', args=[self.pk])
 
@@ -114,6 +124,8 @@ class Cluster(PrimaryModel):
                 name='%(app_label)s_%(class)s_unique_site_name'
             ),
         )
+        verbose_name = _('cluster')
+        verbose_name_plural = _('clusters')
 
     def __str__(self):
         return self.name

+ 4 - 1
netbox/virtualization/models/virtualmachines.py

@@ -157,6 +157,8 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
                 violation_error_message=_("Virtual machine name must be unique per cluster.")
             ),
         )
+        verbose_name = _('virtual machine')
+        verbose_name_plural = _('virtual machines')
 
     def __str__(self):
         return self.name
@@ -307,7 +309,8 @@ class VMInterface(NetBoxModel, BaseInterface, TrackingModelMixin):
                 name='%(app_label)s_%(class)s_unique_virtual_machine_name'
             ),
         )
-        verbose_name = 'interface'
+        verbose_name = _('interface')
+        verbose_name_plural = _('interfaces')
 
     def __str__(self):
         return self.name

+ 6 - 2
netbox/wireless/models.py

@@ -66,7 +66,8 @@ class WirelessLANGroup(NestedGroupModel):
                 name='%(app_label)s_%(class)s_unique_parent_name'
             ),
         )
-        verbose_name = 'Wireless LAN Group'
+        verbose_name = _('wireless LAN group')
+        verbose_name_plural = _('wireless LAN groups')
 
     def get_absolute_url(self):
         return reverse('wireless:wirelesslangroup', args=[self.pk])
@@ -112,7 +113,8 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
 
     class Meta:
         ordering = ('ssid', 'pk')
-        verbose_name = 'Wireless LAN'
+        verbose_name = _('wireless LAN')
+        verbose_name_plural = _('wireless LANs')
 
     def __str__(self):
         return self.ssid
@@ -194,6 +196,8 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
                 name='%(app_label)s_%(class)s_unique_interfaces'
             ),
         )
+        verbose_name = _('wireless link')
+        verbose_name_plural = _('wireless links')
 
     def __str__(self):
         return self.ssid or f'#{self.pk}'