itube-specs 0.0.1044 → 0.0.1046

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/SITE_UPDATES.md CHANGED
@@ -11,6 +11,23 @@
11
11
  коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
12
12
  кому именно (всем / сайтам с N), что будет, если не сделать.
13
13
 
14
+ ## 0.0.1045 — 2026-10-20
15
+
16
+ - **Сайтам, у которых бэк включает хинди (`/v1/site/languages` отдаёт `name: "in"`, `iso: "hi"` — сейчас
17
+ videosection, hdsex):** слой хинди уже знает — `X-Language: in` для бэка (с `hi` бэк отдаёт английский контент),
18
+ общие переводы `i18n/locales/hi-IN.json` (пока пустые, заполняются через CSV слоя), `lib/countries/hi.json`
19
+ для `{secondText}` главной. Чтобы включить хинди на сайте: в `config/locales.config.ts` добавить
20
+ `{ code: 'hi', sub: 'in', file: 'hi-IN.json', language: 'hi-IN', iso: 'hi-IN', name: 'हिन्दी', value: 'hi' }`,
21
+ свой `i18n/locales/hi-IN.json` (мету старого сайта переносят `import-old-meta.mjs` / `import-old-h1.mjs` —
22
+ образец коммит videosection-redesign с хинди), `hi` в `nicheAdjectives` (если есть `%niche%`). В шаблоне
23
+ `meta.index.trending.h1` у хинди `{secondText}` уже несёт послелог (`भारत में`) — «में» не дописывать.
24
+ Не сделать — хинди на сайте нет, хотя бэк его включает.
25
+ - **Всем — ничего делать не надо, к сведению:** переключатель языков и hreflang (`useEnabledLocales`,
26
+ `useSeoLinks`) показывают только локали, объявленные сайтом в `runtimeConfig.public.locales.codes`, и
27
+ включённые бэком. Раньше при пустом ответе бэка показывались все локали i18n — включая приехавшие файлом из
28
+ слоя, без домена у сайта, а у сайта с меньшим набором, чем файлов в слое, лишние локали от бэка проходили и
29
+ при нормальном ответе.
30
+
14
31
  ## 0.0.1041 — 2026-10-19
15
32
 
16
33
  - **Сайтам со своим `composables/use-meta.ts` или форком `pages/videos/[slug].vue` ради меты (videosection-redesign):**
