diff --git a/id/bson_test.go b/id/bson_test.go
index 7ae62ccba4e1dbc2e74ac48b8c5890216a6cba50..c5fca60fb1856ca72138daa5da42d7fb9e6c6674 100644
--- a/id/bson_test.go
+++ b/id/bson_test.go
@@ -41,6 +41,10 @@ func TestID_MarshalUnmarshalBSON(t *testing.T) {
 			name: "RoleID",
 			id:   &ObjectId{Descriptor: &RoleId{RoleID: "1", SpaceId: SpaceId{SpaceID: "1"}}},
 		},
+		{
+			name: "LocaleID",
+			id:   &ObjectId{Descriptor: &LocaleID{LocaleID: "1", SpaceId: SpaceId{SpaceID: "1"}}},
+		},
 		{
 			name: "CollectionId",
 			id:   &ObjectId{Descriptor: &CollectionId{CollectionID: "1", EnvironmentId: EnvironmentId{EnvironmentID: "1", SpaceId: SpaceId{SpaceID: "1"}}}},
diff --git a/id/locale.go b/id/locale.go
new file mode 100644
index 0000000000000000000000000000000000000000..710bc5e34bcf0872ec3381e01b4bab20056419ef
--- /dev/null
+++ b/id/locale.go
@@ -0,0 +1,63 @@
+package id
+
+import "fmt"
+
+const (
+	Locale        = "locale"
+	LocalesPrefix = "locales"
+)
+
+var _ Descriptor = &LocaleID{}
+
+type LocaleID struct {
+	SpaceId
+	LocaleID string `json:"locale_id,omitempty" bson:"locale_id,omitempty"`
+}
+
+func (id *LocaleID) New() Descriptor {
+	return &LocaleID{}
+}
+
+func (id *LocaleID) Type() string { return Locale }
+
+func (id *LocaleID) String() string {
+	return Join(id.SpaceId.String(), LocalesPrefix, id.LocaleID)
+}
+
+func (id *LocaleID) FromParts(parts []string) error {
+	if len(parts) != 4 || parts[2] != LocalesPrefix {
+		return ErrInvalidID
+	}
+	if err := id.SpaceId.FromParts(parts[:2]); err != nil {
+		return err
+	}
+	id.LocaleID = parts[3]
+	return nil
+}
+
+func (id *LocaleID) Map() map[string]any {
+	m := id.SpaceId.Map()
+	m["locale_id"] = id.LocaleID
+	m["type"] = Locale
+	return m
+}
+
+func (id *LocaleID) FromMap(m map[string]any) error {
+	id.LocaleID = m["locale_id"].(string)
+	if id.LocaleID == "" {
+		return fmt.Errorf("%w: LocaleID required", ErrInvalidID)
+	}
+	return id.SpaceId.FromMap(m)
+}
+
+func (id *LocaleID) Validate() error {
+	if id.LocaleID == "" {
+		return fmt.Errorf("%w: LocaleID required", ErrInvalidID)
+	}
+
+	return id.SpaceId.Validate()
+}
+
+func NewLocaleId(spaceID, id string) *ObjectId {
+	return &ObjectId{Descriptor: &LocaleID{SpaceId: SpaceId{SpaceID: spaceID}, LocaleID: id}}
+}
diff --git a/id/object_id_test.go b/id/object_id_test.go
index 4f8d018139ce0b125e09124e5113e94c8031f5b3..56a2c104e000756640eefc978be025c01757d13b 100644
--- a/id/object_id_test.go
+++ b/id/object_id_test.go
@@ -45,6 +45,11 @@ func Test_ParseID(t *testing.T) {
 			id:     "/spaces/<space_id>/roles/<role_id>",
 			result: MustObjectId("/spaces/<space_id>/roles/<role_id>"),
 		},
+		{
+			name:   "LocaleID",
+			id:     "/spaces/<space_id>/locales/<locale_id>",
+			result: MustObjectId("/spaces/<space_id>/locales/<locale_id>"),
+		},
 		{
 			name:   "EnvironmentID",
 			id:     "/spaces/<space_id>/envs/<env_id>",
@@ -165,6 +170,13 @@ func Test_Map(t *testing.T) {
 				RoleID:  "<role_id>",
 			}},
 		},
