@autobusal/providers 1.14.0 → 1.16.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
7
7
  > Note: 1.8.0 through 1.11.0 were published without changelog entries. The
8
8
  > gap is left as-is rather than reconstructed after the fact.
9
9
 
10
+ ## [1.16.0] - 2026-08-02
11
+
12
+ ### Changed
13
+
14
+ - **SEO overrides are per language.** `LabelSettings.seo` and the location
15
+ types carry a `{locale: {title, description}}` map. The label's homepage
16
+ override is sent WHOLE and picked client-side, unlike the country/city
17
+ ones which resolve server-side - not an inconsistency but a bootstrap fact:
18
+ `/api/settings/get` is the one request that runs before i18n exists, so the
19
+ server cannot know which language to answer in. Resolving it there put
20
+ English copy on the Italian homepage.
21
+ - **`seo_keywords` is gone** everywhere. Google has ignored the meta keywords
22
+ tag since 2009; the field could only ever cost somebody an afternoon.
23
+
24
+ ## [1.15.0] - 2026-08-02
25
+
26
+ ### Changed
27
+
28
+ - **The URL decides the language.** `getLanguage()` now reads a locale prefix
29
+ off the path before falling back to the stored preference and then the
30
+ brand default, and takes the brand's `available` list so an unrecognised
31
+ first segment (`/city/tirana`) is never mistaken for a language. This is
32
+ the point of localised URLs: a link has to decide what its recipient sees,
33
+ so someone whose last visit was in Albanian still gets Italian from a
34
+ shared `/it/...` link. Unprefixed URLs are unaffected.
35
+
10
36
  ## [1.14.0] - 2026-08-02
11
37
 
12
38
  ### Changed