@@ -285,13 +285,14 @@ async function changeCountry(item: ISelectItem) {
285
285
  @include font-sm;
286
286
 
287
287
  display: grid;
288
- grid-template-columns: 1fr auto;
288
+ grid-template-columns: minmax(0, 1fr) auto;
289
289
  margin-bottom: 4px;
290
290
  column-gap: 12px;
291
291
  align-items: center;
292
292
  color: $language-item-text;
293
293
  padding: 10px 8px;
294
294
  text-align: start;
295
+ overflow-wrap: anywhere;
295
296
  border-radius: $radius-lg;
296
297
 
297
298
  @include hover(background-color) {
@@ -300,11 +301,11 @@ async function changeCountry(item: ISelectItem) {
300
301
  }
301
302
 
302
303
  .layout-language__details-item:has(img) {
303
- grid-template-columns: 20px 1fr auto;
304
+ grid-template-columns: 20px minmax(0, 1fr) auto;
304
305
  }
305
306
 
306
307
  .layout-language__details-item:has(.layout-language__code) {
307
- grid-template-columns: 32px 1fr auto;
308
+ grid-template-columns: 32px minmax(0, 1fr) auto;
308
309
  }
309
310
 
310
311
  .layout-language__check {
@@ -356,6 +357,7 @@ async function changeCountry(item: ISelectItem) {
356
357
 
357
358
  .layout-language__buttons {
358
359
  display: grid;
360
+ grid-template-columns: minmax(0, 1fr);
359
361
  max-height: 192px;
360
362
  overflow-y: auto;
361
363
  scrollbar-width: none;
@@ -97,6 +97,36 @@ describe('useSeoLinks', () => {
97
97
  expect(hreflangs).not.toContain('zh-CN');
98
98
  });
99
99
 
100
+ it('alternate links только для объявленных сайтом локалей, даже без ответа бэка', () => {
101
+ const locales = useRuntimeConfig().public.locales;
102
+ locales.codes = ['en', 'fr', 'ja'];
103
+ try {
104
+ const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath);
105
+ const hreflangs = alternateLinks.value.map(l => l.hreflang);
106
+ expect(hreflangs).toContain('fr');
107
+ expect(hreflangs).toContain('ja-JP');
108
+ expect(hreflangs).not.toContain('de');
109
+ expect(hreflangs).not.toContain('zh-CN');
110
+ } finally {
111
+ locales.codes = [];
112
+ }
113
+ });
114
+
115
+ it('локаль нужна и сайту, и бэку', () => {
116
+ const locales = useRuntimeConfig().public.locales;
117
+ locales.codes = ['en', 'fr', 'ja'];
118
+ useEnabledLanguageCodes().value = ['en', 'fr', 'de'];
119
+ try {
120
+ const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath);
121
+ const hreflangs = alternateLinks.value.map(l => l.hreflang);
122
+ expect(hreflangs).toContain('fr');
123
+ expect(hreflangs).not.toContain('de');
124
+ expect(hreflangs).not.toContain('ja-JP');
125
+ } finally {
126
+ locales.codes = [];
127
+ }
128
+ });
129
+
100
130
  it('текущая локаль остаётся в alternate, даже если её нет в списке включённых', () => {
101
131
  mockRoute = { path: '/ja/videos', query: {} };
102
132
  mockI18n.locale.value = 'ja';
@@ -4,14 +4,25 @@
4
4
  */
5
5
  export const useEnabledLanguageCodes = () => useState<string[]>('enabledLanguageCodes', () => []);
6
6
 
7
+ /**
8
+ * Предикат «локаль показывается на сайте»: она объявлена сайтом (`runtimeConfig.public.locales.codes`)
9
+ * и включена бэком. Локаль, приехавшая только файлом из слоя, не проходит даже при пустом ответе бэка.
10
+ * Пустой список кодов сайта (слой без приложения) или бэка — это измерение не ограничивает.
11
+ */
12
+ export const useIsLocaleShown = () => {
13
+ const enabledCodes = useEnabledLanguageCodes();
14
+ const siteCodes: string[] = useRuntimeConfig().public.locales?.codes || [];
15
+
16
+ return (code: string) => (!siteCodes.length || siteCodes.includes(code))
17
+ && (!enabledCodes.value.length || enabledCodes.value.includes(code));
18
+ };
19
+
7
20
  /**
8
21
  * Локали i18n, отфильтрованные по включённым на сайте — для переключателей языка и hreflang.
9
22
  */
10
23
  export const useEnabledLocales = () => {
11
24
  const { locales } = useI18n();
12
- const codes = useEnabledLanguageCodes();
25
+ const isShown = useIsLocaleShown();
13
26
 
14
- return computed(() => codes.value.length
15
- ? locales.value.filter(item => codes.value.includes(item.code))
16
- : locales.value);
27
+ return computed(() => locales.value.filter(item => isShown(item.code)));
17
28
  };
@@ -24,7 +24,7 @@ type LocalePathMock = (path: string, localeCode: string) => string;
24
24
  export const useSeoLinks = (baseDomain: string, i18n: I18nMock, localePath: LocalePathMock) => {
25
25
  const route = useRoute();
26
26
  const overrides = useSeoOverrides();
27
- const enabledCodes = useEnabledLanguageCodes();
27
+ const isShown = useIsLocaleShown();
28
28
 
29
29
  const { locales, seo } = useRuntimeConfig().public;
30
30
  const defaultCode: string = locales.default;
@@ -116,14 +116,13 @@ export const useSeoLinks = (baseDomain: string, i18n: I18nMock, localePath: Loca
116
116
  });
117
117
  }
118
118
 
119
- // локаль, выключенная на сайте, отдаёт 404 (server/middleware/locale-guard.ts) —
120
- // такие hreflang вели бы на несуществующие страницы
121
- const isEnabled = (code: string) => !enabledCodes.value.length || enabledCodes.value.includes(code);
119
+ // локаль, выключенная на сайте, отдаёт 404 (server/middleware/locale-guard.ts), а не объявленная
120
+ // сайтом не имеет домена — такие hreflang вели бы на несуществующие страницы
122
121
 
123
122
  const otherLocales = i18n.locales.value.filter(
124
123
  (locale) => {
125
124
  const localeCode = typeof locale === 'string' ? locale : locale.code;
126
- return localeCode !== currentLocaleCode && localeCode !== defaultCode && isEnabled(localeCode);
125
+ return localeCode !== currentLocaleCode && localeCode !== defaultCode && isShown(localeCode);
127
126
  }
128
127
  );
129
128
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.1044",
4
+ "version": "0.0.1046",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -182,6 +182,7 @@ watchEffect(() => {
182
182
 
183
183
  .profile__header-wrapper {
184
184
  display: grid;
185
+ grid-template-columns: minmax(0, 1fr);
185
186
  row-gap: 1px;
186
187
  }
187
188
 
@@ -191,7 +192,7 @@ watchEffect(() => {
191
192
  display: grid;
192
193
  column-gap: 8px;
193
194
  align-items: center;
194
- grid-template-columns: auto 1fr;
195
+ grid-template-columns: auto minmax(0, 1fr);
195
196
  color: $profile-member-text;
196
197
  margin-top: 4px;
197
198
  }
@@ -245,7 +246,7 @@ watchEffect(() => {
245
246
 
246
247
  .profile__header {
247
248
  display: grid;
248
- grid-template-columns: auto 1fr;
249
+ grid-template-columns: auto minmax(0, 1fr);
249
250
  align-items: center;
250
251
  column-gap: 20px;
251
252
  }
@@ -253,6 +254,10 @@ watchEffect(() => {
253
254
  .profile__edit {
254
255
  @include button-variant(profile-edit);
255
256
 
257
+ grid-template-columns: auto minmax(0, 1fr);
258
+ max-width: 100%;
256
259
  margin-top: 12px;
260
+ white-space: normal;
261
+ text-align: start;
257
262
  }
258
263
  </style>