+		{
+			name: "LocaleID",
+			id: &ObjectId{Descriptor: &LocaleID{
+				SpaceId:  SpaceId{SpaceID: "<space_id>"},
+				LocaleID: "<locale_id>",
+			}},
+		},
 		{
 			name: "EnvironmentID",
 			id: &ObjectId{Descriptor: &EnvironmentId{
diff --git a/id/registry.go b/id/registry.go
index 0ffec0240952b742c33a4763c6ee49fa210ce4f0..17c86e4bff8a14ccef7cde99be3a5649230b2254 100644
--- a/id/registry.go
+++ b/id/registry.go
@@ -110,6 +110,7 @@ func RegisterSystemIds(r *Registry) {
 	r.RegisterDescriptor(&SystemId{})
 	r.RegisterDescriptor(&ServiceId{})
 	r.RegisterDescriptor(&OrganizationId{})
+	r.RegisterDescriptor(&LocaleID{})
 }
 
 func GetRegistry() *Registry {
diff --git a/id/system/system.go b/id/system/system.go
index 810062927a8cb21c26acc5e8a32017e9d68ca936..c33995d905a615b6c24422e27239da34141d4752 100644
--- a/id/system/system.go
+++ b/id/system/system.go
@@ -10,6 +10,7 @@ import (
 	"git.perx.ru/perxis/perxis-go/pkg/collections"
 	"git.perx.ru/perxis/perxis-go/pkg/environments"
 	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
 	"git.perx.ru/perxis/perxis-go/pkg/organizations"
 	"git.perx.ru/perxis/perxis-go/pkg/roles"
 	"git.perx.ru/perxis/perxis-go/pkg/spaces"
@@ -74,6 +75,11 @@ func Handler(obj any) *id.ObjectId {
 		var i id.UserId
 		i.UserID = val.GetID(context.TODO())
 		return id.MustObjectId(&i)
+	case *locales.Locale:
+		var i id.LocaleID
+		i.SpaceID = val.SpaceID
+		i.LocaleID = val.ID
+		return id.MustObjectId(&i)
 	}
 	return nil
 }
@@ -92,6 +98,7 @@ func Register(r *id.Registry) {
 	r.RegisterObjectHandler(reflect.TypeOf(&auth.ClientPrincipal{}), Handler)
 	r.RegisterObjectHandler(reflect.TypeOf(&auth.SystemPrincipal{}), Handler)
 	r.RegisterObjectHandler(reflect.TypeOf(&auth.Anonymous{}), Handler)
+	r.RegisterObjectHandler(reflect.TypeOf(&locales.Locale{}), Handler)
 }
 
 func init() {
diff --git a/id/test/object_id_test.go b/id/test/object_id_test.go
index ec2cd931d54b47696b9b3a4d76218e27448f3c64..e29328ddf25f799cbb56707fa292762f69c39207 100644
--- a/id/test/object_id_test.go
+++ b/id/test/object_id_test.go
@@ -9,6 +9,7 @@ import (
 	"git.perx.ru/perxis/perxis-go/pkg/collections"
 	"git.perx.ru/perxis/perxis-go/pkg/environments"
 	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
 	"git.perx.ru/perxis/perxis-go/pkg/organizations"
 	"git.perx.ru/perxis/perxis-go/pkg/roles"
 	"git.perx.ru/perxis/perxis-go/pkg/spaces"
@@ -799,3 +800,89 @@ func Test_RevisionId(t *testing.T) {
 		})
 	}
 }
+
+func Test_LocaleId(t *testing.T) {
+
+	tests := []struct {
+		name   string
+		in     any
+		out    string
+		result *id.ObjectId
+		err    error
+	}{
+		{
+			name: "valid string",
+			in:   "/spaces/<space_id>/locales/<locale_id>",
+			result: &id.ObjectId{Descriptor: &id.LocaleID{
+				SpaceId:  id.SpaceId{SpaceID: "<space_id>"},
+				LocaleID: "<locale_id>",
+			}},
+		},
+		{
+			name: "invalid string",
+			in:   "/locales/<locale_id>",
+			result: &id.ObjectId{Descriptor: &id.LocaleID{
+				SpaceId:  id.SpaceId{SpaceID: "<space_id>"},
+				LocaleID: "<locale_id>",
+			}},
+			err: id.ErrInvalidID,
+		},
+		{
+			name: "valid object",
+			in:   &locales.Locale{SpaceID: "<space_id>", ID: "<locale_id>"},
+			out:  "/spaces/<space_id>/locales/<locale_id>",
+			result: &id.ObjectId{Descriptor: &id.LocaleID{
+				SpaceId:  id.SpaceId{SpaceID: "<space_id>"},
+				LocaleID: "<locale_id>",
+			}},
+		},
+		{
+			name: "valid map",
+			in:   map[string]any{"type": "locale", "space_id": "<space_id>", "locale_id": "<locale_id>"},
+			out:  "/spaces/<space_id>/locales/<locale_id>",
+			result: &id.ObjectId{Descriptor: &id.LocaleID{
+				SpaceId:  id.SpaceId{SpaceID: "<space_id>"},
+				LocaleID: "<locale_id>",
+			}},
+		},
+		{
+			name: "invalid map 1",
+			in:   map[string]any{"type": "client", "space_id": "<space_id>"},
+			out:  "/spaces/<space_id>/locales/<locale_id>",
+			result: &id.ObjectId{Descriptor: &id.LocaleID{
+				SpaceId:  id.SpaceId{SpaceID: "<space_id>"},
+				LocaleID: "<locale_id>",
+			}},
+			err: id.ErrInvalidID,
+		},
+		{
+			name: "invalid map 2",
+			in:   map[string]any{"type": "locale", "locale_id": "<locale_id>"},
+			out:  "/spaces/<space_id>/locales/<locale_id>",
+			result: &id.ObjectId{Descriptor: &id.LocaleID{
+				SpaceId:  id.SpaceId{SpaceID: "<space_id>"},
+				LocaleID: "<locale_id>",
+			}},
+			err: id.ErrInvalidID,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			i, err := id.NewObjectId(tt.in)
+
+			if tt.err != nil {
+				require.ErrorIs(t, err, tt.err)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.result, i)
+			if tt.out == "" {
+				require.Equal(t, tt.in, i.String())
+			} else {
+				require.Equal(t, tt.out, i.String())
+			}
+		})
+	}
+}