@autobusal/providers 1.28.0 → 1.29.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
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## 1.29.0
5
+
6
+ ### Changed
7
+
8
+ - **`getLanguage` takes the language the ORIGIN is about, and it outranks the
9
+ stored preference** — the only thing that does. The stored preference
10
+ exists so a returning visitor who last read the site in Albanian gets
11
+ Albanian at `busmagus.com/`, where the URL expresses no preference of its
12
+ own. On `busmagus.ro` the URL expresses nothing *but* a preference: the
13
+ domain is the request, and honouring a two-month-old `localStorage` entry
14
+ over it would render Albanian at a Romanian URL whose own canonical says it
15
+ is Romanian — reintroduced one returning visitor at a time, and invisible
16
+ to anybody testing in a clean browser.
17
+
18
+ It defaults to whatever `languages()` was told, so the two call sites that
19
+ ask outside of setup (the 500-error text in `apiClient`, the error page in
20
+ `Errors/Message`) answer in the language actually being read rather than
21
+ showing English error copy on a Romanian page.
22
+
23
+ Undefined — a single-domain brand, or an alias claiming no language —
24
+ leaves the ordering exactly as it was.
25
+
26
+ ### Added
27
+
28
+ - **`SettingsData.domain` / `.domains` / `.locale`** — which origin answered,
29
+ every origin the brand answers on, and the language this one is about.
30
+ `domains` is what turns hreflang from locale-to-path-prefix into
31
+ locale-to-**domain** (see `@autobusal/common`'s `localeHref`); its first
32
+ entry is the primary. All three optional, so an older API cannot break a
33
+ newer build.
34
+
35
+ ## 1.28.1
36
+
37
+ ### Fixed
38
+
39
+ - **The stale-chunk guard budgeted two reloads instead of one.** Measured: a
40
+ failure at `/operators` reloaded, the server answered with its canonical
41
+ `/operators/`, the same chunk failed again — and the raw path no longer
42
+ matched the one already spent, so it reloaded a second time. Bounded, but
43
+ twice what was intended, and on a genuinely broken deploy the user watched
44
+ the page flash twice before the error appeared. The budget key now strips a
45
+ trailing slash, so one journey gets one reload.
46
+
47
+ Only visible because the retest ran against a chunk that was *permanently*
48
+ missing rather than replaced — the ordinary stale-bundle case recovers on
49
+ the first reload and never reaches the second.
50
+
3
51
  ## 1.28.0
4
52
 
5
53
  ### Added
package/Setup/Setup.tsx CHANGED
@@ -15,7 +15,11 @@ const Setup = ({ type, children }: Props): JSX.Element => {
15
15
  useGetMenu();
16
16
  useUserBootstrap();
17
17
 
18
- languages(type, data.preferences.languages.default, data.preferences.languages.available);
18
+ // Edited: Ferjolt Ozuni - Date: 2026-08-06
19
+ // `data.locale` - the language THIS origin is about, when it is a domain
20
+ // about a language (a ccTLD) rather than simply the brand's address. It
21
+ // outranks the stored preference; see getLanguage.
22
+ languages(type, data.preferences.languages.default, data.preferences.languages.available, data.locale);
19
23
 
20
24
  // Edited: Ferjolt Ozuni - Date: 2026-08-01
21
25
  // The brand's GTM container, from labels_settings via the admin GUI.
@@ -2,6 +2,13 @@ import i18next from 'i18next';
2
2
  import Backend from 'i18next-http-backend';
3
3
  import { initReactI18next } from 'react-i18next';
4
4
 
5
+ /**
6
+ * The language this ORIGIN is about, remembered from the one place that is
7
+ * told it (`languages()`, from the settings payload) so the call sites that
8
+ * ask outside of setup do not each have to be handed it. See getLanguage.
9
+ */
10
+ let originLocale: string | null | undefined;
11
+
5
12
  /**
6
13
  * The language to start in.
7
14
  *
@@ -16,14 +23,42 @@ import { initReactI18next } from 'react-i18next';
16
23
  * `available` is passed in rather than read from anywhere global so this
17
24
  * stays a pure function of its inputs, and so an unrecognised first segment
18
25
  * (/city/tirana) is never mistaken for a language.
26
+ *
27
+ * Edited: Ferjolt Ozuni - Date: 2026-08-06
28
+ * `domainLocale` - the language THIS origin is about, when it is about one
29
+ * (obtapi's settings payload, `locale`). It outranks the stored preference,
30
+ * which nothing else does.
31
+ *
32
+ * The stored preference exists so a returning visitor who last read the site
33
+ * in Albanian gets Albanian at busmagus.com/, where the URL expresses no
34
+ * preference of its own. On busmagus.ro the URL expresses nothing BUT a
35
+ * preference: the domain is the request, and honouring a two-month-old
36
+ * localStorage entry over it would render Albanian at a Romanian URL whose
37
+ * own canonical says it is Romanian - the mismatch localised URLs exist to
38
+ * remove, reintroduced one returning visitor at a time and invisible to
39
+ * anybody testing in a clean browser.
40
+ *
41
+ * Undefined (a single-domain brand, or an alias claiming no language) leaves
42
+ * the ordering exactly as it was.
43
+ *
44
+ * It DEFAULTS to whatever `languages()` was told, so the two call sites that
45
+ * ask this question outside of setup - the 500-error text in apiClient and
46
+ * the error page in Errors/Message, both of which pass only 'en' - answer in
47
+ * the language the visitor is actually reading rather than dropping back to
48
+ * a stale localStorage entry and showing English error copy on a Romanian
49
+ * page.
19
50
  */
20
- export const getLanguage = (defaultLanguage: string, available: string[] = []): string => {
51
+ export const getLanguage = (defaultLanguage: string, available: string[] = [], domainLocale: string | null | undefined = originLocale): string => {
21
52
  const [ , first ] = window.location.pathname.split('/');
22
53
 
23
54
  if (first && available.includes(first)) {
24
55
  return first;
25
56
  }
26
57
 
58
+ if (domainLocale && (available.length === 0 || available.includes(domainLocale))) {
59
+ return domainLocale;
60
+ }
61
+
27
62
  const stored = localStorage.getItem('language');
28
63
 
29
64
  return (stored && (available.length === 0 || available.includes(stored)))
@@ -43,7 +78,12 @@ export const getLanguage = (defaultLanguage: string, available: string[] = []):
43
78
  // consumer that hasn't set VITE_BUILD_ID yet (no worse than before).
44
79
  const buildId = import.meta.env.VITE_BUILD_ID ?? 'dev';
45
80
 
46
- const languages = (type: string, defaultLanguage: string, available: string[] = []): void => {
81
+ const languages = (type: string, defaultLanguage: string, available: string[] = [], domainLocale?: string | null): void => {
82
+ // Before the early return, not after: the guard is about not initialising
83
+ // i18next twice, and getLanguage's callers need this on every render of
84
+ // the app's life, not only the first one.
85
+ originLocale = domainLocale;
86
+
47
87
  if (i18next.isInitialized) {
48
88
  return;
49
89
  }
@@ -32,6 +32,23 @@
32
32
 
33
33
  const KEY = 'chunk-reload';
34
34
 
35
+ /**
36
+ * The key a reload is budgeted against.
37
+ *
38
+ * Ferjolt Ozuni - Date: 2026-08-06
39
+ * TRAILING SLASH STRIPPED, and that is not tidiness. Measured: a failure at
40
+ * `/operators` reloaded, the server answered with its canonical `/operators/`,
41
+ * the same chunk failed again - and the raw path no longer matched the one
42
+ * already spent, so it reloaded a SECOND time. Bounded, but twice what was
43
+ * intended, and on a genuinely broken deploy the user watches the page flash
44
+ * twice before the error appears. One journey, one budget.
45
+ */
46
+ const budgetKey = (): string => {
47
+ const path = window.location.pathname.replace(/\/+$/, '') || '/';
48
+
49
+ return path + window.location.search;
50
+ };
51
+
35
52
  /**
36
53
  * Which paths this tab has already spent its reload on.
37
54
  */
@@ -78,9 +95,7 @@ const remember = (path: string): boolean => {
78
95
  */
79
96
  const registerStaleChunkRecovery = (): void => {
80
97
  window.addEventListener('vite:preloadError', (event) => {
81
- const path = window.location.pathname + window.location.search;
82
-
83
- if (!remember(path)) {
98
+ if (!remember(budgetKey())) {
84
99
  // Spent already. Let it throw so the error boundary says something,
85
100
  // rather than reloading into the same wall a second time.
86
101
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/types/settings.ts CHANGED
@@ -7,6 +7,26 @@ export interface SettingsData {
7
7
 
8
8
  url: string
9
9
 
10
+ /**
11
+ * Edited: Ferjolt Ozuni - Date: 2026-08-06
12
+ * Multi-TLD: which origin answered this request, every origin this brand
13
+ * answers on, and the language this one is about.
14
+ *
15
+ * `domains` is what turns hreflang from locale-to-path-prefix into
16
+ * locale-to-DOMAIN (see @autobusal/common's localeHref). Its first entry
17
+ * is the primary - the origin registered with the payment gateways, and
18
+ * the one that hosts any language without a domain of its own under a
19
+ * /{locale}/ prefix.
20
+ *
21
+ * `locale` is null for a single-domain brand and for an alias that claims
22
+ * no language; everything derived from these then resolves to exactly the
23
+ * previous single-origin behaviour. All three are optional so an older API
24
+ * cannot break a newer build.
25
+ */
26
+ domain?: string | null
27
+ domains?: { domain: string, locale?: string | null, primary?: boolean }[]
28
+ locale?: string | null
29
+
10
30
  // Edited: Ferjolt Ozuni - Date: 2026-08-01
11
31
  // Present ONLY while the brand is under construction - obtapi omits it
12
32
  // entirely on a live site, so its presence is the switch itself and there