@autobusal/common 1.27.0 → 1.27.2

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,49 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.27.2
3
4
 
4
- ## 1.27.0
5
+ ### Fixed
5
6
 
6
- ### Added
7
+ - **A birth date no longer defaults to today.** `Calendar` pre-fills with
8
+ today when given no value, which is right for every other use of it — a
9
+ departure, a report range, a broadcast day — and wrong for exactly one:
10
+ nobody is born today. On the profile-completion form it meant anybody who
11
+ filled in the fields that *looked* empty saved a date of birth of today,
12
+ silently, and that is the field a passenger's age is judged by when they
13
+ book. Reproduced end to end: the row saved `dob = <today>`.
7
14
 
8
- - **`localeHref`** the one URL a page has in a given language, once a brand
9
- answers on more than one domain. A language either has a **domain of its
10
- own** and lives at that domain's bare paths (`busmagus.ro/city/tirana`), or
11
- it does not and keeps its `/{locale}/` prefix on the primary. Which of the
12
- two is a property of the *language*, not of the page, so this is one
13
- function every caller shares. The deliberate twin of obtapi's
14
- `Libraries\Labels\LocaleUrl::href`. See `magus/MULTI_TLD_PLAN.md` item 4.
15
+ `type="dob"` now starts empty so the field reads as unanswered. The picker
16
+ still opens on the current month `createDate('')` is an Invalid Date and
17
+ the grid built from it renders as nothing but that anchor is display
18
+ only; what the form submits stays empty until a day is clicked, and
19
+ obtapi's new `required|date_format:d/m/Y|before:today` says so if it is
20
+ left that way.
15
21
 
16
- With no domain list — every brand today — it resolves to `origin + path`
17
- and the prefix convention, exactly as before.
18
22
 
19
- ### Changed
23
+ ## 1.27.1
24
+
25
+ ### Removed
26
+
27
+ - **Everything 1.27.0 added for multi-TLD** — `localeHref`, the `LocaleDomain`
28
+ type, `Meta`'s domain-resolved canonical and hreflang, and
29
+ `ChangeLanguage`'s cross-domain navigation. `Meta` is back to resolving both
30
+ from `window.location.origin` plus the `/{locale}/` prefix.
31
+
32
+ The product decision changed after the code landed: ccTLDs will **301 to
33
+ `.com/{locale}/`** rather than be real per-country sites, so none of this
34
+ had a caller and none of it ever would. A ccTLD's geo signal multiplies
35
+ content that already exists, and a new ccTLD starts at zero authority while
36
+ a subdirectory inherits the domain's on day one.
20
37
 
21
- - **`Meta`'s canonical is resolved through the domain that owns the page's
22
- language**, not off `window.location.origin`. Three things follow that the
23
- browser's own host cannot say: an **alias** (a `www.` host, a legacy domain
24
- kept for old links) now canonicalises to the real site instead of declaring
25
- itself the original; `/it/city/tirana` on the `.com` canonicalises to
26
- `busmagus.it/city/tirana` once Italian has a domain, so the prefixed form
27
- stops competing with the ccTLD that replaced it; and the canonical is built
28
- by the same function as the hreflang set, so it is always one of its own
29
- alternates — a canonical that is not is discarded by Google outright, and
30
- nothing on the page would have shown it.
31
-
32
- - **`Meta`'s hreflang alternates point at domains** where a language has one
33
- and keep their prefix where it does not. The set stays whole either way,
34
- which is the part that matters: an hreflang cluster is treated as
35
- reciprocal only when every member declares every other. `x-default`
36
- follows the **primary's** language rather than the current origin's — a
37
- `.ro` build announcing itself as x-default would offer Romanian to
38
- everybody a search engine could not place.
39
-
40
- - **`ChangeLanguage` navigates across domains.** Choosing a language that has
41
- its own domain leaves the site: another origin, its own bundle, its own
42
- settings payload, so it cannot go through the router. The i18n change and
43
- the stored preference are deliberately skipped on that path — they would
44
- apply to the site being left, and storing the language you are walking away
45
- from is how somebody returns to the `.com` later and finds it rendering
46
- Italian at English URLs.
38
+ **Do not downgrade to 1.26.1 to get this** 1.27.0 is published, so
39
+ `@latest` would reintroduce it. This version is 1.26.1's behaviour with a
40
+ higher number.
47
41
 
48
42
  ## 1.26.1
49
43
 
