diff --git a/perxis-proto b/perxis-proto
index fc23183a86463b2aa81e3b7570fad1f873c1e435..2e4728f6f3d5d63dcb9d5548b17af9efce605280 160000
--- a/perxis-proto
+++ b/perxis-proto
@@ -1 +1 @@
-Subproject commit fc23183a86463b2aa81e3b7570fad1f873c1e435
+Subproject commit 2e4728f6f3d5d63dcb9d5548b17af9efce605280
diff --git a/pkg/data/list.go b/pkg/data/list.go
index 90042506c4e0ff0d246239cb78cf6213d3ab934b..4da827db9680bc20237dfc87023db1c31ff8a664 100644
--- a/pkg/data/list.go
+++ b/pkg/data/list.go
@@ -192,3 +192,15 @@ func CloneSlice[T interface{ Clone() T }](s []T) []T {
 	}
 	return result
 }
+
+type Keyed[T comparable] interface {
+	Key() T
+}
+
+func SliceToMap[K comparable, V Keyed[K]](s []V) map[K]V {
+	res := make(map[K]V, len(s))
+	for _, elem := range s {
+		res[elem.Key()] = elem
+	}
+	return res
+}
diff --git a/pkg/data/list_test.go b/pkg/data/list_test.go
index 8231aae992e91f09f50bb71fb1e028e148c35993..c77c226edc8ab885ebcae9cab023d496c175a486 100644
--- a/pkg/data/list_test.go
+++ b/pkg/data/list_test.go
@@ -139,3 +139,18 @@ keyB2: val20
 		})
 	}
 }
+
+type KV struct {
+	K string
+	V string
+}
+
+func (kv *KV) Key() string {
+	return kv.K
+}
+
+func TestSliceToMap(t *testing.T) {
+	s := []*KV{{"a", "1"}, {"b", "2"}, {"c", "3"}}
+	m := SliceToMap(s)
+	assert.Equal(t, map[string]*KV{"a": {"a", "1"}, "b": {"b", "2"}, "c": {"c", "3"}}, m)
+}
diff --git a/pkg/locales/errors.go b/pkg/locales/errors.go
index f5673290341c6813c19a0e21c60849960ce88230..b5876aff89925f394b8e36c1e609dfc6b4581164 100644
--- a/pkg/locales/errors.go
+++ b/pkg/locales/errors.go
@@ -18,4 +18,5 @@ var (
 	ErrSetDefaultLocaleFallback       = errors.New("cannot set fallback for default locale")
 	ErrDeleteDefaultLocale            = errors.New("cannot delete default locale")
 	ErrDeleteFallbackLocale           = errors.New("cannot delete locale that is used as fallback for other locales")
+	ErrCircularDependency             = errors.New("fallback locales contain cycles")
 )
diff --git a/pkg/locales/locale.go b/pkg/locales/locale.go
index 520bc68c68c5f1bbe14398237c5a732ed192b212..e0e32616ee4bd526c7d25c9cbab740e196529328 100644
--- a/pkg/locales/locale.go
+++ b/pkg/locales/locale.go
@@ -1,6 +1,8 @@
 package locales
 
 import (
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
 	pb "git.perx.ru/perxis/perxis-go/proto/locales"
 	"golang.org/x/text/language"
 )
@@ -23,18 +25,18 @@ type Locale struct {
 	Disabled   bool   `json:"disabled" bson:"disabled"`       // Запретить использование локали. Нельзя создавать и редактировать контент для данной локали (кроме default)
 }
 