package/Setup/Setup.tsx CHANGED
@@ -15,7 +15,7 @@ const Setup = ({ type, children }: Props): JSX.Element => {
15
15
  useGetMenu();
16
16
  useUserBootstrap();
17
17
 
18
- languages(type, data.preferences.languages.default);
18
+ languages(type, data.preferences.languages.default, data.preferences.languages.available);
19
19
 
20
20
  // Edited: Ferjolt Ozuni - Date: 2026-08-01
21
21
  // The brand's GTM container, from labels_settings via the admin GUI.
@@ -2,9 +2,34 @@ import i18next from 'i18next';
2
2
  import Backend from 'i18next-http-backend';
3
3
  import { initReactI18next } from 'react-i18next';
4
4
 
5
- export const getLanguage = (defaultLanguage: string): string => (
6
- localStorage.getItem('language') ?? defaultLanguage
7
- );
5
+ /**
6
+ * The language to start in.
7
+ *
8
+ * Edited: Ferjolt Ozuni - Date: 2026-08-02
9
+ * A locale-prefixed URL now wins over the stored preference. That ordering
10
+ * is the whole point of localised URLs: a link has to decide what its
11
+ * recipient sees, so somebody whose last visit was in Albanian must still
12
+ * get Italian when they open a shared /it/... link. The stored preference
13
+ * still covers every unprefixed URL - the homepage a returning visitor
14
+ * types in, and the whole account area, which is not localised.
15
+ *
16
+ * `available` is passed in rather than read from anywhere global so this
17
+ * stays a pure function of its inputs, and so an unrecognised first segment
18
+ * (/city/tirana) is never mistaken for a language.
19
+ */
20
+ export const getLanguage = (defaultLanguage: string, available: string[] = []): string => {
21
+ const [ , first ] = window.location.pathname.split('/');
22
+
23
+ if (first && available.includes(first)) {
24
+ return first;
25
+ }
26
+
27
+ const stored = localStorage.getItem('language');
28
+
29
+ return (stored && (available.length === 0 || available.includes(stored)))
30
+ ? stored
31
+ : defaultLanguage;
32
+ };
8
33
 
9
34
  // Cache-busting query param for the language files. `Cache-Control: no-cache`
10
35
  // on the obtapi side only asks caches to revalidate before reuse - in
@@ -18,7 +43,7 @@ export const getLanguage = (defaultLanguage: string): string => (
18
43
  // consumer that hasn't set VITE_BUILD_ID yet (no worse than before).
19
44
  const buildId = import.meta.env.VITE_BUILD_ID ?? 'dev';
20
45
 
21
- const languages = (type: string, defaultLanguage: string): void => {
46
+ const languages = (type: string, defaultLanguage: string, available: string[] = []): void => {
22
47
  if (i18next.isInitialized) {
23
48
  return;
24
49
  }
@@ -30,7 +55,7 @@ const languages = (type: string, defaultLanguage: string): void => {
30
55
  interpolation: {
31
56
  escapeValue: false
32
57
  },
33
- lng: getLanguage(defaultLanguage),
58
+ lng: getLanguage(defaultLanguage, available),
34
59
  fallbackLng: defaultLanguage,
35
60
  debug: import.meta.env.DEV,
36
61
  backend: {
@@ -45,7 +70,7 @@ const languages = (type: string, defaultLanguage: string): void => {
45
70
  // immediately (don't wait for the async backend fetch) and keep it in sync
46
71
  // on every future change, whether triggered by ChangeLanguage or anything
47
72
  // else that calls i18next.changeLanguage().
48
- document.documentElement.lang = getLanguage(defaultLanguage);
73
+ document.documentElement.lang = getLanguage(defaultLanguage, available);
49
74
  i18next.on('languageChanged', (lng) => {
50
75
  document.documentElement.lang = lng;
51
76
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
@@ -30,9 +30,14 @@ export interface CountryData {
30
30
  // Optional hand-written override for the public country page's title/
31
31
  // description/keywords - see LocationCountry.tsx for the fallback-to-
32
32
  // template logic when these are unset.
33
- seo_title?: string
34
- seo_description?: string
35
- seo_keywords?: string
33
+ // Edited: Ferjolt Ozuni - Date: 2026-08-02
34
+ // Resolved to the request's language server-side on the PUBLIC read,
35
+ // so these stay flat there; `seo` is the raw per-locale map the admin
36
+ // form reads and writes. `seo_keywords` is gone - Google has ignored
37
+ // the meta keywords tag since 2009.
38
+ seo_title?: string | null
39
+ seo_description?: string | null
40
+ seo?: Record<string, { title?: string, description?: string }>
36
41
  }
37
42
 
38
43
  export interface CityStats {
@@ -74,9 +79,14 @@ export interface CityData {
74
79
  // Optional hand-written override for the public city page's title/
75
80
  // description/keywords - see LocationCity.tsx for the fallback-to-
76
81
  // template logic when these are unset.
77
- seo_title?: string
78
- seo_description?: string
79
- seo_keywords?: string
82
+ // Edited: Ferjolt Ozuni - Date: 2026-08-02
83
+ // Resolved to the request's language server-side on the PUBLIC read,
84
+ // so these stay flat there; `seo` is the raw per-locale map the admin
85
+ // form reads and writes. `seo_keywords` is gone - Google has ignored
86
+ // the meta keywords tag since 2009.
87
+ seo_title?: string | null
88
+ seo_description?: string | null
89
+ seo?: Record<string, { title?: string, description?: string }>
80
90
  }
81
91
 
82
92
  export interface StopData {
package/types/settings.ts CHANGED
@@ -37,14 +37,18 @@ export interface SettingsData {
37
37
  }
38
38
 
39
39
  // Edited: Ferjolt Ozuni - Date: 2026-07-31
40
- // Optional admin-managed override for the homepage's title/description/
41
- // keywords (Labels\AdminController::seo()) - StaticHome.tsx prefers these
42
- // over its i18n translation-template values when set.
43
- seo?: {
44
- title?: string
45
- description?: string
46
- keywords?: string
47
- }
40
+ // Optional admin-managed override for the homepage's title/description
41
+ // (Labels\AdminController::seo()) - StaticHome.tsx prefers these over its
42
+ // i18n translation-template values when set.
43
+ //
44
+ // Edited: Ferjolt Ozuni - Date: 2026-08-02
45
+ // The WHOLE per-language map, picked client-side by StaticHome - unlike
46
+ // the country/city overrides, which resolve server-side. This one request
47
+ // runs before i18n exists (Setup initialises i18n from the settings it is
48
+ // waiting on), so the server cannot know which language to answer in.
49
+ // A language with no entry has no override and keeps its own template.
50
+ // `keywords` is gone: Google has ignored the meta keywords tag since 2009.
51
+ seo?: Record<string, { title?: string, description?: string }>
48
52
 
49
53
  preferences: {
50
54
  company: string
@@ -226,11 +230,11 @@ export interface LabelSettings {
226
230
  // Edited: Ferjolt Ozuni - Date: 2026-07-31
227
231
  // Admin-managed homepage SEO override (public seo? above is populated
228
232
  // from this) - see Labels\AdminController::seo().
229
- seo?: {
230
- title?: string
231
- description?: string
232
- keywords?: string
233
- }
233
+ //
234
+ // Edited: Ferjolt Ozuni - Date: 2026-08-02
235
+ // Keyed by locale. A language with no entry has no override at all, which
236
+ // is what returns that language's homepage to its own template.
237
+ seo?: Record<string, { title?: string, description?: string }>
234
238
 
235
239
  // Edited: Ferjolt Ozuni - Date: 2026-08-01
236
240
  // Admin-managed Google Tag Manager container. The PUBLIC settings payload