itube-specs 0.0.939 → 0.0.941

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +4 -2
  2. package/assets/scss/themes/dark/_theme.scss +1 -0
  3. package/assets/scss/themes/light/_theme.scss +1 -0
  4. package/components/filter/chips.vue +33 -1
  5. package/components/filter/model.vue +7 -3
  6. package/components/section/nav-links.vue +5 -1
  7. package/components/ui/pagination.vue +9 -0
  8. package/components/view/model-videos.vue +1 -1
  9. package/components/view/models.vue +1 -1
  10. package/components/view/video.vue +1 -1
  11. package/components/view/videos.vue +1 -1
  12. package/components/view/webcams.vue +7 -7
  13. package/composables/__tests__/use-model-filter-chips.test.ts +36 -0
  14. package/composables/fetch/use-fetch-playlists-by-id.ts +2 -1
  15. package/composables/fetch/use-fetch-playlists-get-videos-by-id.ts +2 -1
  16. package/composables/use-filter-pending.ts +4 -2
  17. package/composables/use-model-filter-chips.ts +24 -2
  18. package/i18n/locales/de-DE.json +34 -0
  19. package/i18n/locales/en-US.json +38 -0
  20. package/i18n/locales/es-ES.json +34 -0
  21. package/i18n/locales/fr-FR.json +34 -0
  22. package/i18n/locales/it-IT.json +34 -0
  23. package/i18n/locales/ja-JP.json +34 -0
  24. package/i18n/locales/ko-KR.json +34 -0
  25. package/i18n/locales/ru-RU.json +34 -0
  26. package/i18n/locales/zh-CN.json +34 -0
  27. package/nuxt.config.ts +17 -0
  28. package/package.json +7 -1
  29. package/pages/channels/[channel].vue +1 -1
  30. package/scripts/i18n-check.mjs +197 -0
  31. package/scripts/i18n-sheet.mjs +258 -0
  32. package/types/filter-chip-item.d.ts +4 -0
  33. package/utils/ui-pagination/__tests__/get-pagination-link.test.ts +8 -3
  34. package/utils/ui-pagination/get-pagination-link.ts +11 -3
