itube-specs 0.0.946 → 0.0.948
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.
- package/components/card/category.vue +2 -1
- package/components/card/model-mini.vue +5 -3
- package/components/category/list.vue +2 -1
- package/components/info/grid.vue +2 -1
- package/components/info/main.vue +19 -3
- package/components/layout/language.vue +30 -3
- package/composables/__tests__/use-client-country.test.ts +27 -27
- package/composables/use-client-country.ts +22 -20
- package/lib/countries/__tests__/countries.test.ts +60 -0
- package/lib/countries/de.json +251 -0
- package/lib/countries/en.json +251 -0
- package/lib/countries/es.json +251 -0
- package/lib/countries/fr.json +251 -0
- package/lib/countries/index.ts +51 -0
- package/lib/countries/it.json +251 -0
- package/lib/countries/ja.json +251 -0
- package/lib/countries/ko.json +251 -0
- package/lib/countries/ru.json +251 -0
- package/lib/countries/zh.json +251 -0
- package/package.json +1 -1
- package/pages/index.vue +6 -4
- package/plugins/countries.server.ts +13 -47
- package/runtime/index.ts +1 -0
- package/runtime/utils/server/__tests__/get-path-locale.test.ts +27 -0
- package/runtime/utils/server/get-path-locale.ts +10 -0
- package/utils/__tests__/iso-code-to-country-name.test.ts +30 -0
- package/utils/iso-code-to-country-name.ts +22 -0
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
<div class="card-category__wrapper-header">
|
|
22
22
|
<span class="card-category__title">
|
|
23
23
|
{{ card.title }}
|
|
24
|
+
<!-- Флаг декоративный: страна уже названа в заголовке рядом -->
|
|
24
25
|
<img
|
|
25
26
|
v-if="card.icon"
|
|
26
27
|
:src="`/icons/flags/${card.icon}.svg`"
|
|
27
|
-
:alt="card.icon"
|
|
28
28
|
class="card-category__icon"
|
|
29
29
|
width="17"
|
|
30
30
|
height="17"
|
|
31
31
|
loading="lazy"
|
|
32
|
+
alt=""
|
|
32
33
|
>
|
|
33
34
|
</span>
|
|
34
35
|
<span
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
<div class="card-model-mini__wrapper">
|
|
14
14
|
<p class="card-model-mini__name">
|
|
15
15
|
{{ card.title }}
|
|
16
|
+
<!-- Название страны текстом рядом не продублировано, поэтому оно в `alt`.
|
|
17
|
+
Форма как в CardModel: `name` — код для файла флага, `title` — читаемое имя. -->
|
|
16
18
|
<img
|
|
17
|
-
v-if="card.country"
|
|
18
|
-
:alt="card.country"
|
|
19
|
-
:src="`/icons/flags/${card.country}.svg`"
|
|
19
|
+
v-if="card.country?.name"
|
|
20
|
+
:alt="card.country.title"
|
|
21
|
+
:src="`/icons/flags/${card.country.name}.svg`"
|
|
20
22
|
class="card-model-mini__flag"
|
|
21
23
|
width="16"
|
|
22
24
|
height="12"
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
class="category-list__link"
|
|
10
10
|
>
|
|
11
11
|
{{ category.title }}
|
|
12
|
+
<!-- Флаг декоративный: страна уже названа в тексте ссылки -->
|
|
12
13
|
<img
|
|
13
14
|
v-if="category.icon"
|
|
14
15
|
:src="`/icons/flags/${category.icon}.svg`"
|
|
15
|
-
:alt="category.icon"
|
|
16
16
|
class="category-list__link-image"
|
|
17
17
|
width="17"
|
|
18
18
|
height="17"
|
|
19
19
|
loading="lazy"
|
|
20
|
+
alt=""
|
|
20
21
|
>
|
|
21
22
|
</NuxtLink>
|
|
22
23
|
<NuxtLink
|
package/components/info/grid.vue
CHANGED
|
@@ -41,13 +41,14 @@
|
|
|
41
41
|
]"
|
|
42
42
|
class="info-grid__item-value"
|
|
43
43
|
>
|
|
44
|
+
<!-- Флаг декоративный: страна названа в `value.title` ниже -->
|
|
44
45
|
<img
|
|
45
46
|
v-if="item.name === 'nationality_iso_codes' && isoCodes"
|
|
46
47
|
:src="`/icons/flags/${isoCodes[vIndex]}.svg`"
|
|
47
|
-
:alt="isoCodes[vIndex]"
|
|
48
48
|
class="info-grid__item-flag"
|
|
49
49
|
width="20"
|
|
50
50
|
height="14"
|
|
51
|
+
alt=""
|
|
51
52
|
>
|
|
52
53
|
<UiIcon
|
|
53
54
|
v-else-if="item.icon"
|
package/components/info/main.vue
CHANGED
|
@@ -77,11 +77,13 @@
|
|
|
77
77
|
size="14"
|
|
78
78
|
/>
|
|
79
79
|
<span class="info-main__born-text">{{ card.birthSummary }}</span>
|
|
80
|
+
<!-- Национальность нигде больше не подписана — флаг единственный её носитель,
|
|
81
|
+
поэтому в `alt` идёт название страны, а не ISO-код. -->
|
|
80
82
|
<img
|
|
81
83
|
v-for="(item, index) in card.nationalityIsoCodes"
|
|
82
84
|
:key="`nationalityIsoCodes-${index}`"
|
|
83
85
|
:src="`/icons/flags/${item}.svg`"
|
|
84
|
-
:alt="item"
|
|
86
|
+
:alt="isoCodeToCountryName(item, locale)"
|
|
85
87
|
class="info-main__born-flag"
|
|
86
88
|
width="20"
|
|
87
89
|
height="14"
|
|
@@ -157,8 +159,9 @@ import type {
|
|
|
157
159
|
} from '../../types';
|
|
158
160
|
import { AuthorizationApiService } from '../../services/api/authorization.service';
|
|
159
161
|
import { groupParametersByGroup } from '../../utils/info-main/group-parameters-by-group';
|
|
162
|
+
import { isoCodeToCountryName } from '../../utils/iso-code-to-country-name';
|
|
160
163
|
|
|
161
|
-
const { t } = useI18n();
|
|
164
|
+
const { t, locale } = useI18n();
|
|
162
165
|
|
|
163
166
|
const props = defineProps<{
|
|
164
167
|
card: ICardInfo
|
|
@@ -211,13 +214,26 @@ function getParameter(name: string) {
|
|
|
211
214
|
return props.card.parameters?.find(p => p.name === name)?.values?.[ 0 ]?.name;
|
|
212
215
|
}
|
|
213
216
|
|
|
217
|
+
/**
|
|
218
|
+
* `IModelCard.country` — пара «код файла флага + читаемое имя», как её отдаёт
|
|
219
|
+
* cleanModelCard. Раньше сюда уезжал целиком массив `nationalityIsoCodes`: у модели с
|
|
220
|
+
* двумя национальностями путь к флагу выходил `/icons/flags/us,gb.svg`, а у модели без
|
|
221
|
+
* национальностей пустой массив проходил `v-if` и давал битую картинку. Берём первый код.
|
|
222
|
+
*/
|
|
223
|
+
const shareCountry = computed(() => {
|
|
224
|
+
const iso = props.card.nationalityIsoCodes?.[0] || '';
|
|
225
|
+
if (!iso) return undefined;
|
|
226
|
+
|
|
227
|
+
return { name: iso, title: isoCodeToCountryName(iso, locale.value) };
|
|
228
|
+
});
|
|
229
|
+
|
|
214
230
|
const shareModelCard = computed<IModelCard>(() => ({
|
|
215
231
|
title: props.card.title,
|
|
216
232
|
videosCount: props.card.videosCount,
|
|
217
233
|
thumbUrl: props.card.img || '',
|
|
218
234
|
guid: props.card.guid || '',
|
|
219
235
|
parameters: [],
|
|
220
|
-
country:
|
|
236
|
+
country: shareCountry.value,
|
|
221
237
|
age: getParameter('age'),
|
|
222
238
|
}));
|
|
223
239
|
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
|
|
133
133
|
<script setup lang="ts">
|
|
134
134
|
import type { ISelectItem } from '../../types';
|
|
135
|
+
import { isoCodeToCountryName } from '../../utils/iso-code-to-country-name';
|
|
135
136
|
|
|
136
137
|
const { locale, locales } = useI18n();
|
|
137
138
|
const switchLocalePath = useSwitchLocalePath();
|
|
@@ -143,20 +144,46 @@ defineProps<{
|
|
|
143
144
|
}>();
|
|
144
145
|
|
|
145
146
|
const {
|
|
147
|
+
code: clientCountryCode,
|
|
146
148
|
icon: clientCountryIcon,
|
|
147
|
-
|
|
149
|
+
place: clientCountryPlace,
|
|
148
150
|
flagSrc: clientCountryFlag,
|
|
149
151
|
countriesItems,
|
|
150
152
|
setCountry,
|
|
151
153
|
loadCountries,
|
|
152
154
|
} = useClientCountry();
|
|
153
155
|
|
|
156
|
+
// Именительный падеж считает ICU, своей таблицы имён нет. Пустой `place` = служебный код
|
|
157
|
+
// гео-заголовка (`XX`, `T1`, `AP`), такую «страну» не подписываем.
|
|
158
|
+
const clientCountryName = computed(() => (
|
|
159
|
+
clientCountryPlace.value ? isoCodeToCountryName(clientCountryCode.value, locale.value) : ''
|
|
160
|
+
));
|
|
161
|
+
|
|
154
162
|
const isCountryOpen = ref(false);
|
|
155
163
|
const countryValue = ref('');
|
|
164
|
+
|
|
165
|
+
// Список стран приходит с API только по-английски — подписываем его тем же ICU-словарём,
|
|
166
|
+
// что и текущую страну, и пересортировываем коллатором: порядок с бэка — английский
|
|
167
|
+
// алфавит. Нераспознанный код `isoCodeToCountryName` вернёт как есть — тогда имя с API.
|
|
168
|
+
const localizedCountries = computed(() => {
|
|
169
|
+
const collator = new Intl.Collator(locale.value);
|
|
170
|
+
|
|
171
|
+
return countriesItems.value
|
|
172
|
+
.map((item) => {
|
|
173
|
+
const code = (item.key || '').toUpperCase();
|
|
174
|
+
const name = isoCodeToCountryName(code, locale.value);
|
|
175
|
+
const title = name === code ? item.title : name;
|
|
176
|
+
return { ...item, title, value: title, apiTitle: item.title };
|
|
177
|
+
})
|
|
178
|
+
.sort((a, b) => collator.compare(a.title, b.title));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Ищем и по английскому имени: привычка набирать `germany` не должна ломаться на ru-локали
|
|
156
182
|
const filteredCountries = computed(() => {
|
|
157
|
-
if (!countryValue.value) return
|
|
183
|
+
if (!countryValue.value) return localizedCountries.value;
|
|
158
184
|
const query = countryValue.value.toLowerCase();
|
|
159
|
-
return
|
|
185
|
+
return localizedCountries.value.filter(item => item.title.toLowerCase().includes(query)
|
|
186
|
+
|| item.apiTitle.toLowerCase().includes(query));
|
|
160
187
|
});
|
|
161
188
|
|
|
162
189
|
function onCountryToggle(e: Event) {
|
|
@@ -9,7 +9,8 @@ vi.mock('../../services/api/dictionaries.service', () => ({
|
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
11
|
const setCode = (code: string) => useState<string>('clientCountryCode', () => '').value = code;
|
|
12
|
-
|
|
12
|
+
/** Признак реальной страны: ставит SSR-плагин из lib/countries */
|
|
13
|
+
const setPlace = (place: string) => useState<string>('clientCountryPlace', () => '').value = place;
|
|
13
14
|
|
|
14
15
|
describe('useClientCountry', () => {
|
|
15
16
|
beforeEach(() => {
|
|
@@ -17,17 +18,17 @@ describe('useClientCountry', () => {
|
|
|
17
18
|
getCountries.mockReset();
|
|
18
19
|
});
|
|
19
20
|
|
|
20
|
-
it('без кода не отдаёт ни флаг, ни
|
|
21
|
-
const { icon,
|
|
21
|
+
it('без кода не отдаёт ни флаг, ни обстоятельство места', () => {
|
|
22
|
+
const { icon, place, flagSrc } = useClientCountry();
|
|
22
23
|
|
|
23
24
|
expect(icon.value).toBe('');
|
|
24
|
-
expect(
|
|
25
|
+
expect(place.value).toBe('');
|
|
25
26
|
expect(flagSrc.value).toBe('');
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
it('строит иконку и путь к флагу из кода', () => {
|
|
29
30
|
setCode('ad');
|
|
30
|
-
|
|
31
|
+
setPlace('в Андорре');
|
|
31
32
|
const { icon, flagSrc } = useClientCountry();
|
|
32
33
|
|
|
33
34
|
expect(icon.value).toBe('ad');
|
|
@@ -36,7 +37,7 @@ describe('useClientCountry', () => {
|
|
|
36
37
|
|
|
37
38
|
it('нормализует код: регистр и пробелы', () => {
|
|
38
39
|
setCode(' AD ');
|
|
39
|
-
|
|
40
|
+
setPlace('в Андорре');
|
|
40
41
|
const { icon, flagSrc } = useClientCountry();
|
|
41
42
|
|
|
42
43
|
expect(icon.value).toBe('ad');
|
|
@@ -45,7 +46,7 @@ describe('useClientCountry', () => {
|
|
|
45
46
|
|
|
46
47
|
it('учитывает страны, где код флага отличается от ISO-кода', () => {
|
|
47
48
|
setCode('cn');
|
|
48
|
-
|
|
49
|
+
setPlace('в Китае');
|
|
49
50
|
const { icon, flagSrc } = useClientCountry();
|
|
50
51
|
|
|
51
52
|
expect(icon.value).toBe('zh');
|
|
@@ -53,19 +54,28 @@ describe('useClientCountry', () => {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
it('не показывает флаг для служебного кода, которого нет в словаре', () => {
|
|
56
|
-
// Cloudflare отдаёт XX для неизвестного IP и T1 для Tor — файла такого флага
|
|
57
|
+
// Cloudflare отдаёт XX для неизвестного IP и T1 для Tor — файла такого флага нет.
|
|
58
|
+
// Реальность страны определяет непустой `place`: по Intl.DisplayNames не проверить,
|
|
59
|
+
// она вернёт `XX` как `XX`, а `US` в en-локали — как `US`
|
|
57
60
|
setCode('xx');
|
|
58
|
-
const { icon,
|
|
61
|
+
const { icon, place, flagSrc } = useClientCountry();
|
|
59
62
|
|
|
60
63
|
expect(icon.value).toBe('xx');
|
|
61
|
-
expect(
|
|
64
|
+
expect(place.value).toBe('');
|
|
62
65
|
expect(flagSrc.value).toBe('');
|
|
63
66
|
});
|
|
64
67
|
|
|
65
|
-
it('
|
|
68
|
+
it('обстоятельство места берёт из состояния, поставленного SSR', () => {
|
|
69
|
+
setCode('cz');
|
|
70
|
+
setPlace('в Чехии');
|
|
71
|
+
const { place } = useClientCountry();
|
|
72
|
+
|
|
73
|
+
expect(place.value).toBe('в Чехии');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('загруженный список стран флаг не ломает', async () => {
|
|
66
77
|
setCode('ad');
|
|
67
|
-
|
|
68
|
-
setResolvedName('USA');
|
|
78
|
+
setPlace('в Андорре');
|
|
69
79
|
getCountries.mockResolvedValue({
|
|
70
80
|
countries: [
|
|
71
81
|
{ title: 'Andorra', isoCodes: ['AD', 'AND'] },
|
|
@@ -73,30 +83,20 @@ describe('useClientCountry', () => {
|
|
|
73
83
|
],
|
|
74
84
|
});
|
|
75
85
|
|
|
76
|
-
const {
|
|
86
|
+
const { flagSrc, loadCountries } = useClientCountry();
|
|
77
87
|
await loadCountries();
|
|
78
88
|
|
|
79
|
-
expect(name.value).toBe('Andorra');
|
|
80
89
|
expect(flagSrc.value).toBe('/icons/flags/ad.svg');
|
|
81
90
|
});
|
|
82
91
|
|
|
83
|
-
it('
|
|
84
|
-
setCode('ad');
|
|
85
|
-
setResolvedName('Andorra');
|
|
86
|
-
const { name } = useClientCountry();
|
|
87
|
-
|
|
88
|
-
expect(name.value).toBe('Andorra');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('выбор страны меняет и код, и имя', () => {
|
|
92
|
+
it('выбор страны меняет код и флаг', () => {
|
|
92
93
|
setCode('ad');
|
|
93
|
-
|
|
94
|
-
const { code,
|
|
94
|
+
setPlace('в Андорре');
|
|
95
|
+
const { code, flagSrc, setCountry } = useClientCountry();
|
|
95
96
|
|
|
96
97
|
setCountry({ title: 'USA', value: 'USA', icon: 'us', key: 'us' });
|
|
97
98
|
|
|
98
99
|
expect(code.value).toBe('us');
|
|
99
|
-
expect(name.value).toBe('USA');
|
|
100
100
|
expect(flagSrc.value).toBe('/icons/flags/us.svg');
|
|
101
101
|
});
|
|
102
102
|
|
|
@@ -7,17 +7,23 @@ const COOKIE_OPTS = { maxAge: 60 * 60 * 24 * 365, path: '/', secure: true, sameS
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Единый источник страны клиента: ISO-код (гео-заголовок прокси или ручной выбор
|
|
10
|
-
* в селекторе), иконка флага и
|
|
10
|
+
* в селекторе), иконка флага и обстоятельство места для заголовков.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Всё резолвится ОТ ТЕКУЩЕГО кода — своего кеша нет. Раньше имя страны лежало в отдельной
|
|
13
|
+
* куке `clientCountryName` рядом с парной `clientCountryNameCode`, и пара могла разъехаться:
|
|
14
|
+
* заголовок писал одну страну, флаг и селектор показывали другую (реальный кейс на проде:
|
|
15
|
+
* `clientCountryName=USA` при коде `ad`).
|
|
16
|
+
*
|
|
17
|
+
* Имени тут нет намеренно: именительный падеж один в один отдаёт `Intl.DisplayNames`, см.
|
|
18
|
+
* utils/iso-code-to-country-name.ts — потребитель зовёт её со своей локалью.
|
|
16
19
|
*/
|
|
17
20
|
export const useClientCountry = () => {
|
|
18
21
|
const code = useState<string>('clientCountryCode', () => '');
|
|
19
|
-
const resolvedName = useState<string>('clientCountryName', () => '');
|
|
20
22
|
const countriesItems = useState<ISelectItem[]>('countriesItems', () => []);
|
|
23
|
+
// Обстоятельство места («в Чехии») ставит только SSR-плагин из lib/countries: косвенных
|
|
24
|
+
// падежей и артиклей в ECMA-402 нет. Ручной выбор страны в селекторе перезагружает
|
|
25
|
+
// страницу, поэтому клиенту словарь не нужен.
|
|
26
|
+
const place = useState<string>('clientCountryPlace', () => '');
|
|
21
27
|
const codeCookie = useCookie<string | null>('clientCountryCode', COOKIE_OPTS);
|
|
22
28
|
|
|
23
29
|
/** ISO-код → имя файла флага (у части стран код локали отличается от кода страны) */
|
|
@@ -28,22 +34,18 @@ export const useClientCountry = () => {
|
|
|
28
34
|
|
|
29
35
|
const icon = computed(() => toIcon(code.value));
|
|
30
36
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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` : ''));
|
|
37
|
+
// Флаг только для страны, которая есть в словаре: гео-заголовок умеет присылать служебные
|
|
38
|
+
// коды (Cloudflare `XX` для неизвестного IP и `T1` для Tor, MaxMind `AP`) — файла такого
|
|
39
|
+
// флага нет, вышла бы битая картинка. Непустой `place` и есть этот признак: по
|
|
40
|
+
// `Intl.DisplayNames` не проверить, она вернёт `XX` как `XX`, а `US` в en-локали — как `US`.
|
|
41
|
+
const flagSrc = computed(() => (icon.value && place.value ? `/icons/flags/${icon.value}.svg` : ''));
|
|
42
42
|
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Ручной выбор страны в селекторе: наружу переживает перезагрузку только код. `place`
|
|
45
|
+
* пересчитает следующий SSR — layout/language.vue тут же уходит в `location.href`.
|
|
46
|
+
*/
|
|
44
47
|
const setCountry = (item: ISelectItem) => {
|
|
45
48
|
code.value = item.key || '';
|
|
46
|
-
resolvedName.value = item.title;
|
|
47
49
|
codeCookie.value = code.value || null;
|
|
48
50
|
};
|
|
49
51
|
|
|
@@ -69,5 +71,5 @@ export const useClientCountry = () => {
|
|
|
69
71
|
}
|
|
70
72
|
};
|
|
71
73
|
|
|
72
|
-
return { code, icon,
|
|
74
|
+
return { code, icon, place, flagSrc, countriesItems, setCountry, loadCountries };
|
|
73
75
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getCountryPlace } from '../index';
|
|
3
|
+
import en from '../en.json';
|
|
4
|
+
|
|
5
|
+
describe('getCountryPlace', () => {
|
|
6
|
+
it('отдаёт обстоятельство места с предлогом и падежом', async () => {
|
|
7
|
+
expect(await getCountryPlace('cz', 'ru')).toBe('в Чехии');
|
|
8
|
+
expect(await getCountryPlace('cu', 'ru')).toBe('на Кубе');
|
|
9
|
+
expect(await getCountryPlace('fr', 'ru')).toBe('во Франции');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('учитывает артикли и сочленённые предлоги других локалей', async () => {
|
|
13
|
+
expect(await getCountryPlace('us', 'en')).toBe('in the United States');
|
|
14
|
+
expect(await getCountryPlace('ph', 'de')).toBe('auf den Philippinen');
|
|
15
|
+
expect(await getCountryPlace('us', 'it')).toBe('negli Stati Uniti');
|
|
16
|
+
expect(await getCountryPlace('us', 'fr')).toBe('aux États-Unis');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('для локалей без предлога отдаёт именительный падеж — частица живёт в шаблоне', async () => {
|
|
20
|
+
expect(await getCountryPlace('cz', 'ja')).toBe('チェコ');
|
|
21
|
+
expect(await getCountryPlace('cz', 'ko')).toBe('체코');
|
|
22
|
+
expect(await getCountryPlace('cz', 'zh')).toBe('捷克');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('нормализует код: регистр и пробелы', async () => {
|
|
26
|
+
expect(await getCountryPlace(' CZ ', 'ru')).toBe('в Чехии');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('на пустом и служебном коде отдаёт пустую строку', async () => {
|
|
30
|
+
expect(await getCountryPlace('', 'ru')).toBe('');
|
|
31
|
+
// Cloudflare XX (неизвестный IP) и T1 (Tor), MaxMind AP — не ISO-страны
|
|
32
|
+
expect(await getCountryPlace('xx', 'ru')).toBe('');
|
|
33
|
+
expect(await getCountryPlace('t1', 'ru')).toBe('');
|
|
34
|
+
expect(await getCountryPlace('ap', 'ru')).toBe('');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('неизвестную локаль резолвит английским словарём', async () => {
|
|
38
|
+
expect(await getCountryPlace('cz', 'pt')).toBe('in Czechia');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('словари покрывают все локали одним набором кодов', async () => {
|
|
42
|
+
const codes = Object.keys(en).sort();
|
|
43
|
+
|
|
44
|
+
for (const locale of ['de', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'zh']) {
|
|
45
|
+
const { default: places } = await import(`../${locale}.json`);
|
|
46
|
+
|
|
47
|
+
expect(Object.keys(places).sort(), locale).toEqual(codes);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('именительного падежа не держит — его отдаёт ICU один в один', async () => {
|
|
52
|
+
for (const locale of ['de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'zh']) {
|
|
53
|
+
const { default: places } = await import(`../${locale}.json`);
|
|
54
|
+
const displayNames = new Intl.DisplayNames([locale], { type: 'region' });
|
|
55
|
+
const missing = Object.keys(places).filter(code => !displayNames.of(code.toUpperCase()));
|
|
56
|
+
|
|
57
|
+
expect(missing, locale).toEqual([]);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|