itube-specs 0.0.869 → 0.0.870

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.
@@ -27,8 +27,8 @@
27
27
  @click="showLanguageMenu"
28
28
  >
29
29
  <img
30
- v-if="clientCountryIcon"
31
- :src="`/icons/flags/${clientCountryIcon}.svg`"
30
+ v-if="clientCountryFlag"
31
+ :src="clientCountryFlag"
32
32
  :alt="clientCountryIcon"
33
33
  class="layout-header__language-icon"
34
34
  width="24"
@@ -96,14 +96,8 @@
96
96
  import { AuthSteps } from '../../runtime';
97
97
  import { onClickOutside } from '@vueuse/core';
98
98
  import { AuthorizationApiService } from '../../services/api/authorization.service';
99
- import { isoCodeMap } from '../../utils/iso-code-map';
100
99
 
101
- const clientCountryCode = useState<string>('clientCountryCode', () => '');
102
- const clientCountryIcon = computed(() => {
103
- const code = clientCountryCode.value;
104
- if (!code) return '';
105
- return isoCodeMap[code] || code;
106
- });
100
+ const { icon: clientCountryIcon, flagSrc: clientCountryFlag } = useClientCountry();
107
101
 
108
102
  const burgerShow = ref(false);
109
103
  const showLanguage = ref(false);
@@ -42,9 +42,9 @@
42
42
  >
43
43
  <summary class="layout-language__summary">
44
44
  <img
45
- v-if="clientCountryIcon"
45
+ v-if="clientCountryFlag"
46
46
  :alt="clientCountryName"
47
- :src="`/icons/flags/${clientCountryIcon}.svg`"
47
+ :src="clientCountryFlag"
48
48
  class="layout-language__icon"
49
49
  width="20"
50
50
  height="20"
@@ -109,8 +109,6 @@
109
109
 
110
110
  <script setup lang="ts">
111
111
  import type { ISelectItem } from '../../types';
112
- import { isoCodeMap } from '../../utils/iso-code-map';
113
- import { DictionariesApiService } from '../../services/api/dictionaries.service';
114
112
 
115
113
  const { locale, locales } = useI18n();
116
114
  const switchLocalePath = useSwitchLocalePath();
@@ -119,22 +117,14 @@ defineProps<{
119
117
  iconSize: string
120
118
  }>();
121
119
 
122
- const countriesItems = useState<ISelectItem[]>('countriesItems', () => []);
123
- const clientCountryCode = useState<string>('clientCountryCode', () => '');
124
-
125
- const clientCountryIcon = computed(() => {
126
- const code = clientCountryCode.value;
127
- if (!code) return '';
128
- return isoCodeMap[code] || code;
129
- });
130
-
131
- const clientCountryNameState = useState<string>('clientCountryName', () => '');
132
-
133
- const clientCountryName = computed(() => {
134
- if (!clientCountryIcon.value) return '';
135
- const fromList = countriesItems.value.find(item => item.icon === clientCountryIcon.value)?.value;
136
- return fromList || clientCountryNameState.value || '';
137
- });
120
+ const {
121
+ icon: clientCountryIcon,
122
+ name: clientCountryName,
123
+ flagSrc: clientCountryFlag,
124
+ countriesItems,
125
+ setCountry,
126
+ loadCountries,
127
+ } = useClientCountry();
138
128
 
139
129
  const isCountryOpen = ref(false);
140
130
  const countryValue = ref('');
@@ -144,26 +134,6 @@ const filteredCountries = computed(() => {
144
134
  return countriesItems.value.filter(item => item.title.toLowerCase().includes(query));
145
135
  });
146
136
 