package/README.md CHANGED
@@ -183,9 +183,11 @@ ad-конфиг), теперь читается из `runtimeConfig.public` (з
183
183
 
184
184
  - **Слой:** компоненты, composables, API-сервисы, доменные утилиты, runtime-константы/хелперы, типы,
185
185
  `lib/` (продуктовые данные), `config/` (темы, брейкпоинты), middleware (`auth`/`normalize`/`age`), SSR-плагины
186
- (`country`/`mobile`/`services`/`sentry`/`adv`), BFF-хендлеры `server/api/**` — всё site-agnostic.
186
+ (`country`/`mobile`/`services`/`sentry`/`adv`), BFF-хендлеры `server/api/**`, общие i18n-ключи
187
+ (`i18n/locales/` — навигация, сортировки, UI-обвязка) — всё site-agnostic.
187
188
  - **Приложение:** только per-site — **значения** config (флаги/реклама/локали) через `runtimeConfig.public`
188
- (слой держит пустые контейнеры-заглушки), i18n-локали, `public/` и **выбор темы** (`theme:` в
189
+ (слой держит пустые контейнеры-заглушки), свои i18n-ключи (SEO/ниша) и оверрайды общих,
190
+ `public/` и **выбор темы** (`theme:` в
189
191
  `config/site.config.ts`). Своего SCSS у приложения нет: `assets/scss/` — пустой слот-оверрайд.
190
192
  `server/` целиком в слое, включая locale-кластер.
191
193
 
@@ -713,6 +713,7 @@ $filter-chips-bg--hover: $color-zinc-800-60;
713
713
  $filter-chips-close: $color-zinc-500;
714
714
  $filter-chips-close--hover: $color-white;
715
715
  $filter-chips-label: $color-zinc-500;
716
+ $filter-chips-flag-border: $color-white-20;
716
717
  $filter-chips-row-clear-text: $color-zinc-500;
717
718
  $filter-chips-row-clear-text--hover: $color-white;
718
719
 
@@ -762,6 +762,7 @@ $filter-chips-bg--hover: $color-gray-300;
762
762
  $filter-chips-close: $color-gray-500;
763
763
  $filter-chips-close--hover: $color-gray-950;
764
764
  $filter-chips-label: $color-gray-500;
765
+ $filter-chips-flag-border: $color-gray-300;
765
766
  $filter-chips-row-clear-text: $color-gray-500;
766
767
  $filter-chips-row-clear-text--hover: $color-gray-950;
767
768
 
@@ -1,10 +1,25 @@
1
- <!-- Чипса выбранного фильтра: приглушённое название фильтра, значение и сброс по клику -->
1
+ <!-- Чипса выбранного фильтра: приглушённое название фильтра, значение и сброс по клику.
2
+
3
+ У языковой чипсы вебкамов вместо названия фильтра идёт флаг страны языка, а подпись
4
+ уже собрана переводом («Russian speaking») — поэтому у неё гасится капитализация слов.
5
+ Файла флага для языка может не быть (Китай, Корея) — тогда чипса остаётся без него. -->
2
6
  <template>
3
7
  <button
8
+ :class="{'--exact-case': item.flag}"
4
9
  class="filter-chips"
5
10
  type="button"
6
11
  @click="emit('click')"
7
12
  >
13
+ <img
14
+ v-if="item.flag && !isFlagBroken"
15
+ :src="`/icons/flags/${item.flag}.svg`"
16
+ :alt="item.flag"
17
+ class="filter-chips__flag"
18
+ width="16"
19
+ height="16"
20
+ loading="lazy"
21
+ @error="isFlagBroken = true"
22
+ >
8
23
  <span class="filter-chips__text">
9
24
  <span
10
25
  v-if="item.label && !withoutLabel"
@@ -31,6 +46,8 @@ defineProps<{
31
46
  const emit = defineEmits<{
32
47
  (eventName: 'click'): void
33
48
  }>();
49
+
50
+ const isFlagBroken = ref(false);
34
51
  </script>
35
52
 
36
53
  <style lang="scss">
@@ -58,6 +75,21 @@ const emit = defineEmits<{
58
75
  }
59
76
  }
60
77
 
78
+ .filter-chips.--exact-case {
79
+ text-transform: none;
80
+ }
81
+
82
+ // «бабл» как у флагов в тумбе модели, только мельче: круг со срезанным левым нижним углом
83
+ .filter-chips__flag {
84
+ flex-shrink: 0;
85
+ width: 16px;
86
+ height: 16px;
87
+ object-fit: cover;
88
+ border: 1px solid $filter-chips-flag-border;
89
+ border-radius: 50%;
90
+ border-end-start-radius: 0;
91
+ }
92
+
61
93
  // подпись и значение читаются одной фразой, поэтому не отдельный флекс-элемент
62
94
  .filter-chips__label {
63
95
  padding-right: 4px;
@@ -78,7 +78,7 @@ const props = defineProps<{
78
78
 
79
79
  const model = defineModel<boolean>();
80
80
 
81
- const { t, locale } = useI18n();
81
+ const { t, te, locale } = useI18n();
82
82
  const route = useRoute();
83
83
  const router = useRouter();
84
84
 
@@ -108,8 +108,12 @@ const pendingRouteLike = reactive({
108
108
  },
109
109
  });
110
110
 
111
- const { chipsItems } = useFilterChipsItems(filters, route, t, locale);
112
- const { chipsItems: pendingChipsItems } = useFilterChipsItems(filters, pendingRouteLike, t, locale);
111
+ /** Пустая строка вместо имени ключа: у сайта перевода может не быть, и тогда
112
+ * подпись чипсы соберётся без него (см. `useFilterChipsItems`). */
113
+ const translate = (key: string, params?: Record<string, unknown>) => (te(key) ? t(key, params || {}) : '');
114
+
115
+ const { chipsItems } = useFilterChipsItems(filters, route, translate, locale);
116
+ const { chipsItems: pendingChipsItems } = useFilterChipsItems(filters, pendingRouteLike, translate, locale);
113
117
 
114
118
  const quickFilters = computed(() =>
115
119
  filters.value
@@ -41,12 +41,16 @@ function linkTo(item: INavLinkItem, index: number) {
41
41
  return { path: route.path, query: query };
42
42
  }
43
43
 
44
- if (!item.key) return { query: { ...route.query } };
44
+ // путь всегда явный: локацию без него роутер разрешает сам и у нишевых разделов
45
+ // (они живут на алиасах записей) терял префикс — а из `resolve` собирается `href`,
46
+ // так что промах уезжал в разметку ссылки
47
+ if (!item.key) return { path: route.path, query: { ...route.query } };
45
48
 
46
49
  const oldQuery = { ...route.query };
47
50
  delete oldQuery['page'];
48
51
 
49
52
  return {
53
+ path: route.path,
50
54
  query: {
51
55
  ...oldQuery,
52
56
  [item.key]: item.value
@@ -1,3 +1,12 @@
1
+ <!-- Пагинация: номера страниц, «Предыдущая»/«Следующая» и путь с текущим query.
2
+
3
+ Через `Lazy…` подключать МОЖНО, но только с `hydrate-on-idle`, не с
4
+ `hydrate-on-visible`. Пагинация стоит внизу страницы, а фильтры и сортировка
5
+ меняют query из первого экрана: пока компонент не гидрирован, в DOM висит
6
+ разметка от серверного рендера, и гидрация её атрибуты не переписывает. Клик
7
+ мышью после этого работает (навигация идёт по объекту `to`), а «открыть в новой
8
+ вкладке» уходило по устаревшему `href` — без выбранных фильтров и сортировки.
9
+ Ключ `:key` от числа страниц спасал только когда их количество менялось. -->
1
10
  <template>
2
11
  <nav
3
12
  v-if="length > 1"
@@ -58,7 +58,7 @@
58
58
  :key="`pagination-${data.allPages}`"
59
59
  :total="data.allPages"
60
60
  class="view-model-videos__pagination _pagination"
61
- hydrate-on-visible
61
+ hydrate-on-idle
62
62
  />
63
63
 
64
64
  <div
@@ -108,7 +108,7 @@
108
108
  :key="`pagination-${data.allPages}`"
109
109
  :total="data.allPages"
110
110
  class="view-models__pagination _pagination"
111
- hydrate-on-visible
111
+ hydrate-on-idle
112
112
  />
113
113
  </div>
114
114
  </template>
@@ -83,7 +83,7 @@
83
83
  :total="total"
84
84
  :limit="10"
85
85
  class="view-video__pagination _pagination"
86
- hydrate-on-visible
86
+ hydrate-on-idle
87
87
  />
88
88
 
89
89
  <LazySectionSwiperCards
@@ -66,7 +66,7 @@
66
66
  :key="`pagination-${data.allPages}`"
67
67
  :total="data.allPages"
68
68
  class="view-videos__pagination _pagination"
69
- hydrate-on-visible
69
+ hydrate-on-idle
70
70
  />
71
71
 
72
72
  <SectionCategoriesIsland
@@ -108,7 +108,7 @@ query-параметрах `filter_*`. Формат провайдера при
108
108
  :total="allPages"
109
109
  arrow-icon
110
110
  class="_pagination"
111
- hydrate-on-visible
111
+ hydrate-on-idle
112
112
  />
113
113
  </div>
114
114
  </template>
@@ -213,10 +213,10 @@ const sections = computed<ISwitcherItem[]>(() => [
213
213
  ]);
214
214
 
215
215
  /**
216
- * Вход в раздел: если интерфейс не английский, сразу показываем моделей, которые
217
- * говорят на языке пользователя. Только при первом заходе за сессию — иначе
218
- * снятый пользователем фильтр возвращался бы сам, — и только если такой язык у
219
- * провайдера в выдаче есть (иначе получили бы пустую страницу).
216
+ * Вход в раздел: сразу показываем моделей, которые говорят на языке пользователя.
217
+ * Только при первом заходе за сессию — иначе снятый пользователем фильтр
218
+ * возвращался бы сам, — и только если такой язык у провайдера в выдаче есть
219
+ * (иначе получили бы пустую страницу).
220
220
  */
221
221
  const isLanguageFilterApplied = useState('webcams-language-filter', () => false);
222
222
 
@@ -225,12 +225,12 @@ onMounted(() => {
225
225
  isLanguageFilterApplied.value = true;
226
226
 
227
227
  const code = locale.value.slice(0, 2).toLowerCase();
228
- if (code === 'en' || filterCount.value > 0) return;
228
+ if (filterCount.value > 0) return;
229
229
 
230
230
  const languageFilter = filters.value.find(item => item.name === 'language');
231
231
  if (!languageFilter?.options.some(option => option.name === code)) return;
232
232
 
233
- router.replace({ query: { ...route.query, filter_language: code } });
233
+ router.replace({ path: route.path, query: { ...route.query, filter_language: code } });
234
234
  });
235
235
 
236
236
  function translate(key: string, fallback: string) {
@@ -36,6 +36,42 @@ describe('useFilterChipsItems', () => {
36
36
  expect(chipsItems.value[0].value).toEqual(['filter_hair_color']);
37
37
  });
38
38
 
39
+ it('языковая чипса: флаг страны языка и подпись переводом, без названия фильтра', () => {
40
+ const filters = ref([{
41
+ name: 'language',
42
+ title: 'Language',
43
+ options: [
44
+ { name: 'es', title: 'Spanish' },
45
+ { name: 'ru', title: 'Russian' },
46
+ ],
47
+ }]);
48
+ const route = { query: { filter_language: 'es' } };
49
+ const translate = (key: string, params?: Record<string, unknown>) => `${params?.lang} speaking (${key})`;
50
+ const { chipsItems } = useFilterChipsItems(filters, route, translate, ref('en'));
51
+
52
+ expect(chipsItems.value[0]).toEqual({
53
+ title: 'Spanish speaking (webcams.speaking)',
54
+ flag: 'es',
55
+ value: ['filter_language'],
56
+ });
57
+ });
58
+
59
+ it('языковая чипса: язык без своей страны берёт флаг по мапе, без перевода — только язык', () => {
60
+ const filters = ref([{
61
+ name: 'language',
62
+ title: 'Language',
63
+ options: [{ name: 'ja', title: 'Japanese' }],
64
+ }]);
65
+ const route = { query: { filter_language: 'ja' } };
66
+ const { chipsItems } = useFilterChipsItems(filters, route, () => '', ref('en'));
67
+
68
+ expect(chipsItems.value[0]).toEqual({
69
+ title: 'Japanese',
70
+ flag: 'jp',
71
+ value: ['filter_language'],
72
+ });
73
+ });
74
+
39
75
  it('группирует from/to фильтры', () => {
40
76
  const filters = ref([{
41
77
  name: 'age',
@@ -1,5 +1,6 @@
1
1
  import { useRoute } from 'vue-router';
2
- import { AsyncData, type Language } from '../../runtime';
2
+ import { AsyncData } from '../../runtime';
3
+ import type { Language } from '../../runtime';
3
4
  import type { IPlaylistData } from '../../types';
4
5
 
5
6
  export const useFetchPlaylistsById = async (apiMethod: (...args: any[]) => Promise<IPlaylistData>) => {
@@ -1,4 +1,5 @@
1
- import { AsyncData, type Language } from '../../runtime';
1
+ import { AsyncData } from '../../runtime';
2
+ import type { Language } from '../../runtime';
2
3
  import type { IVideoCard, PaginatedResponse } from '../../types';
3
4
  import { useRoute } from 'vue-router';
4
5
 
@@ -64,8 +64,10 @@ export function useFilterPending(options: {
64
64
  options.open.value = false;
65
65
  const query = options.encode(values.value, route.query);
66
66
 
67
- if (options.navigate === 'replace') router.replace({ query });
68
- else router.push({ query });
67
+ // путь явно: локация без него разрешается роутером, и у нишевых разделов
68
+ // (алиасы записей) префикс ниши пропадал
69
+ if (options.navigate === 'replace') router.replace({ path: route.path, query });
70
+ else router.push({ path: route.path, query });
69
71
  }
70
72
 
71
73
  return { values, count, isFiltered, set, remove, replace, reset, apply };
@@ -2,12 +2,16 @@ import type { IFilterChipItem, IModelFilter, IModelFilterOptions } from '../type
2
2
  import type { LocationQuery } from '#vue-router';
3
3
  import { getMonth } from '../runtime';
4
4
  import { buildChipParts } from '../utils/filter-chips/build-chip-parts';
5
+ import { LANG_TO_FLAG } from '../utils/webcams/lang-to-flag';
5
6
  import type { Ref } from 'vue';
6
7
 
8
+ /** Единственный фильтр-язык у нас в вебкамах: у моделей такого поля нет. */
9
+ const LANGUAGE_FILTER = 'language';
10
+
7
11
  export function useFilterChipsItems(
8
12
  filters: Ref<IModelFilter[]>,
9
13
  route: { query: LocationQuery },
10
- t: (key: string) => string,
14
+ t: (key: string, params?: Record<string, unknown>) => string,
11
15
  locale: Ref<string>
12
16
  ) {
13
17
  const chipsItems = computed<IFilterChipItem[]>(() => {
@@ -83,6 +87,22 @@ export function useFilterChipsItems(
83
87
  const getValueText = (items: string[]) =>
84
88
  [...new Set(items.map(getValue).filter(v => v !== undefined && v !== null && v !== ''))].join(' - ');
85
89
 
90
+ /**
91
+ * Язык — не просто значение фильтра, а свойство модели: в облаке он читается
92
+ * как «Russian speaking» и с флагом страны языка, без приглушённой подписи
93
+ * фильтра (по макету раздела вебкамов). Флаг лежит файлом по коду СТРАНЫ, а
94
+ * фильтр приходит языком — переводим по мапе; нет файла — чипса просто без
95
+ * флага. Если у сайта ключа перевода ещё нет, остаётся название языка.
96
+ */
97
+ const buildLanguageChip = (valueText: string) => {
98
+ const code = String(route.query[`filter_${LANGUAGE_FILTER}`] || '').toLowerCase();
99
+
100
+ return {
101
+ title: t('webcams.speaking', { lang: valueText }) || valueText,
102
+ flag: LANG_TO_FLAG[code] || code,
103
+ };
104
+ };
105
+
86
106
  return groups
87
107
  .map(item => {
88
108
  const filter = findFilter(item);
@@ -90,7 +110,9 @@ export function useFilterChipsItems(
90
110
  const valueText = getValueText(item);
91
111
  if (!valueText) return null;
92
112
  return {
93
- ...buildChipParts(filter.title, valueText),
113
+ ...(filter.name === LANGUAGE_FILTER
114
+ ? buildLanguageChip(valueText)
115
+ : buildChipParts(filter.title, valueText)),
94
116
  value: item.map(
95
117
  sub => `filter_${sub.replace(/ /g, '_')}`
96
118
  ),
@@ -0,0 +1,34 @@
1
+ {
2
+ "navigation": {
3
+ "main": "Hauptmenü",
4
+ "videos": "Videos",
5
+ "categories": "Kategorien",
6
+ "top_categories": "Top-Kategorien",
7
+ "channels": "Kanäle",
8
+ "live_sex": "Live-Sex",
9
+ "history": "Verlauf",
10
+ "models": "Models",
11
+ "moments": "Momente",
12
+ "playlists": "Playlists",
13
+ "network": "Netzwerk",
14
+ "profile": "Profil",
15
+ "settings": "Einstellungen",
16
+ "my_playlists": "Meine Playlists",
17
+ "back_to_main_page": "Zur Hauptseite zurück",
18
+ "watch_later": "Später ansehen"
19
+ },
20
+ "sort": {
21
+ "network": "Netzwerk",
22
+ "popular": "Beliebt",
23
+ "trending": "Angesagt",
24
+ "new_videos": "Neue Videos",
25
+ "new": "Neu",
26
+ "new_short": "Neu",
27
+ "recommended": "Empfohlen",
28
+ "az": "A-Z",
29
+ "most_videos": "Meiste Videos",
30
+ "last_updated": "Zuletzt aktualisiert",
31
+ "longest": "Längste",
32
+ "longest_short": "Längste"
33
+ }
34
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "navigation": {
3
+ "main": "Main",
4
+ "videos": "Videos",
5
+ "categories": "Categories",
6
+ "top_categories": "Top categories",
7
+ "channels": "Channels",
8
+ "live_sex": "Live Sex",
9
+ "webcams": "Webcams",
10
+ "history": "History",
11
+ "models": "Models",
12
+ "moments": "Moments",
13
+ "playlists": "Playlists",
14
+ "network": "Network",
15
+ "profile": "Profile",
16
+ "settings": "Settings",
17
+ "my_playlists": "My playlists",
18
+ "library": "Library",
19
+ "back_to_main_page": "Back to Main page",
20
+ "watch_later": "Watch Later"
21
+ },
22
+ "sort": {
23
+ "recent": "Recent",
24
+ "relevant": "Relevant",
25
+ "network": "Network",
26
+ "popular": "Popular",
27
+ "trending": "Trending",
28
+ "new_videos": "New videos",
29
+ "new": "New",
30
+ "new_short": "New",
31
+ "recommended": "Recommended",
32
+ "az": "A-Z",
33
+ "most_videos": "Most videos",
34
+ "last_updated": "Last updated",
35
+ "longest": "Longest",
36
+ "longest_short": "Longest"
37
+ }
38
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "navigation": {
3
+ "main": "Principal",
4
+ "videos": "Vídeos",
5
+ "categories": "Categorías",
6
+ "top_categories": "Categorías principales",
7
+ "channels": "Canales",
8
+ "models": "Modelos",
9
+ "moments": "Momentos",
10
+ "playlists": "Listas de reproducción",
11
+ "network": "Red",
12
+ "profile": "Perfil",
13
+ "settings": "Configuraciones",
14
+ "my_playlists": "Mis listas de reproducción",
15
+ "back_to_main_page": "Volver a la página principal",
16
+ "history": "Historial",
17
+ "live_sex": "Sexo en vivo",
18
+ "watch_later": "Ver después"
19
+ },
20
+ "sort": {
21
+ "network": "Red",
22
+ "popular": "Popular",
23
+ "trending": "Tendencia",
24
+ "new_videos": "Vídeos nuevos",
25
+ "new": "Nuevo",
26
+ "new_short": "Nuevo",
27
+ "recommended": "Recomendado",
28
+ "az": "A-Z",
29
+ "most_videos": "Más videos",
30
+ "last_updated": "Última actualización",
31
+ "longest": "Más largo",
32
+ "longest_short": "Largos"
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "navigation": {
3
+ "main": "Principal",
4
+ "videos": "Vidéos",
5
+ "categories": "Catégories",
6
+ "top_categories": "Catégories principales",
7
+ "channels": "Chaînes",
8
+ "models": "Modèles",
9
+ "moments": "Moments",
10
+ "playlists": "Listes de lecture",
11
+ "network": "Réseau",
12
+ "profile": "Profil",
13
+ "settings": "Paramètres",
14
+ "my_playlists": "Mes listes de lecture",
15
+ "back_to_main_page": "Retour à la page principale",
16
+ "history": "Historique",
17
+ "live_sex": "Sexe en direct",
18
+ "watch_later": "Regarder plus tard"
19
+ },
20
+ "sort": {
21
+ "network": "Réseau",
22
+ "popular": "Populaire",
23
+ "trending": "Tendance",
24
+ "new_videos": "Nouvelles vidéos",
25
+ "new": "Nouveau",
26
+ "new_short": "Nouveau",
27
+ "recommended": "Recommandé",
28
+ "az": "A-Z",
29
+ "most_videos": "Plus de vidéos",
30
+ "last_updated": "Dernière mise à jour",
31
+ "longest": "Les plus longues",
32
+ "longest_short": "Longues"
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "navigation": {
3
+ "main": "Principale",
4
+ "videos": "Video",
5
+ "categories": "Categorie",
6
+ "top_categories": "Categorie principali",
7
+ "channels": "Canali",
8
+ "models": "Modelli",
9
+ "moments": "Momenti",
10
+ "playlists": "Playlist",
11
+ "network": "Rete",
12
+ "profile": "Profilo",
13
+ "settings": "Impostazioni",
14
+ "my_playlists": "Le mie playlist",
15
+ "back_to_main_page": "Torna alla pagina principale",
16
+ "history": "Cronologia",
17
+ "live_sex": "Sesso dal vivo",
18
+ "watch_later": "Guarda dopo"
19
+ },
20
+ "sort": {
21
+ "network": "Rete",
22
+ "popular": "Popolare",
23
+ "trending": "Tendenza",
24
+ "new_videos": "Nuovi video",
25
+ "new": "Nuovo",
26
+ "new_short": "Nuovo",
27
+ "recommended": "Raccomandato",
28
+ "az": "A-Z",
29
+ "most_videos": "Più video",
30
+ "last_updated": "Ultimo aggiornamento",
31
+ "longest": "Più lungo",
32
+ "longest_short": "Lunghi"
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "navigation": {
3
+ "main": "メイン",
4
+ "videos": "ビデオ",
5
+ "top_categories": "トップカテゴリ",
6
+ "categories": "カテゴリ",
7
+ "channels": "チャンネル",
8
+ "models": "モデル",
9
+ "moments": "瞬間",
10
+ "playlists": "プレイリスト",
11
+ "network": "ネットワーク",
12
+ "profile": "プロフィール",
13
+ "settings": "設定",
14
+ "my_playlists": "マイプレイリスト",
15
+ "back_to_main_page": "メインページに戻る",
16
+ "history": "履歴",
17
+ "live_sex": "ライブ",
18
+ "watch_later": "後で見る"
19
+ },
20
+ "sort": {
21
+ "network": "ネットワーク",
22
+ "popular": "人気",
23
+ "trending": "トレンド",
24
+ "new_videos": "新着ビデオ",
25
+ "new": "新しい",
26
+ "new_short": "新しい",
27
+ "recommended": "おすすめ",
28
+ "az": "A-Z",
29
+ "most_videos": "ビデオ数最多",
30
+ "last_updated": "最終更新",
31
+ "longest": "最長",
32
+ "longest_short": "最長"
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "navigation": {
3
+ "main": "메인",
4
+ "videos": "비디오",
5
+ "categories": "카테고리",
6
+ "top_categories": "상위 카테고리",
7
+ "channels": "채널",
8
+ "models": "모델",
9
+ "moments": "순간",
10
+ "playlists": "재생목록",
11
+ "network": "네트워크",
12
+ "profile": "프로필",
13
+ "settings": "설정",
14
+ "my_playlists": "내 재생목록",
15
+ "back_to_main_page": "메인 페이지로 돌아가기",
16
+ "history": "기록",
17
+ "live_sex": "라이브",
18
+ "watch_later": "나중에 보기"
19
+ },
20
+ "sort": {
21
+ "network": "네트워크",
22
+ "popular": "인기",
23
+ "trending": "트렌드",
24
+ "new_videos": "신규 비디오",
25
+ "new": "새로운",
26
+ "new_short": "새로운",
27
+ "recommended": "추천",
28
+ "az": "A-Z",
29
+ "most_videos": "동영상 수 최다",
30
+ "last_updated": "마지막 업데이트",
31
+ "longest": "가장 긴",
32
+ "longest_short": "가장 긴"
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "navigation": {
3
+ "main": "Главная",
4
+ "videos": "Видео",
5
+ "categories": "Категории",
6
+ "top_categories": "Топ категории",
7
+ "channels": "Каналы",
8
+ "live_sex": "Прямой эфир",
9
+ "history": "История",
10
+ "models": "Модели",
11
+ "moments": "Моменты",
12
+ "playlists": "Плейлисты",
13
+ "network": "Сеть",
14
+ "profile": "Профиль",
15
+ "settings": "Настройки",
16
+ "my_playlists": "Мои плейлисты",
17
+ "back_to_main_page": "Вернуться на главную",
18
+ "watch_later": "Смотреть позже"
19
+ },
20
+ "sort": {
21
+ "network": "Сеть",
22
+ "popular": "Популярное",
23
+ "trending": "В тренде",
24
+ "new_videos": "Новые видео",
25
+ "new": "Новое",
26
+ "new_short": "Новые",
27
+ "recommended": "Рекомендуемые",
28
+ "az": "A-Z",
29
+ "most_videos": "Больше видео",
30
+ "last_updated": "Последние",
31
+ "longest": "Самые длинные",
32
+ "longest_short": "Длинные"
33
+ }
34
+ }