@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 +32 -38
- package/Calendar/Calendar.tsx +23 -2
- package/ChangeLanguage/ChangeLanguage.tsx +2 -23
- package/Locale/locale.ts +0 -66
- package/Meta.tsx +4 -59
- package/index.ts +2 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,49 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.27.2
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
### Fixed
|
|
5
6
|
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
package/Calendar/Calendar.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 }${
|
|
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,
|
|
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
|
-
|
|
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:
|
|
232
|
+
{ locale: 'x-default', href: `${ origin }${ withLocale(path, fallback, fallback, available) }` },
|
|
288
233
|
...available.map(locale => ({
|
|
289
234
|
locale,
|
|
290
|
-
href:
|
|
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
|
|
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
|
+
};
|