147
- const isCountryLoading = ref(false);
148
-
149
- // Список стран не приходит в payload (см. plugins/countries.server.ts) — догружаем
150
- // его лениво на клиенте при первом открытии селектора.
151
- async function loadCountries() {
152
- if (countriesItems.value.length || isCountryLoading.value) return;
153
- isCountryLoading.value = true;
154
- try {
155
- const data = await DictionariesApiService.getCountries();
156
- countriesItems.value = (data?.countries ?? []).map((item) => {
157
- const iso = item.isoCodes[0].toLowerCase();
158
- return { title: item.title, value: item.title, icon: isoCodeMap[iso] || iso, key: iso };
159
- });
160
- } catch {
161
- // оставляем пустым — следующее открытие попробует снова
162
- } finally {
163
- isCountryLoading.value = false;
164
- }
165
- }
166
-
167
137
  function onCountryToggle(e: Event) {
168
138
  isCountryOpen.value = (e.target as HTMLDetailsElement).open;
169
139
  if (isCountryOpen.value) loadCountries();
@@ -191,18 +161,10 @@ function changeLocale(locale: string) {
191
161
  window.location.href = switchLocalePath(locale);
192
162
  }
193
163
 
194
- const COUNTRY_COOKIE_OPTS = { maxAge: 60 * 60 * 24 * 365, path: '/', secure: true, sameSite: 'lax' as const };
195
- const countryCookie = useCookie('clientCountryCode', COUNTRY_COOKIE_OPTS);
196
- const countryNameCookie = useCookie('clientCountryName', COUNTRY_COOKIE_OPTS);
197
- const countryNameCodeCookie = useCookie('clientCountryNameCode', COUNTRY_COOKIE_OPTS);
198
-
199
- function changeCountry(item: ISelectItem) {
200
- const code = item.key || '';
201
- clientCountryCode.value = code;
202
- clientCountryNameState.value = item.value;
203
- countryCookie.value = code;
204
- countryNameCookie.value = item.value;
205
- countryNameCodeCookie.value = code;
164
+ async function changeCountry(item: ISelectItem) {
165
+ setCountry(item);
166
+ // куку пишет вотчер useCookie — ждём флаш, иначе навигация может срезать запись
167
+ await nextTick();
206
168
  window.location.href = '/';
207
169
  }
208
170
  </script>
@@ -0,0 +1,121 @@
1
+ // @vitest-environment nuxt
2
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
3
+ import { useClientCountry } from '../use-client-country';
4
+
5
+ const getCountries = vi.fn();
6
+
7
+ vi.mock('../../services/api/dictionaries.service', () => ({
8
+ DictionariesApiService: { getCountries: () => getCountries() },
9
+ }));
10
+
11
+ const setCode = (code: string) => useState<string>('clientCountryCode', () => '').value = code;
12
+ const setResolvedName = (name: string) => useState<string>('clientCountryName', () => '').value = name;
13
+
14
+ describe('useClientCountry', () => {
15
+ beforeEach(() => {
16
+ useNuxtApp().payload.state = {};
17
+ getCountries.mockReset();
18
+ });
19
+
20
+ it('без кода не отдаёт ни флаг, ни имя', () => {
21
+ const { icon, name, flagSrc } = useClientCountry();
22
+
23
+ expect(icon.value).toBe('');
24
+ expect(name.value).toBe('');
25
+ expect(flagSrc.value).toBe('');
26
+ });
27
+
28
+ it('строит иконку и путь к флагу из кода', () => {
29
+ setCode('ad');
30
+ setResolvedName('Andorra');
31
+ const { icon, flagSrc } = useClientCountry();
32
+
33
+ expect(icon.value).toBe('ad');
34
+ expect(flagSrc.value).toBe('/icons/flags/ad.svg');
35
+ });
36
+
37
+ it('нормализует код: регистр и пробелы', () => {
38
+ setCode(' AD ');
39
+ setResolvedName('Andorra');
40
+ const { icon, flagSrc } = useClientCountry();
41
+
42
+ expect(icon.value).toBe('ad');
43
+ expect(flagSrc.value).toBe('/icons/flags/ad.svg');
44
+ });
45
+
46
+ it('учитывает страны, где код флага отличается от ISO-кода', () => {
47
+ setCode('cn');
48
+ setResolvedName('China');
49
+ const { icon, flagSrc } = useClientCountry();
50
+
51
+ expect(icon.value).toBe('zh');
52
+ expect(flagSrc.value).toBe('/icons/flags/zh.svg');
53
+ });
54
+
55
+ it('не показывает флаг для служебного кода, которого нет в словаре', () => {
56
+ // Cloudflare отдаёт XX для неизвестного IP и T1 для Tor — файла такого флага нет
57
+ setCode('xx');
58
+ const { icon, name, flagSrc } = useClientCountry();
59
+
60
+ expect(icon.value).toBe('xx');
61
+ expect(name.value).toBe('');
62
+ expect(flagSrc.value).toBe('');
63
+ });
64
+
65
+ it('имя следует за кодом и не может остаться от другой страны', async () => {
66
+ setCode('ad');
67
+ // имя, доставшееся от предыдущей страны — именно этот рассинхрон ловим
68
+ setResolvedName('USA');
69
+ getCountries.mockResolvedValue({
70
+ countries: [
71
+ { title: 'Andorra', isoCodes: ['AD', 'AND'] },
72
+ { title: 'USA', isoCodes: ['US', 'USA'] },
73
+ ],
74
+ });
75
+
76
+ const { name, flagSrc, loadCountries } = useClientCountry();
77
+ await loadCountries();
78
+
79
+ expect(name.value).toBe('Andorra');
80
+ expect(flagSrc.value).toBe('/icons/flags/ad.svg');
81
+ });
82
+
83
+ it('до загрузки списка берёт имя, отрезолвленное на сервере', () => {
84
+ setCode('ad');
85
+ setResolvedName('Andorra');
86
+ const { name } = useClientCountry();
87
+
88
+ expect(name.value).toBe('Andorra');
89
+ });
90
+
91
+ it('выбор страны меняет и код, и имя', () => {
92
+ setCode('ad');
93
+ setResolvedName('Andorra');
94
+ const { code, name, flagSrc, setCountry } = useClientCountry();
95
+
96
+ setCountry({ title: 'USA', value: 'USA', icon: 'us', key: 'us' });
97
+
98
+ expect(code.value).toBe('us');
99
+ expect(name.value).toBe('USA');
100
+ expect(flagSrc.value).toBe('/icons/flags/us.svg');
101
+ });
102
+
103
+ it('не перезапрашивает список, если он уже загружен', async () => {
104
+ getCountries.mockResolvedValue({ countries: [{ title: 'Andorra', isoCodes: ['AD'] }] });
105
+ const { loadCountries } = useClientCountry();
106
+
107
+ await loadCountries();
108
+ await loadCountries();
109
+
110
+ expect(getCountries).toHaveBeenCalledTimes(1);
111
+ });
112
+
113
+ it('падение запроса оставляет список пустым — следующее открытие попробует снова', async () => {
114
+ getCountries.mockRejectedValue(new Error('500'));
115
+ const { countriesItems, loadCountries } = useClientCountry();
116
+
117
+ await loadCountries();
118
+
119
+ expect(countriesItems.value).toEqual([]);
120
+ });
121
+ });
@@ -0,0 +1,73 @@
1
+ import type { ISelectItem } from '../types';
2
+ import { isoCodeMap } from '../utils/iso-code-map';
3
+ import { DictionariesApiService } from '../services/api/dictionaries.service';
4
+
5
+ /** Ручной выбор страны переживает перезагрузку год — читает его server/middleware/country.ts */
6
+ const COOKIE_OPTS = { maxAge: 60 * 60 * 24 * 365, path: '/', secure: true, sameSite: 'lax' as const };
7
+
8
+ /**
9
+ * Единый источник страны клиента: ISO-код (гео-заголовок прокси или ручной выбор
10
+ * в селекторе), иконка флага и локализованное имя.
11
+ *
12
+ * Имя всегда резолвится ОТ ТЕКУЩЕГО кода — своего кеша у него нет. Раньше имя
13
+ * лежало в отдельной куке `clientCountryName` рядом с парной `clientCountryNameCode`,
14
+ * и пара могла разъехаться: заголовок писал одну страну, флаг и селектор показывали
15
+ * другую (реальный кейс на проде: `clientCountryName=USA` при коде `ad`).
16
+ */
17
+ export const useClientCountry = () => {
18
+ const code = useState<string>('clientCountryCode', () => '');
19
+ const resolvedName = useState<string>('clientCountryName', () => '');
20
+ const countriesItems = useState<ISelectItem[]>('countriesItems', () => []);
21
+ const codeCookie = useCookie<string | null>('clientCountryCode', COOKIE_OPTS);
22
+
23
+ /** ISO-код → имя файла флага (у части стран код локали отличается от кода страны) */
24
+ const toIcon = (value: string) => {
25
+ const iso = value.trim().toLowerCase();
26
+ return iso ? isoCodeMap[iso] || iso : '';
27
+ };
28
+
29
+ const icon = computed(() => toIcon(code.value));
30
+
31
+ // Список стран догружается лениво на клиенте (см. layout/language.vue) — если он
32
+ // уже есть, имя берём из него: там оно точно совпадает с текущим кодом.
33
+ const name = computed(() => {
34
+ if (!icon.value) return '';
35
+ return countriesItems.value.find(item => item.icon === icon.value)?.title || resolvedName.value;
36
+ });
37
+
38
+ // Флаг только для страны, которую смогли назвать: гео-заголовок умеет отдавать
39
+ // служебные коды (Cloudflare `XX` для неизвестного IP и `T1` для Tor, MaxMind `AP`),
40
+ // их нет ни в словаре стран, ни среди файлов флагов — вышла бы битая картинка.
41
+ const flagSrc = computed(() => (icon.value && name.value ? `/icons/flags/${icon.value}.svg` : ''));
42
+
43
+ /** Ручной выбор страны в селекторе: наружу переживает перезагрузку только код. */
44
+ const setCountry = (item: ISelectItem) => {
45
+ code.value = item.key || '';
46
+ resolvedName.value = item.title;
47
+ codeCookie.value = code.value || null;
48
+ };
49
+
50
+ const isLoading = ref(false);
51
+
52
+ /**
53
+ * Полный список стран в payload не кладётся (см. plugins/countries.server.ts) —
54
+ * догружаем лениво на клиенте, когда открыли селектор.
55
+ */
56
+ const loadCountries = async () => {
57
+ if (countriesItems.value.length || isLoading.value) return;
58
+ isLoading.value = true;
59
+ try {
60
+ const data = await DictionariesApiService.getCountries();
61
+ countriesItems.value = (data?.countries ?? []).map((item) => {
62
+ const iso = item.isoCodes[0].toLowerCase();
63
+ return { title: item.title, value: item.title, icon: toIcon(iso), key: iso };
64
+ });
65
+ } catch {
66
+ // оставляем пустым — следующее открытие попробует снова
67
+ } finally {
68
+ isLoading.value = false;
69
+ }
70
+ };
71
+
72
+ return { code, icon, name, flagSrc, countriesItems, setCountry, loadCountries };
73
+ };
@@ -3,6 +3,16 @@ import type { Ref } from 'vue';
3
3
  import { useRoute } from 'vue-router';
4
4
  import type { IChipsItem } from '../types';
5
5
 
6
+ type MetaOptions = {
7
+ /**
8
+ * Подменяет i18n-ключ заголовка (по умолчанию 'h1') — вызывается с фактической
9
+ * сортировкой, потому что варианты заголовка есть не у каждой из них.
10
+ * Нужен, когда подставляемого текста может не быть и в шаблоне остаётся
11
+ * висячий предлог (`… Videos in ` без страны).
12
+ */
13
+ h1Key?: (sortType: string) => string
14
+ };
15
+
6
16
  /**
7
17
  * Генерирует реактивные meta (title, description) и h1 с учётом сортировки, страницы и i18n-ключей.
8
18
  * @param t - функция перевода (i18n)
@@ -13,8 +23,9 @@ import type { IChipsItem } from '../types';
13
23
  * @param secondText - второй подставляемый текст
14
24
  * @param thirdText - третий подставляемый текст
15
25
  * @param fourthText - четвёртый подставляемый текст
26
+ * @param options - уточнения (подмена ключа h1)
16
27
  */
17
- export function useMeta(t: (key: string, params?: Record<string, unknown>) => string, page: string, brandName: string, sortOptions?: IChipsItem[], text?: Ref<string>, secondText?: Ref<string>, thirdText?: Ref<string>, fourthText?: Ref<string>) {
28
+ export function useMeta(t: (key: string, params?: Record<string, unknown>) => string, page: string, brandName: string, sortOptions?: IChipsItem[], text?: Ref<string>, secondText?: Ref<string>, thirdText?: Ref<string>, fourthText?: Ref<string>, options?: MetaOptions) {
18
29
  const route = useRoute();
19
30
 
20
31
  const sortType = computed(() => {
@@ -46,7 +57,7 @@ export function useMeta(t: (key: string, params?: Record<string, unknown>) => st
46
57
  }
47
58
 
48
59
  const metaTitle = computed(() => getPath('title'));
49
- const h1 = computed(() => getPath('h1'));
60
+ const h1 = computed(() => getPath(options?.h1Key?.(sortType.value) || 'h1'));
50
61
 
51
62
  const meta = computed(() => {
52
63
  const description = getPath('meta_description');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.869",
4
+ "version": "0.0.870",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
package/pages/index.vue CHANGED
@@ -21,9 +21,8 @@ import { convertQueryCategories, convertString, AsyncData } from '../runtime';
21
21
  import { VideosApiService } from '../services/api/videos.service';
22
22
  import { ModelsApiService } from '../services/api/models.service';
23
23
  import { CategoriesApiService } from '../services/api/categories.service';
24
- import { isoCodeMap } from '../utils/iso-code-map';
25
24
  import { sortItemsDefault } from '../lib';
26
- import type { ISelectItem, IChipsItem } from '../types';
25
+ import type { IChipsItem } from '../types';
27
26
 
28
27
  const { t } = useI18n();
29
28
 
@@ -62,28 +61,28 @@ const routeQueryCategories = computed(() => {
62
61
  return '';
63
62
  });
64
63
 
65
- const clientCountryCode = useState<string>('clientCountryCode', () => '');
66
- const countriesItems = useState<ISelectItem[]>('countriesItems', () => []);
67
- const clientCountryNameState = useState<string>('clientCountryName', () => '');
64
+ const clientCountry = useClientCountry();
68
65
 
69
- const countryName = computed(() => {
70
- if (route.query['sort']) return '';
71
- const code = clientCountryCode.value;
72
- if (!code) return '';
73
- const icon = isoCodeMap[code] || code;
74
- const fromList = countriesItems.value.find(item => item.icon === icon)?.title;
75
- return fromList || clientCountryNameState.value || '';
76
- });
66
+ // Страна в заголовке уместна только на дефолтной сортировке (в остальных шаблонах
67
+ // её нет) — при явной сортировке гасим и подпись, и флаг.
68
+ const isCountryShown = computed(() => !route.query['sort'] && !!clientCountry.name.value);
77
69
 
78
- const { meta, h1 } = useMeta(t, pageName, useAppConfig().brandName, sortItemsDefault, routeQueryCategories, countryName);
70
+ const countryName = computed(() => (isCountryShown.value ? clientCountry.name.value : ''));
79
71
 
80
- const countryFlagImg = computed(() => {
81
- if (route.query['sort']) return undefined;
82
- const code = clientCountryCode.value;
83
- if (!code) return undefined;
84
- const icon = isoCodeMap[code] || code;
85
- return `/icons/flags/${icon}.svg`;
86
- });
72
+ const { meta, h1 } = useMeta(
73
+ t,
74
+ pageName,
75
+ useAppConfig().brandName,
76
+ sortItemsDefault,
77
+ routeQueryCategories,
78
+ countryName,
79
+ undefined,
80
+ undefined,
81
+ // Страну не определили — берём заголовок без неё, иначе останется висячий предлог.
82
+ { h1Key: sortType => (sortType === 'trending' && !countryName.value ? 'h1_no_country' : 'h1') },
83
+ );
84
+
85
+ const countryFlagImg = computed(() => (isCountryShown.value ? clientCountry.flagSrc.value : undefined));
87
86
 
88
87
  watchEffect(() => {
89
88
  useHead(meta.value);
@@ -1,45 +1,32 @@
1
1
  // SSR-плагин: по ISO-коду клиента резолвит имя страны в useState('clientCountryName')
2
2
  // для гео-заголовка. Полный список стран в payload не кладётся (его лениво догружает
3
- // селектор языка на клиенте). Результат кэшируется в куку и в память процесса.
4
- import type { ICountries, ISelectItem } from '../types';
5
- import { isoCodeMap } from '../utils/iso-code-map';
3
+ // селектор языка на клиенте). Результат кэшируется в память процесса.
4
+ import type { ICountries } from '../types';
6
5
 
7
- const COOKIE_OPTS = { maxAge: 60 * 60 * 24 * 365, path: '/', secure: true, sameSite: 'lax' as const };
6
+ /** Опции легаси-кук — нужны только чтобы их погасить (см. ниже) */
7
+ const LEGACY_COOKIE_OPTS = { path: '/', secure: true, sameSite: 'lax' as const };
8
8
 
9
- type CountriesCache = {
10
- items: ISelectItem[];
11
- nameByIso: Map<string, string>;
12
- };
9
+ const cache = new Map<string, Promise<Map<string, string> | null>>();
13
10
 
14
- const cache = new Map<string, Promise<CountriesCache | null>>();
15
-
16
- function buildCache(data: ICountries): CountriesCache {
17
- const items: ISelectItem[] = [];
11
+ function buildNameByIso(data: ICountries): Map<string, string> {
18
12
  const nameByIso = new Map<string, string>();
19
13
 
20
14
  for (const item of data.countries) {
21
- const iso = item.isoCodes[0].toLowerCase();
22
- items.push({
23
- title: item.title,
24
- value: item.title,
25
- icon: isoCodeMap[iso] || iso,
26
- key: iso,
27
- });
28
15
  for (const code of item.isoCodes) {
29
16
  nameByIso.set(code.toLowerCase(), item.title);
30
17
  }
31
18
  }
32
- return { items, nameByIso };
19
+ return nameByIso;
33
20
  }
34
21
 
35
- function getCountries(language: string, domain: string): Promise<CountriesCache | null> {
22
+ function getNameByIso(language: string, domain: string): Promise<Map<string, string> | null> {
36
23
  const key = `${language}|${domain}`;
37
24
  let promise = cache.get(key);
38
25
  if (!promise) {
39
26
  promise = $fetch<ICountries>('/api/dictionaries/countries', {
40
27
  headers: { 'x-language': language, 'x-domain': domain },
41
28
  })
42
- .then(data => (data?.countries?.length ? buildCache(data) : null))
29
+ .then(data => (data?.countries?.length ? buildNameByIso(data) : null))
43
30
  .catch(() => {
44
31
  cache.delete(key);
45
32
  return null;
@@ -51,34 +38,29 @@ function getCountries(language: string, domain: string): Promise<CountriesCache
51
38
 
52
39
  export default defineNuxtPlugin(async () => {
53
40
  const event = useRequestEvent();
54
- const code = (event?.context?.clientCountryCode as string | undefined) || '';
41
+ const code = ((event?.context?.clientCountryCode as string | undefined) || '').trim().toLowerCase();
55
42
 
56
43
  const countryName = useState<string>('clientCountryName', () => '');
57
44
 
45
+ // Имя страны больше не кешируется в куках: пара `clientCountryName` +
46
+ // `clientCountryNameCode` умела разъезжаться с кодом страны (заголовок писал одну
47
+ // страну, флаг и селектор показывали другую), а выигрыш был только в одном
48
+ // сетевом запросе на холодный процесс. Старые куки гасим, чтобы не висели год.
49
+ const legacyName = useCookie<string | null>('clientCountryName', LEGACY_COOKIE_OPTS);
50
+ const legacyNameCode = useCookie<string | null>('clientCountryNameCode', LEGACY_COOKIE_OPTS);
51
+ if (legacyName.value) legacyName.value = null;
52
+ if (legacyNameCode.value) legacyNameCode.value = null;
53
+
58
54
  // Резолвим только имя текущей страны для гео-заголовка. Полный список стран
59
55
  // НЕ кладём в useState (не сериализуем в payload) — language.vue догружает
60
56
  // его лениво на клиенте при открытии селектора.
61
57
  if (!code) return;
62
58
 
63
- const codeCookie = useCookie<string>('clientCountryNameCode', COOKIE_OPTS);
64
- const nameCookie = useCookie<string>('clientCountryName', COOKIE_OPTS);
65
-
66
- // Тёплая кука → имя уже известно, серверный фетч стран не нужен.
67
- if (nameCookie.value && codeCookie.value === code) {
68
- countryName.value = nameCookie.value;
69
- return;
70
- }
71
-
72
59
  const language = useRequestHeaders(['x-language'])['x-language'] ?? '';
73
60
  const domain = useRuntimeConfig().public.xDomain as string;
74
61
 
75
- const result = await getCountries(language, domain);
76
- if (!result) return;
62
+ const nameByIso = await getNameByIso(language, domain);
63
+ if (!nameByIso) return;
77
64
 
78
- const name = result.nameByIso.get(code.toLowerCase());
79
- if (name) {
80
- countryName.value = name;
81
- nameCookie.value = name;
82
- codeCookie.value = code;
83
- }
65
+ countryName.value = nameByIso.get(code) || '';
84
66
  });
@@ -1,10 +1,10 @@
1
1
  // Определяет страну клиента и кладёт ISO-код в event.context.clientCountryCode:
2
- // сначала из cookie `clientCountryCode`, иначе из заголовков прокси (Cloudflare
3
- // `cf-ipcountry` / `x-country-code` / `x-country`).
2
+ // сначала из cookie `clientCountryCode` (ручной выбор в селекторе), иначе из
3
+ // заголовков прокси (Cloudflare `cf-ipcountry` / `x-country-code` / `x-country`).
4
4
  export default defineEventHandler((event) => {
5
5
  const cookie = getCookie(event, 'clientCountryCode');
6
6
  if (cookie) {
7
- event.context.clientCountryCode = cookie;
7
+ event.context.clientCountryCode = cookie.trim().toLowerCase();
8
8
  return;
9
9
  }
10
10