diff --git a/perxis-proto b/perxis-proto
index d05d75325479800baef608bcde55c46b42b019e0..64fe5194e41d6d1023d6d78e794e7458ecef1888 160000
--- a/perxis-proto
+++ b/perxis-proto
@@ -1 +1 @@
-Subproject commit d05d75325479800baef608bcde55c46b42b019e0
+Subproject commit 64fe5194e41d6d1023d6d78e794e7458ecef1888
diff --git a/pkg/items/item.go b/pkg/items/item.go
index c808c719628eb512d71589be53c9cd2f073b9c3d..1a226a7627a0435a2b77480ebc2095e126dae37d 100644
--- a/pkg/items/item.go
+++ b/pkg/items/item.go
@@ -107,8 +107,15 @@ type Item struct {
 	// Если указан, то создается перевод для указанного языка, поле translations игнорируется
 	LocaleID string `json:"locale_id" bson:"-"`
 
-	// Используется при одновременной установке/получении нескольких переводов
-	// Ключами является идентификатор локали, значениями - данные переводов
+	// Позволяет одновременно установить/получить несколько переводов и производить манипуляции с переводами
+	// Ключами является идентификатор локали, значениями - данные переводы
+	// При обновлении не происходит валидация или модификация каждого из переводов в соответствие со схемой,
+	// поэтому обновление через поле `translations` стоит выполнять с аккуратностью
+	// Для удаления переводов реализована следующая логика:
+	// - {"lang":nil|{}} - сброс перевода для языка
+	// - {"lang":map{...}} - установка перевода для языка
+	// - {"lang":map{...}, "*":nil} - установка перевода для языка, сброс остальных переводов
+	// - {"*":nil} - сброс всех переводов
 	Translations map[string]map[string]interface{} `json:"translations" bson:"translations,omitempty"`
 
 	// Список идентификаторов локалей, для которых есть переводы.
@@ -177,31 +184,44 @@ func (i *Item) ToMap() map[string]interface{} {
 	}
 }
 