@@ -32,7 +32,24 @@ interface Props {
32
32
 
33
33
  const Calendar = ({ type, name, defaultValue, t, validation, minDate, refs, onUpdate, onChange }: Props): JSX.Element => {
34
34
  const [ show, setShow ] = useState<boolean>(false);
35
- const [ prepared, setPrepared ] = useState<string>(defaultValue ?? prepareDate(new Date()));
35
+
36
+ /**
37
+ * Edited: Ferjolt Ozuni - Date: 2026-08-06
38
+ * A BIRTH DATE does not default to today.
39
+ *
40
+ * Defaulting to today is right for every other use of this picker - a
41
+ * departure, a report range, a broadcast day - and wrong for exactly one:
42
+ * nobody is born today. On the profile-completion form it meant anybody
43
+ * who filled in the fields that LOOKED empty saved a date of birth of
44
+ * today, silently, and that is the field a passenger's age is judged by
45
+ * when they book. Reproduced end to end: the row saved dob = today.
46
+ *
47
+ * Empty instead, so the field reads as unanswered and the API's
48
+ * `required|date_format:d/m/Y|before:today` says so if it is left that way.
49
+ */
50
+ const [ prepared, setPrepared ] = useState<string>(
51
+ defaultValue ?? (type === 'dob' ? '' : prepareDate(new Date()))
52
+ );
36
53
  const [ selected, setSelected ] = useState<string>(prepared);
37
54
 
38
55
  const ref = useRef<HTMLDivElement>(null);
@@ -57,7 +74,11 @@ const Calendar = ({ type, name, defaultValue, t, validation, minDate, refs, onUp
57
74
  { show && (
58
75
  <Picker
59
76
  type={ type }
60
- value={ prepared }
77
+ // An empty birth date still needs a month to OPEN on: createDate('')
78
+ // is an Invalid Date and the grid built from it renders as nothing.
79
+ // The anchor is display only - what the form submits stays empty
80
+ // until a day is actually clicked.
81
+ value={ prepared || prepareDate(new Date()) }
61
82
  selected={ selected }
62
83
  minDate={ minDate }
63
84
  t={ t }
@@ -3,7 +3,7 @@ import { TFunction, i18n } from 'i18next';
3
3
  import { useLocation, useNavigate } from 'react-router-dom';
4
4
  import { useOutside } from '@autobusal/hooks';
5
5
  import Flag from './Flag';
6
- import { withLocale, localeHref } from '../Locale/locale';
6
+ import { withLocale } from '../Locale/locale';
7
7
  import { Container, Title, ContainerButtons, ButtonChange } from './styles';
8
8
  import { useGetSettings } from '@autobusal/providers/services';
9
9
 
@@ -55,27 +55,6 @@ const ChangeLanguage = ({ type, t, i18n, localised = false, onClose }: Props): J
55
55
  * language in place and stay put.
56
56
  */
57
57
  const onChange = (language: string): void => {
58
- const tail = `${ location.search }${ location.hash }`;
59
-
60
- // Edited: Ferjolt Ozuni - Date: 2026-08-06
61
- // A language with a DOMAIN of its own lives on that domain, so choosing
62
- // it leaves this site. That cannot go through the router - another
63
- // origin, its own bundle, its own settings payload - so it is a real
64
- // navigation, and the two state changes below are deliberately skipped:
65
- // they would apply to the site being left rather than the one being
66
- // opened, and storing the language we are walking away from is how
67
- // somebody returns to the .com later and finds it rendering Italian at
68
- // English URLs.
69
- if (localised) {
70
- const href = localeHref(location.pathname, language, fallback, available, data.domains ?? [], window.location.origin);
71
-
72
- if (!href.startsWith(window.location.origin)) {
73
- window.location.assign(`${ href }${ tail }`);
74
-
75
- return;
76
- }
77
- }
78
-
79
58
  i18n.changeLanguage(language);
80
59
  localStorage.setItem('language', language);
81
60
 
@@ -83,7 +62,7 @@ const ChangeLanguage = ({ type, t, i18n, localised = false, onClose }: Props): J
83
62
  const to = withLocale(location.pathname, language, fallback, available);
84
63
 
85
64
  if (to !== location.pathname) {
86
- navigate(`${ to }${ tail }`);
65
+ navigate(`${ to }${ location.search }${ location.hash }`);
87
66
  }
88
67
  }
89
68
 
package/Locale/locale.ts CHANGED
@@ -58,72 +58,6 @@ export const withLocale = (pathname: string, locale: string, defaultLocale: stri
58
58
  return clean === '/' ? `/${ locale }` : `/${ locale }${ clean }`;
59
59
  };
60
60
 
61
- /**
62
- * One origin a brand answers on, and the language it serves there.
63
- *
64
- * Ferjolt Ozuni - Date: 2026-08-06
65
- * Mirrors obtapi's Settings\ViewController::domains(). `locale` may be null:
66
- * an alias (a www. host, a legacy domain kept alive for old links) answers
67
- * without claiming a language, so nothing points hreflang at it.
68
- */
69
- export interface LocaleDomain {
70
- domain: string
71
- locale?: string | null
72
- primary?: boolean
73
- }
74
-
75
- /**
76
- * The ONE URL a page has in a given language.
77
- *
78
- * Ferjolt Ozuni - Date: 2026-08-06 - see magus/MULTI_TLD_PLAN.md item 4.
79
- *
80
- * A language reaches its own URL one of two ways, and which one is a
81
- * property of the language rather than of the page:
82
- *
83
- * - it has a DOMAIN of its own, and lives at that domain's bare paths -
84
- * busmagus.ro/city/tirana, not busmagus.com/ro/city/tirana;
85
- * - it does not, and lives under a /{locale}/ prefix on the primary.
86
- *
87
- * Both forms have to be derivable from anywhere, because the canonical, the
88
- * hreflang set and the language switcher must agree to the character. They
89
- * are the same claim about the same page said three times, and a canonical
90
- * that is not itself in its own hreflang set is discarded outright.
91
- *
92
- * `fallbackOrigin` is what to call "here" when the brand has no domain list
93
- * at all - an older API, or a payload that has not arrived. It resolves to
94
- * the previous single-origin behaviour exactly, which is why this is safe to
95
- * put on every page before any brand has a second domain.
96
- */
97
- export const localeHref = (
98
- pathname: string,
99
- locale: string,
100
- defaultLocale: string,
101
- available: string[],
102
- domains: LocaleDomain[],
103
- fallbackOrigin: string
104
- ): string => {
105
- const { path } = splitLocale(pathname, available);
106
-
107
- const clean = path === '' ? '/' : path;
108
-
109
- const owner = domains.find(item => item.locale === locale);
110
-
111
- if (owner) {
112
- return owner.domain + clean;
113
- }
114
-
115
- const primary = domains.find(item => item.primary);
116
-
117
- // Which language is unprefixed on the primary - NOT `defaultLocale`, which
118
- // on a ccTLD is that ccTLD's own language (obtapi answers the per-origin
119
- // question there). Reading it from the primary row is what stops the .ro
120
- // build announcing English as busmagus.com/en/... when English is the
121
- // thing that lives at busmagus.com/... itself.
122
- const unprefixed = primary?.locale ?? defaultLocale;
123
-
124
- return (primary?.domain ?? fallbackOrigin) + withLocale(clean, locale, unprefixed, available);
125
- };
126
-
127
61
  /**
128
62
  * Which language a URL is asking for, falling back to the stored preference
129
63
  * and then the brand default.
package/Meta.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useEffect } from 'react';
2
2
  import { useLocation } from 'react-router-dom';
3
- import { splitLocale, localeHref } from './Locale/locale';
3
+ import { splitLocale, withLocale } from './Locale/locale';
4
4
  import { pageview } from '@autobusal/providers/Setup/analytics';
5
5
  import { useGetSettings } from '@autobusal/providers/services';
6
6
 
@@ -155,45 +155,10 @@ const normalisePath = (pathname: string): string => (
155
155
  pathname.length > 1 && pathname.endsWith('/') ? pathname.replace(/\/+$/, '') : pathname
156
156
  );
157
157
 
158
- /**
159
- * Edited: Ferjolt Ozuni - Date: 2026-08-06
160
- * Resolved through the domain that OWNS this page's language rather than off
161
- * whichever host the visitor happens to be on - see localeHref and
162
- * magus/MULTI_TLD_PLAN.md item 4.
163
- *
164
- * Three things fall out of that which `window.location.origin` cannot say:
165
- *
166
- * - an ALIAS (a www. host, a legacy domain kept alive for old links) now
167
- * canonicalises to the real site instead of declaring itself the
168
- * original, which is the whole job of a canonical and the one case where
169
- * taking it from the browser is actively wrong;
170
- * - /it/city/tirana served from the .com canonicalises to
171
- * busmagus.it/city/tirana once Italian has a domain, so the prefixed form
172
- * stops competing with the ccTLD that replaced it;
173
- * - the canonical is built by the same function as the hreflang set, so it
174
- * is always one of its own alternates. A canonical that is not is
175
- * discarded by Google outright, and nothing on the page would show it.
176
- *
177
- * With no domain list this is `origin + path`, exactly as before.
178
- */
179
158
  const useCanonicalUrl = (explicit?: string): string => {
180
159
  const location = useLocation();
181
160
 
182
- const { data } = useGetSettings();
183
-
184
- if (explicit) {
185
- return explicit;
186
- }
187
-
188
- const available = data?.preferences?.languages?.available ?? [];
189
-
190
- const fallback = data?.preferences?.languages?.default ?? 'en';
191
-
192
- const path = normalisePath(location.pathname);
193
-
194
- const { locale } = splitLocale(path, available);
195
-
196
- return localeHref(path, locale ?? fallback, fallback, available, data?.domains ?? [], window.location.origin);
161
+ return explicit ?? `${ window.location.origin }${ normalisePath(location.pathname) }`;
197
162
  };
198
163
 
199
164
  // A single-page app never reloads, so GTM's page-load trigger fires exactly
@@ -242,19 +207,6 @@ const usePageviewTracking = (path: string, title: string): void => {
242
207
  * Suppressed for noindex pages: they have nothing to offer alternates for,
243
208
  * and telling a crawler about language variants of a page it has just been
244
209
  * asked not to index is noise at best.
245
- *
246
- * Edited: Ferjolt Ozuni - Date: 2026-08-06
247
- * Each language now resolves to the DOMAIN that owns it where it has one,
248
- * and keeps its /{locale}/ prefix on the primary where it does not - see
249
- * localeHref. The set stays whole either way, which is the part that
250
- * matters: Google treats an hreflang cluster as reciprocal only when every
251
- * member declares every other, so a page cannot list "the domains" and leave
252
- * out the languages that have none.
253
- *
254
- * x-default follows the PRIMARY's own language rather than this origin's.
255
- * The .com is the answer when there is no signal about the visitor; a .ro
256
- * build announcing itself as x-default would be offering Romanian to
257
- * everyone the search engine could not place.
258
210
  */
259
211
  const useAlternates = (noIndex: boolean, explicitUrl?: string): { locale: string, href: string }[] => {
260
212
  const location = useLocation();
@@ -274,20 +226,13 @@ const useAlternates = (noIndex: boolean, explicitUrl?: string): { locale: string
274
226
 
275
227
  const origin = window.location.origin;
276
228
 
277
- const domains = data?.domains ?? [];
278
-
279
229
  const { path } = splitLocale(normalisePath(location.pathname), available);
280
230
 
281
- // The primary's own language, so x-default lands on the .com. Falls back
282
- // to this origin's default when there is no domain list at all, which is
283
- // the single-domain case and exactly the previous behaviour.
284
- const neutral = domains.find(item => item.primary)?.locale ?? fallback;
285
-
286
231
  return [
287
- { locale: 'x-default', href: localeHref(path, neutral, fallback, available, domains, origin) },
232
+ { locale: 'x-default', href: `${ origin }${ withLocale(path, fallback, fallback, available) }` },
288
233
  ...available.map(locale => ({
289
234
  locale,
290
- href: localeHref(path, locale, fallback, available, domains, origin)
235
+ href: `${ origin }${ withLocale(path, locale, fallback, available) }`
291
236
  }))
292
237
  ];
293
238
  };
package/index.ts CHANGED
@@ -36,7 +36,7 @@ import LocaleGate from './Locale/LocaleGate';
36
36
  import SeoOverride from './SeoOverride/SeoOverride';
37
37
  import HandoffBanner from './HandoffBanner/HandoffBanner';
38
38
  import localiseRoutes from './Locale/routes';
39
- import { splitLocale, withLocale, localeFromPath, localeHref } from './Locale/locale';
39
+ import { splitLocale, withLocale, localeFromPath } from './Locale/locale';
40
40
  import RouteItem from './RouteItem/RouteItem';
41
41
  import Row from './Table/Row';
42
42
  import Table from './Table/Table';
@@ -91,13 +91,10 @@ export {
91
91
  splitLocale,
92
92
  withLocale,
93
93
  localeFromPath,
94
- localeHref,
95
94
  RouteItem,
96
95
  Row,
97
96
  Table,
98
97
  UnderConstruction,
99
98
  useUnderConstruction,
100
99
  Viewer
101
- };
102
-
103
- export type { LocaleDomain } from './Locale/locale';
100
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "1.27.0",
3
+ "version": "1.27.2",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"