-func (l Locale) Clone() *Locale {
+func (locale *Locale) Clone() *Locale {
 	return &Locale{
-		ID:         l.ID,
-		SpaceID:    l.SpaceID,
-		Name:       l.Name,
-		NativeName: l.NativeName,
-		Code:       l.Code,
-		Fallback:   l.Fallback,
-		Direction:  l.Direction,
-		Weight:     l.Weight,
-		NoPublish:  l.NoPublish,
-		Disabled:   l.Disabled,
+		ID:         locale.ID,
+		SpaceID:    locale.SpaceID,
+		Name:       locale.Name,
+		NativeName: locale.NativeName,
+		Code:       locale.Code,
+		Fallback:   locale.Fallback,
+		Direction:  locale.Direction,
+		Weight:     locale.Weight,
+		NoPublish:  locale.NoPublish,
+		Disabled:   locale.Disabled,
 	}
 }
 
@@ -46,6 +48,13 @@ func (locale *Locale) Equal(other *Locale) bool {
 	return locale == other || locale != nil && other != nil && *locale == *other
 }
 
+func (locale *Locale) Key() string {
+	if locale == nil {
+		return ""
+	}
+	return locale.ID
+}
+
 // Возвращает язык локали, например "en", "ru"
 func (locale *Locale) GetLanguage() string {
 	lang, err := language.Parse(locale.Code)
@@ -102,3 +111,54 @@ func LocaleFromProto(protoLocale *pb.Locale) *Locale {
 	}
 	return locale
 }
+
+func FallbackSort(locales []*Locale) ([]*Locale, error) {
+	if len(locales) == 0 {
+		return locales, nil
+	}
+
+	var (
+		unsorted = data.SliceToMap(locales)
+		sorted   = make([]*Locale, 0, len(locales))
+		visited  = make(map[string]bool, len(locales))
+	)
+
+	for {
+		changed := false
+		for _, node := range locales {
+
+			if visited[node.ID] {
+				continue
+			}
+
+			if _, exist := unsorted[node.Fallback]; !exist && node.Fallback != "" {
+				return nil, errors.Wrap(ErrNotFound, node.Fallback)
+			}
+
+			if node.IsDefault() {
+				sorted = append([]*Locale{node}, sorted...)
+				visited[node.ID] = true
+				changed = true
+				continue
+			}
+
+			if node.Fallback == "" || visited[node.Fallback] {
+				sorted = append(sorted, node)
+				visited[node.ID] = true
+				changed = true
+			}
+		}
+
+		// Все локали перенесены в список отсортированных
+		if len(sorted) == len(locales) {
+			break
+		}
+
+		// Если не произошло изменений, но не все локали отсортированы - обнаружены циклические зависимости
+		if !changed {
+			return nil, ErrCircularDependency
+		}
+	}
+
+	return sorted, nil
+}
diff --git a/pkg/locales/locale_test.go b/pkg/locales/locale_test.go
index 5875107a120b71ff4c89eb0f28cc79c6cd225586..664e369277d701e8c34b818433c492ee688695f4 100644
--- a/pkg/locales/locale_test.go
+++ b/pkg/locales/locale_test.go
@@ -3,6 +3,7 @@ package locales
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -79,3 +80,218 @@ func TestLocale_Equal(t *testing.T) {
 		require.Equal(t, tt.want, got)
 	}
 }