-// SetData устанавливает перевод в нужное поле записи, предварительно рассчитав из него дельту по
-// отношению к основным данным
-func (i *Item) SetData(dt map[string]interface{}, localizer *localizer.Localizer) (err error) {
-	if localizer != nil && localizer.LocaleID() != locales.DefaultID {
-		if i.Translations == nil {
-			i.Translations = make(map[string]map[string]interface{})
-		}
-		i.Translations[localizer.LocaleID()] = dt
-		i.Translations[localizer.LocaleID()], err = localizer.ExtractTranslation(i.Data, i.Translations)
-		if !data.Contains(localizer.LocaleID(), i.TranslationsIDs) {
-			i.TranslationsIDs = append(i.TranslationsIDs, localizer.LocaleID())
-		}
+// SetData устанавливает перевод в нужное поле записи
+func (i *Item) SetData(dt map[string]interface{}, localeID string) {
+	if localeID == "" || localeID == locales.DefaultID {
+		i.Data = dt
 		return
 	}
-
-	i.Data = dt
+	if i.Translations == nil {
+		i.Translations = map[string]map[string]interface{}{localeID: dt}
+		if !data.Contains(localeID, i.TranslationsIDs) {
+			i.TranslationsIDs = append(i.TranslationsIDs, localeID)
+		}
+	}
 	return
 }
 
 // GetData возвращает полные локализованные данные записи
-func (i *Item) GetData(localizer *localizer.Localizer) (map[string]interface{}, error) {
-	if localizer != nil {
-		return localizer.Localize(i.Data, i.Translations)
+func (i *Item) GetData(localeID string) map[string]interface{} {
+	if localeID == "" || localeID == locales.DefaultID {
+		return i.Data
+	}
+	if i.Translations != nil {
+		return i.Translations[localeID]
+	}
+	return nil
+}
+
+func (i *Item) AddTranslations(translations map[string]map[string]interface{}) {
+	if i.Translations == nil {
+		i.Translations = make(map[string]map[string]interface{}, len(translations))
+	}
+	for l, t := range translations {
+		i.Translations[l] = t
+	}
+	for k := range translations {
+		if !data.Contains(k, i.TranslationsIDs) {
+			i.TranslationsIDs = append(i.TranslationsIDs, k)
+		}
 	}
-	return i.Data, nil
 }
 
 func (i *Item) Localize(localizer *localizer.Localizer) (err error) {
@@ -217,25 +237,25 @@ func (i *Item) Localize(localizer *localizer.Localizer) (err error) {
 	return nil
 }
 
-// UnsetTranslation устанавливает значение перевода в nil, для сброса перевода для языка
-// "localeID":map{...}, "*":nil} - установка перевода для языка, сброс остальных переводов
-// {"*":nil} -сброс всех переводов
-func (i *Item) UnsetTranslation(localeID string) {
-	if i.Translations == nil {
-		i.Translations = make(map[string]map[string]interface{})
-	}
-
-	i.Translations[localeID] = nil
-}
-
-// SetTranslation устанавливает перевод для языка
-func (i *Item) SetTranslation(dt map[string]interface{}, localeID string) {
-	if i.Translations == nil {
-		i.Translations = make(map[string]map[string]interface{})
-	}
-
-	i.Translations[localeID] = dt
-}
+// // UnsetTranslation устанавливает значение перевода в nil, для сброса перевода для языка
+// // "localeID":map{...}, "*":nil} - установка перевода для языка, сброс остальных переводов
+// // {"*":nil} -сброс всех переводов
+// func (i *Item) UnsetTranslation(localeID string) {
+// 	if i.Translations == nil {
+// 		i.Translations = make(map[string]map[string]interface{})
+// 	}
+//
+// 	i.Translations[localeID] = nil
+// }
+//
+// // SetTranslation устанавливает перевод для языка
+// func (i *Item) SetTranslation(dt map[string]interface{}, localeID string) {
+// 	if i.Translations == nil {
+// 		i.Translations = make(map[string]map[string]interface{})
+// 	}
+//
+// 	i.Translations[localeID] = dt
+// }
 
 func (i *Item) Encode(ctx context.Context, s *schema.Schema) (*Item, error) {
 	res := *i
@@ -250,9 +270,12 @@ func (i *Item) Encode(ctx context.Context, s *schema.Schema) (*Item, error) {
 	if len(i.Translations) > 0 {
 		res.Translations = make(map[string]map[string]interface{}, len(i.Translations))
 		for l, v := range i.Translations {
+			if len(v) == 0 {
+				res.Translations[l] = v
+				continue
+			}
 			dt, err := schema.Encode(ctx, s, v)
 			if err != nil {
-				// return errors.WithField(err, fmt.Sprintf("translations.%s", l))
 				return nil, err
 			}
 			res.Translations[l] = dt.(map[string]interface{})
@@ -268,12 +291,15 @@ func (i *Item) Decode(ctx context.Context, s *schema.Schema) (*Item, error) {
 		res.Data, err = s.Decode(ctx, i.Data)
 		if err != nil {
 			return nil, err
-			// return errors.WithField(err, "data")
 		}
 	}
 	if len(i.Translations) > 0 {
 		res.Translations = make(map[string]map[string]interface{}, len(i.Translations))
 		for l, v := range i.Translations {
+			if len(v) == 0 {
+				res.Translations[l] = v
+				continue
+			}
 			dt, err := schema.Decode(ctx, s, v)
 			if err != nil {
 				return nil, err
@@ -298,18 +324,13 @@ func (i *Item) ProcessData(ctx context.Context, sch *schema.Schema, fn ProcessDa
 
 	tr := make(map[string]map[string]interface{})
 	for _, l := range locales {
-		data, err := i.GetData(localizer.NewLocalizer(localizer.Config{
-			Schema:           sch,
-			Locales:          locales,
-			LocaleID:         l.ID,
-			AllowNoPublished: false,
-			AllowDisabled:    false,
-		}))
+		itm := *i
+		err := itm.Localize(localizer.NewLocalizer(localizer.Config{Schema: sch, Locales: locales, LocaleID: l.ID}))
 		if err != nil {
 			return nil, errors.WithField(err, fmt.Sprintf("translations.%s", l.ID))
 		}
 
-		dt, err := fn(ctx, sch, data)
+		dt, err := fn(ctx, sch, itm.Data)
 		if err != nil {
 			return nil, errors.WithField(err, fmt.Sprintf("translations.%s", l.ID))
 		}
@@ -531,7 +552,11 @@ func ItemToProto(item *Item) *pb.Item {
 	if item.Translations != nil {
 		protoItem.Translations = make(map[string]*structpb.Struct, len(item.Translations))
 		for k, v := range item.Translations {
-			protoItem.Translations[k], _ = structpb.NewStruct(v)
+			var t *structpb.Struct
+			if v != nil {
+				t, _ = structpb.NewStruct(v)
+			}
+			protoItem.Translations[k] = t
 		}
 	}
 
@@ -582,8 +607,16 @@ func ItemFromProto(protoItem *pb.Item) *Item {
 
 	if protoItem.Translations != nil {
 		item.Translations = make(map[string]map[string]interface{}, len(protoItem.Translations))
+	}
+	if protoItem.Translations != nil {
 		for k, v := range protoItem.Translations {
-			item.Translations[k] = v.AsMap()
+			// При proto.Marshal/Unmarshal `nil`-значение превращается в `Struct{}`, поэтому логика
+			// много смысла не несет, но хотя бы здесь сохраним различие пустого и nil значений
+			var t map[string]interface{}
+			if v != nil {
+				t = v.AsMap()
+			}
+			item.Translations[k] = t
 		}
 	}
 
diff --git a/pkg/items/item_test.go b/pkg/items/item_test.go
index 47e7f02be2014b3c94e4a7c0d29d4ccab7dbca63..2cef5e75414f55e09d3e5f42d4ed4ee56f0cd866 100644
--- a/pkg/items/item_test.go
+++ b/pkg/items/item_test.go
@@ -1,6 +1,7 @@
 package items
 
 import (
+	"context"
 	"fmt"
 	"testing"
 	"time"
@@ -8,6 +9,7 @@ import (
 	"git.perx.ru/perxis/perxis-go/pkg/schema"
 	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestItem_Set(t *testing.T) {
@@ -95,3 +97,169 @@ func TestGetField(t *testing.T) {
 		})
 	}
 }
+
+func TestItem_Proto(t *testing.T) {
+	w := time.Now().UTC()
+	tests := []struct {
+		name string
+		item *Item
+	}{
+		{
+			name: "All fields are filled",
+			item: &Item{
+				ID:           "id-1",
+				SpaceID:      "space-1",
+				EnvID:        "env-1",
+				CollectionID: "coll-1",
+				State:        StateDraft,
+				CreatedRevAt: w.Add(time.Hour),
+				CreatedBy:    "user-1",
+				CreatedAt:    w,
+				UpdatedAt:    w.Add(time.Hour),
+				UpdatedBy:    "user-2",
+				Data:         map[string]any{"a": "b", "c": "d", "x": nil},
+				LocaleID:     "ru",
+				Translations: map[string]map[string]interface{}{
+					"ru": {"a": "B"},
+					"en": nil,
+				},
+				TranslationsIDs:     []string{"ru", "en"},
+				RevisionID:          "rev-1",
+				RevisionDescription: "desc-1",
+				Permissions:         PermissionsAllowAny,
+				SearchScore:         100.0,
+				Deleted:             false,
+				Hidden:              true,
+				Template:            false,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			assert.Equal(t, tc.item, ItemFromProto(ItemToProto(tc.item)))
+		})
+	}
+}
+
+func TestItem_Encode_Decode(t *testing.T) {
+	w := time.Now().UTC()
+	tests := []struct {
+		name string
+		item *Item
+	}{
+		{
+			name: "Data",
+			item: &Item{
+				ID:           "id-1",
+				SpaceID:      "space-1",
+				EnvID:        "env-1",
+				CollectionID: "coll-1",
+				State:        StateDraft,
+				CreatedRevAt: w.Add(time.Hour),
+				CreatedBy:    "user-1",
+				CreatedAt:    w,
+				UpdatedAt:    w.Add(time.Hour),
+				UpdatedBy:    "user-2",
+				Data: map[string]any{
+					"a": "text-a",
+					"b": 124.1,
+					"c": map[string]interface{}{"x": "y"},
+					"d": []interface{}{"k", "l", "m"},
+				},
+				RevisionID:          "rev-1",
+				RevisionDescription: "desc-1",
+				Permissions:         PermissionsAllowAny,
+				SearchScore:         100.0,
+				Deleted:             false,
+				Hidden:              true,
+				Template:            false,
+			},
+		},
+		{
+			name: "Data and Translations",
+			item: &Item{
+				ID:           "id-1",
+				SpaceID:      "space-1",
+				EnvID:        "env-1",
+				CollectionID: "coll-1",
+				State:        StateDraft,
+				CreatedRevAt: w.Add(time.Hour),
+				CreatedBy:    "user-1",
+				CreatedAt:    w,
+				UpdatedAt:    w.Add(time.Hour),
+				UpdatedBy:    "user-2",
+				Data: map[string]any{
+					"a": "text-a",
+					"b": 124.1,
+					"c": map[string]interface{}{"x": "y"},
+					"d": []interface{}{"k", "l", "m"},
+				},
+				LocaleID: "ru",
+				Translations: map[string]map[string]interface{}{
+					"ru": {"a": "ru-a"},
+					"en": {"a": "en-a"},
+				},
+				TranslationsIDs:     []string{"ru", "en"},
+				RevisionID:          "rev-1",
+				RevisionDescription: "desc-1",
+				Permissions:         PermissionsAllowAny,
+				SearchScore:         100.0,
+				Deleted:             false,
+				Hidden:              false,
+				Template:            false,
+			},
+		},
+		{
+			name: "Nil Translation",
+			item: &Item{
+				ID:           "id-1",
+				SpaceID:      "space-1",
+				EnvID:        "env-1",
+				CollectionID: "coll-1",
+				State:        StateDraft,
+				CreatedRevAt: w.Add(time.Hour),
+				CreatedBy:    "user-1",
+				CreatedAt:    w,
+				UpdatedAt:    w.Add(time.Hour),
+				UpdatedBy:    "user-2",
+				Data: map[string]any{
+					"a": "text-a",
+					"b": 124.1,
+					"c": map[string]interface{}{"x": "y"},
+					"d": []interface{}{"k", "l", "m"},
+				},
+				LocaleID: "ru",
+				Translations: map[string]map[string]interface{}{
+					"ru": {"a": "ru-a"},
+					"en": nil,
+				},
+				TranslationsIDs:     []string{"ru", "en"},
+				RevisionID:          "rev-1",
+				RevisionDescription: "desc-1",
+				Permissions:         PermissionsAllowAny,
+				SearchScore:         100.0,
+				Deleted:             false,
+				Hidden:              false,
+				Template:            false,
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+			sch := schema.New(
+				"a", field.String(),
+				"b", field.Number(field.NumberFormatFloat),
+				"c", field.Object("x", field.String()),
+				"d", field.Array(field.String()),
+			)
+			enc, err := tc.item.Encode(ctx, sch)
+			require.NoError(t, err)
+			dec, err := enc.Decode(ctx, sch)
+			require.NoError(t, err)
+			assert.Equal(t, tc.item, dec)
+		})
+	}
+}
diff --git a/proto/items/items.pb.go b/proto/items/items.pb.go
index c71dcb83bf0e0b1db89ab919391f79efa43bc6e4..8a6ed540cf9a1c9f21bd313f830d587d3468bd1d 100644
--- a/proto/items/items.pb.go
+++ b/proto/items/items.pb.go
@@ -384,14 +384,23 @@ type Item struct {
 	Template            bool                   `protobuf:"varint,21,opt,name=template,proto3" json:"template,omitempty"`
 	Permissions         *Permissions           `protobuf:"bytes,22,opt,name=permissions,proto3" json:"permissions,omitempty"`
 	SearchScore         float64                `protobuf:"fixed64,23,opt,name=search_score,json=searchScore,proto3" json:"search_score,omitempty"` // релеватность элемента при полнотекстовом поиске
-	// При создании или обновлении идентификатор локали в котором создается запись, опционально.
+	// Идентификатор локали полученной записи.
+	// При создании или обновлении идентификатор локали, в которой создается запись, опционально.
 	// Если указан, то создается перевод для указанного языка, поле translations игнорируется
-	LocaleId string `protobuf:"bytes,100,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // идентификатор локали полученной записи
+	LocaleId string `protobuf:"bytes,100,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"`
 	// Позволяет одновременно установить/получить несколько переводов и производить манипуляции с переводами
 	// Ключами является идентификатор локали, значениями - данные переводы
+	// При обновлении не происходит валидация или модификация каждого из переводов в соответствие со схемой,
+	// поэтому обновление через поле `translations` стоит выполнять с аккуратностью
+	// Для удаления переводов реализована следующая логика:
+	// - {"lang":nil|{}} - сброс перевода для языка
+	// - {"lang":map{...}} - установка перевода для языка
+	// - {"lang":map{...}, "*":nil} - установка перевода для языка, сброс остальных переводов
+	// - {"*":nil} - сброс всех переводов
 	Translations map[string]*structpb.Struct `protobuf:"bytes,12,rep,name=translations,proto3" json:"translations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
-	// список идентификаторов локалей для которых есть переводы
-	// соотвествует ключам в translations
+	// Список идентификаторов локалей, для которых есть переводы
+	// Соотвествует списку переводов в translations, при получении записи всегда возвращается
+	// полный список. Невозможно обновить вручную: формируется системой
 	TranslationsIds []string `protobuf:"bytes,101,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"`
 }
 
@@ -1044,13 +1053,15 @@ type FindOptions struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Options         *common.FindOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
-	Deleted         bool                `protobuf:"varint,3,opt,name=deleted,proto3" json:"deleted,omitempty"`
-	Regular         bool                `protobuf:"varint,4,opt,name=regular,proto3" json:"regular,omitempty"`
-	Hidden          bool                `protobuf:"varint,5,opt,name=hidden,proto3" json:"hidden,omitempty"`
-	Templates       bool                `protobuf:"varint,6,opt,name=templates,proto3" json:"templates,omitempty"`
-	LocaleId        string              `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"`                      // Язык перевода который будет использоваться. Если не указан, то возвращаются данные для языка по умолчанию
-	TranslationsIds []string            `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` // Список идентификаторов переводов/локалей, которых должны быть включены в результат
+	Options   *common.FindOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
+	Deleted   bool                `protobuf:"varint,3,opt,name=deleted,proto3" json:"deleted,omitempty"`
+	Regular   bool                `protobuf:"varint,4,opt,name=regular,proto3" json:"regular,omitempty"`
+	Hidden    bool                `protobuf:"varint,5,opt,name=hidden,proto3" json:"hidden,omitempty"`
+	Templates bool                `protobuf:"varint,6,opt,name=templates,proto3" json:"templates,omitempty"`
+	LocaleId  string              `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если не указан, то возвращаются данные для языка по умолчанию
+	// Список идентификаторов переводов/локалей, которых должны быть включены в результат
+	// Возможно указание '*' для получения всех переводов
+	TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"`
 }
 
 func (x *FindOptions) Reset() {
@@ -1186,8 +1197,10 @@ type GetPublishedOptions struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	LocaleId        string   `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"`                      // Язык перевода который будет использоваться. Если не указан, то возвращаются данные для языка по умолчанию
-	TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` // Список идентификаторов переводов/локалей, которых должны быть включены в результат
+	LocaleId string `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если не указан, то возвращаются данные для языка по умолчанию
+	// Список идентификаторов переводов/локалей, которых должны быть включены в результат
+	// Возможно указание '*' для получения всех переводов
+	TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"`
 }
 
 func (x *GetPublishedOptions) Reset() {
@@ -1439,11 +1452,13 @@ type FindPublishedOptions struct {
 
 	Options *common.FindOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"`
 	// string locale_id = 3; // язык для поиска переводов. Если не указан, то возвращаются данные для языка по умолчанию
-	Regular         bool     `protobuf:"varint,4,opt,name=regular,proto3" json:"regular,omitempty"`
-	Hidden          bool     `protobuf:"varint,5,opt,name=hidden,proto3" json:"hidden,omitempty"`
-	Templates       bool     `protobuf:"varint,6,opt,name=templates,proto3" json:"templates,omitempty"`
-	LocaleId        string   `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"`                      // Язык перевода который будет использоваться. Если не указан, то возвращаются данные для языка по умолчанию
-	TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"` // Список идентификаторов переводов/локалей, которых должны быть включены в результат
+	Regular   bool   `protobuf:"varint,4,opt,name=regular,proto3" json:"regular,omitempty"`
+	Hidden    bool   `protobuf:"varint,5,opt,name=hidden,proto3" json:"hidden,omitempty"`
+	Templates bool   `protobuf:"varint,6,opt,name=templates,proto3" json:"templates,omitempty"`
+	LocaleId  string `protobuf:"bytes,7,opt,name=locale_id,json=localeId,proto3" json:"locale_id,omitempty"` // Язык перевода который будет использоваться. Если не указан, то возвращаются данные для языка по умолчанию
+	// Список идентификаторов переводов/локалей, которых должны быть включены в результат
+	// Возможно указание '*' для получения всех переводов
+	TranslationsIds []string `protobuf:"bytes,8,rep,name=translations_ids,json=translationsIds,proto3" json:"translations_ids,omitempty"`
 }
 
 func (x *FindPublishedOptions) Reset() {