+
+func TestFallbackSort(t *testing.T) {
+	tests := []struct {
+		name    string
+		input   []*Locale
+		want    []*Locale
+		wantErr bool
+	}{
+		{
+			name:    "Empty input",
+			input:   nil,
+			want:    nil,
+			wantErr: false,
+		},
+		{
+			name:    "Empty input",
+			input:   []*Locale{},
+			want:    []*Locale{},
+			wantErr: false,
+		},
+		{
+			name: "One locale not default",
+			input: []*Locale{
+				{ID: "a"},
+			},
+			want: []*Locale{
+				{ID: "a"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "One default locale",
+			input: []*Locale{
+				{ID: DefaultID},
+			},
+			want: []*Locale{
+				{ID: DefaultID},
+			},
+			wantErr: false,
+		},
+		{
+			name: "One locale with fallback",
+			input: []*Locale{
+				{ID: DefaultID, Fallback: "a"},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Fallback locale not exists",
+			input: []*Locale{
+				{ID: "a", Fallback: "c"},
+				{ID: "b", Fallback: DefaultID},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "Not found default",
+			input: []*Locale{
+				{ID: "a", Fallback: DefaultID},
+				{ID: "b", Fallback: "d"},
+				{ID: "c", Fallback: DefaultID},
+				{ID: "d", Fallback: "c"},
+				{ID: "e", Fallback: "a"},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Simple",
+			input: []*Locale{
+				{ID: DefaultID},
+				{ID: "a", Fallback: DefaultID},
+				{ID: "b", Fallback: "d"},
+				{ID: "c", Fallback: DefaultID},
+				{ID: "d", Fallback: "c"},
+				{ID: "e", Fallback: "a"},
+			},
+			want: []*Locale{
+				{ID: DefaultID},
+				{ID: "a", Fallback: DefaultID},
+				{ID: "c", Fallback: DefaultID},
+				{ID: "d", Fallback: "c"},
+				{ID: "e", Fallback: "a"},
+				{ID: "b", Fallback: "d"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Simple #2",
+			input: []*Locale{
+				{ID: "a", Fallback: DefaultID},
+				{ID: "b", Fallback: "d"},
+				{ID: "c", Fallback: DefaultID},
+				{ID: "d", Fallback: "c"},
+				{ID: "e", Fallback: "a"},
+				{ID: DefaultID},
+			},
+			want: []*Locale{
+				{ID: DefaultID},
+				{ID: "a", Fallback: DefaultID},
+				{ID: "c", Fallback: DefaultID},
+				{ID: "d", Fallback: "c"},
+				{ID: "e", Fallback: "a"},
+				{ID: "b", Fallback: "d"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Simple #3",
+			input: []*Locale{
+				{ID: DefaultID},
+				{ID: "a"},
+				{ID: "b"},
+			},
+			want: []*Locale{
+				{ID: DefaultID},
+				{ID: "a"},
+				{ID: "b"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Simple #4",
+			input: []*Locale{
+				{ID: "a"},
+				{ID: "b"},
+				{ID: DefaultID},
+			},
+			want: []*Locale{
+				{ID: DefaultID},
+				{ID: "a"},
+				{ID: "b"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Simple #5",
+			input: []*Locale{
+				{ID: DefaultID},
+				{ID: "a", Fallback: "b"},
+				{ID: "b", Fallback: "c"},
+				{ID: "c"},
+			},
+			want: []*Locale{
+				{ID: DefaultID},
+				{ID: "c"},
+				{ID: "b", Fallback: "c"},
+				{ID: "a", Fallback: "b"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Simple #6",
+			input: []*Locale{
+				{ID: "a", Fallback: "b"},
+				{ID: "b", Fallback: "c"},
+				{ID: "c"},
+			},
+			want: []*Locale{
+				{ID: "c"},
+				{ID: "b", Fallback: "c"},
+				{ID: "a", Fallback: "b"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Cyclical dependency",
+			input: []*Locale{
+				{ID: DefaultID},
+				{ID: "a", Fallback: "b"},
+				{ID: "b", Fallback: "c"},
+				{ID: "c", Fallback: "a"},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "Cyclical dependency #2",
+			input: []*Locale{
+				{ID: DefaultID},
+				{ID: "a", Fallback: DefaultID},
+				{ID: "b", Fallback: "d"},
+				{ID: "c", Fallback: "b"},
+				{ID: "d", Fallback: "c"},
+				{ID: "e", Fallback: "a"},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "Cyclical dependency #3",
+			input: []*Locale{
+				{ID: DefaultID},
+				{ID: "a", Fallback: "b"},
+				{ID: "b", Fallback: "a"},
+				{ID: "c", Fallback: "d"},
+				{ID: "d", Fallback: "c"},
+			},
+			want:    nil,
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := FallbackSort(tt.input)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}