@museumwnf/viewer-core 1.14.0 → 1.15.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/LICENSE.md +44 -44
- package/README.md +658 -635
- package/package.json +50 -50
- package/src/AppRoot.vue +58 -58
- package/src/catalogue/dates.js +209 -209
- package/src/catalogue/facets.js +61 -61
- package/src/catalogue/keywordIndex.js +289 -289
- package/src/catalogue/listQuery.js +100 -100
- package/src/catalogue/pagination.js +57 -57
- package/src/catalogue/searchLanguage.js +32 -32
- package/src/catalogue/timeline.js +220 -220
- package/src/composables/useCatalogueData.js +141 -141
- package/src/composables/useDataPackage.js +98 -98
- package/src/composables/useEntities.js +100 -100
- package/src/composables/useRecordLanguage.js +71 -71
- package/src/conventions.js +252 -148
- package/src/createViewer.js +29 -29
- package/src/i18n/I18nText.vue +17 -17
- package/src/i18n/I18nTextInline.vue +17 -17
- package/src/i18n/index.js +88 -88
- package/src/i18n/language.js +127 -127
- package/src/i18n/markdown.js +265 -265
- package/src/index.js +51 -50
- package/src/injectionKeys.js +1 -1
- package/src/languages.js +49 -49
- package/src/legacy/index.js +58 -58
- package/src/record/collectionTree.js +307 -307
- package/src/record/derivations.js +154 -154
- package/src/record/partners.js +87 -87
- package/src/record/recordSheet.js +231 -231
- package/src/rights.js +53 -53
- package/src/router/index.js +138 -138
- package/src/siteConfig.js +37 -37
- package/src/styles/base.css +49 -49
- package/src/testing/checkOfferedLanguages.js +67 -67
- package/src/testing/index.js +3 -3
- package/src/testing/smoke.js +103 -103
- package/src/testing/viteConfig.js +90 -90
- package/src/views/DetailView.vue +53 -53
- package/src/views/HomeView.vue +20 -20
- package/src/views/ListView.vue +61 -61
- package/src/views/NotFoundView.vue +8 -8
- package/src/vite.js +8 -8
package/src/i18n/index.js
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
import { computed, inject, ref } from 'vue'
|
|
2
|
-
|
|
3
|
-
// The renderers travel with the texts: a package that has only this entry
|
|
4
|
-
// point (@museumwnf/viewer-layout) renders a Markdown prop through the same
|
|
5
|
-
// pipeline as everything else.
|
|
6
|
-
export { md, mdInline, mdStrip, renderBlock, renderInline, renderPlain } from './markdown.js'
|
|
7
|
-
|
|
8
|
-
// The whole of it: look a key up in the active language, fall back to English,
|
|
9
|
-
// and return the key itself when neither has it, so a missing text shows up as
|
|
10
|
-
// its own name rather than as a blank page.
|
|
11
|
-
//
|
|
12
|
-
// The scope of this module is frozen. It does lookup, fallback and reactivity;
|
|
13
|
-
// it will never grow interpolation, pluralisation or a message format. A text
|
|
14
|
-
// is a text — a value produced at run time is rendered by the component next
|
|
15
|
-
// to the text, never inside it. A future need outside that is a different
|
|
16
|
-
// system, not an extension of this one.
|
|
17
|
-
|
|
18
|
-
// Symbol.for, not Symbol: this module is reachable through two specifiers —
|
|
19
|
-
// `@museumwnf/viewer-core` (which createViewer imports relatively) and
|
|
20
|
-
// `@museumwnf/viewer-core/i18n` (which viewer-layout imports) — and a bundler
|
|
21
|
-
// that resolves them to two module instances would create two distinct keys.
|
|
22
|
-
// The application would then provide one and the layout inject the other,
|
|
23
|
-
// which reads as "no texts installed" in a package that plainly installed
|
|
24
|
-
// them. A key from the global registry is the same key in every instance.
|
|
25
|
-
export const VIEWER_I18N = Symbol.for('@museumwnf/viewer-core:i18n')
|
|
26
|
-
|
|
27
|
-
const BASE_LANGUAGE = 'en'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Apply the one merge rule of the platform: local wins.
|
|
31
|
-
*
|
|
32
|
-
* Each argument is `{ <language>: { <key>: <text> } }` — the shared bundle
|
|
33
|
-
* first, the website's own file last. Keys carry their namespace, so nothing
|
|
34
|
-
* collides by accident; an entry is overloaded only when a website spells out
|
|
35
|
-
* the same name.
|
|
36
|
-
*/
|
|
37
|
-
export function mergeMessages(...catalogues) {
|
|
38
|
-
const merged = {}
|
|
39
|
-
for (const catalogue of catalogues) {
|
|
40
|
-
for (const [language, entries] of Object.entries(catalogue ?? {})) {
|
|
41
|
-
merged[language] = { ...merged[language], ...entries }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return merged
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function createI18n({ messages = {}, locale = BASE_LANGUAGE } = {}) {
|
|
48
|
-
const current = ref(locale)
|
|
49
|
-
|
|
50
|
-
const t = (key) => {
|
|
51
|
-
const active = messages[current.value]
|
|
52
|
-
if (active && typeof active[key] === 'string') return active[key]
|
|
53
|
-
const base = messages[BASE_LANGUAGE]
|
|
54
|
-
if (base && typeof base[key] === 'string') return base[key]
|
|
55
|
-
return key
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const i18n = {
|
|
59
|
-
locale: current,
|
|
60
|
-
availableLocales: Object.keys(messages),
|
|
61
|
-
t,
|
|
62
|
-
install(app) {
|
|
63
|
-
app.provide(VIEWER_I18N, i18n)
|
|
64
|
-
// `$t` in a template, `useI18n()` in a script — the two shapes Vue
|
|
65
|
-
// applications already expect of a translation helper.
|
|
66
|
-
app.config.globalProperties.$t = t
|
|
67
|
-
},
|
|
68
|
-
}
|
|
69
|
-
return i18n
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function useI18n() {
|
|
73
|
-
const i18n = inject(VIEWER_I18N, null)
|
|
74
|
-
if (!i18n) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
'useI18n() was called in an application that has no texts installed. ' +
|
|
77
|
-
'Applications built with createViewer() have them; a test mounting a ' +
|
|
78
|
-
'component on its own must install createI18n({ messages }) itself.'
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
return i18n
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The active language, for a component that only needs to read it. */
|
|
85
|
-
export function useLocale() {
|
|
86
|
-
const { locale } = useI18n()
|
|
87
|
-
return computed(() => locale.value)
|
|
88
|
-
}
|
|
1
|
+
import { computed, inject, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
// The renderers travel with the texts: a package that has only this entry
|
|
4
|
+
// point (@museumwnf/viewer-layout) renders a Markdown prop through the same
|
|
5
|
+
// pipeline as everything else.
|
|
6
|
+
export { md, mdInline, mdStrip, renderBlock, renderInline, renderPlain } from './markdown.js'
|
|
7
|
+
|
|
8
|
+
// The whole of it: look a key up in the active language, fall back to English,
|
|
9
|
+
// and return the key itself when neither has it, so a missing text shows up as
|
|
10
|
+
// its own name rather than as a blank page.
|
|
11
|
+
//
|
|
12
|
+
// The scope of this module is frozen. It does lookup, fallback and reactivity;
|
|
13
|
+
// it will never grow interpolation, pluralisation or a message format. A text
|
|
14
|
+
// is a text — a value produced at run time is rendered by the component next
|
|
15
|
+
// to the text, never inside it. A future need outside that is a different
|
|
16
|
+
// system, not an extension of this one.
|
|
17
|
+
|
|
18
|
+
// Symbol.for, not Symbol: this module is reachable through two specifiers —
|
|
19
|
+
// `@museumwnf/viewer-core` (which createViewer imports relatively) and
|
|
20
|
+
// `@museumwnf/viewer-core/i18n` (which viewer-layout imports) — and a bundler
|
|
21
|
+
// that resolves them to two module instances would create two distinct keys.
|
|
22
|
+
// The application would then provide one and the layout inject the other,
|
|
23
|
+
// which reads as "no texts installed" in a package that plainly installed
|
|
24
|
+
// them. A key from the global registry is the same key in every instance.
|
|
25
|
+
export const VIEWER_I18N = Symbol.for('@museumwnf/viewer-core:i18n')
|
|
26
|
+
|
|
27
|
+
const BASE_LANGUAGE = 'en'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Apply the one merge rule of the platform: local wins.
|
|
31
|
+
*
|
|
32
|
+
* Each argument is `{ <language>: { <key>: <text> } }` — the shared bundle
|
|
33
|
+
* first, the website's own file last. Keys carry their namespace, so nothing
|
|
34
|
+
* collides by accident; an entry is overloaded only when a website spells out
|
|
35
|
+
* the same name.
|
|
36
|
+
*/
|
|
37
|
+
export function mergeMessages(...catalogues) {
|
|
38
|
+
const merged = {}
|
|
39
|
+
for (const catalogue of catalogues) {
|
|
40
|
+
for (const [language, entries] of Object.entries(catalogue ?? {})) {
|
|
41
|
+
merged[language] = { ...merged[language], ...entries }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return merged
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createI18n({ messages = {}, locale = BASE_LANGUAGE } = {}) {
|
|
48
|
+
const current = ref(locale)
|
|
49
|
+
|
|
50
|
+
const t = (key) => {
|
|
51
|
+
const active = messages[current.value]
|
|
52
|
+
if (active && typeof active[key] === 'string') return active[key]
|
|
53
|
+
const base = messages[BASE_LANGUAGE]
|
|
54
|
+
if (base && typeof base[key] === 'string') return base[key]
|
|
55
|
+
return key
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const i18n = {
|
|
59
|
+
locale: current,
|
|
60
|
+
availableLocales: Object.keys(messages),
|
|
61
|
+
t,
|
|
62
|
+
install(app) {
|
|
63
|
+
app.provide(VIEWER_I18N, i18n)
|
|
64
|
+
// `$t` in a template, `useI18n()` in a script — the two shapes Vue
|
|
65
|
+
// applications already expect of a translation helper.
|
|
66
|
+
app.config.globalProperties.$t = t
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
return i18n
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useI18n() {
|
|
73
|
+
const i18n = inject(VIEWER_I18N, null)
|
|
74
|
+
if (!i18n) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'useI18n() was called in an application that has no texts installed. ' +
|
|
77
|
+
'Applications built with createViewer() have them; a test mounting a ' +
|
|
78
|
+
'component on its own must install createI18n({ messages }) itself.'
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
return i18n
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The active language, for a component that only needs to read it. */
|
|
85
|
+
export function useLocale() {
|
|
86
|
+
const { locale } = useI18n()
|
|
87
|
+
return computed(() => locale.value)
|
|
88
|
+
}
|
package/src/i18n/language.js
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
import { watch } from 'vue'
|
|
2
|
-
import { START_LOCATION } from 'vue-router'
|
|
3
|
-
|
|
4
|
-
// Which language a visitor gets, decided once, in one place.
|
|
5
|
-
//
|
|
6
|
-
// Before this, a website opened at `languages[0]` and stayed there: no
|
|
7
|
-
// negotiation, no memory between visits, nothing in the URL, and no `lang` or
|
|
8
|
-
// `dir` on the document — so a right-to-left language rendered left to right.
|
|
9
|
-
|
|
10
|
-
const STORAGE_KEY = 'mwnf.language'
|
|
11
|
-
|
|
12
|
-
export const RTL_LANGUAGES = new Set([
|
|
13
|
-
'ar', 'arc', 'ckb', 'dv', 'fa', 'he', 'ku', 'ps', 'sd', 'ug', 'ur', 'yi',
|
|
14
|
-
])
|
|
15
|
-
|
|
16
|
-
export function isRtl(code) {
|
|
17
|
-
return RTL_LANGUAGES.has(String(code ?? '').toLowerCase().split('-')[0])
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The URL first, then the visitor's own earlier choice, then what their
|
|
22
|
-
* browser asks for, then English. A language that is not offered is ignored at
|
|
23
|
-
* every step, so nothing here can select a language the website does not have.
|
|
24
|
-
*/
|
|
25
|
-
export function negotiateLanguage(offered, { requested, stored, preferred = [] } = {}) {
|
|
26
|
-
const list = offered?.length ? offered : ['en']
|
|
27
|
-
const match = (code) => {
|
|
28
|
-
if (!code) return undefined
|
|
29
|
-
const wanted = String(code).toLowerCase()
|
|
30
|
-
return (
|
|
31
|
-
list.find((candidate) => candidate.toLowerCase() === wanted) ??
|
|
32
|
-
list.find((candidate) => candidate.toLowerCase().split('-')[0] === wanted.split('-')[0])
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
return (
|
|
36
|
-
match(requested) ??
|
|
37
|
-
match(stored) ??
|
|
38
|
-
preferred.map(match).find(Boolean) ??
|
|
39
|
-
match('en') ??
|
|
40
|
-
list[0]
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function readStoredLanguage() {
|
|
45
|
-
try {
|
|
46
|
-
return globalThis.localStorage?.getItem(STORAGE_KEY) ?? null
|
|
47
|
-
} catch {
|
|
48
|
-
// A browser refusing storage is not a reason to fail to render a page.
|
|
49
|
-
return null
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function storeLanguage(code) {
|
|
54
|
-
try {
|
|
55
|
-
globalThis.localStorage?.setItem(STORAGE_KEY, code)
|
|
56
|
-
} catch {
|
|
57
|
-
/* see readStoredLanguage */
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function applyDocumentLanguage(code) {
|
|
62
|
-
if (typeof document === 'undefined') return
|
|
63
|
-
document.documentElement.setAttribute('lang', code)
|
|
64
|
-
document.documentElement.setAttribute('dir', isRtl(code) ? 'rtl' : 'ltr')
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function browserLanguages() {
|
|
68
|
-
if (typeof navigator === 'undefined') return []
|
|
69
|
-
return navigator.languages ?? (navigator.language ? [navigator.language] : [])
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The language to start in, before the router has resolved a route. The URL is
|
|
74
|
-
* deliberately not read here — the router owns it, and reading it twice is how
|
|
75
|
-
* the two disagree.
|
|
76
|
-
*/
|
|
77
|
-
export function resolveInitialLanguage(offered) {
|
|
78
|
-
return negotiateLanguage(offered, {
|
|
79
|
-
stored: readStoredLanguage(),
|
|
80
|
-
preferred: browserLanguages(),
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* On a website offering more than one language the URL is the source of truth:
|
|
86
|
-
* `?lang=` carries it, so a page can be linked, bookmarked and shared in the
|
|
87
|
-
* language it was read in. A single-language website never gets the parameter.
|
|
88
|
-
*
|
|
89
|
-
* It is a query parameter rather than a path segment because the legacy routes
|
|
90
|
-
* these websites reproduce are matched by path: prefixing them would break
|
|
91
|
-
* every URL that has ever been published.
|
|
92
|
-
*/
|
|
93
|
-
export function connectLanguageToRouter({ locale, offered, router }) {
|
|
94
|
-
const multilingual = offered.length > 1
|
|
95
|
-
|
|
96
|
-
router.beforeEach((to) => {
|
|
97
|
-
const next = negotiateLanguage(offered, {
|
|
98
|
-
requested: multilingual ? to.query.lang : undefined,
|
|
99
|
-
stored: readStoredLanguage(),
|
|
100
|
-
preferred: browserLanguages(),
|
|
101
|
-
})
|
|
102
|
-
if (locale.value !== next) locale.value = next
|
|
103
|
-
if (multilingual && to.query.lang !== next) {
|
|
104
|
-
// Resolves on the second pass, when the query already says `next`.
|
|
105
|
-
return { ...to, query: { ...to.query, lang: next }, replace: true }
|
|
106
|
-
}
|
|
107
|
-
return true
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
// A change of language from the switcher rewrites the URL. Not before the
|
|
111
|
-
// first navigation has resolved, though: the router is then still at its
|
|
112
|
-
// start location, `/`, and a replace from there cancels the navigation the
|
|
113
|
-
// visitor arrived with — every deep link landed on the home page. The guard
|
|
114
|
-
// above puts the language in the URL of that first navigation itself.
|
|
115
|
-
watch(
|
|
116
|
-
locale,
|
|
117
|
-
(code) => {
|
|
118
|
-
applyDocumentLanguage(code)
|
|
119
|
-
storeLanguage(code)
|
|
120
|
-
const current = router.currentRoute.value
|
|
121
|
-
if (multilingual && current !== START_LOCATION && current.query.lang !== code) {
|
|
122
|
-
router.replace({ query: { ...current.query, lang: code } })
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
{ immediate: true }
|
|
126
|
-
)
|
|
127
|
-
}
|
|
1
|
+
import { watch } from 'vue'
|
|
2
|
+
import { START_LOCATION } from 'vue-router'
|
|
3
|
+
|
|
4
|
+
// Which language a visitor gets, decided once, in one place.
|
|
5
|
+
//
|
|
6
|
+
// Before this, a website opened at `languages[0]` and stayed there: no
|
|
7
|
+
// negotiation, no memory between visits, nothing in the URL, and no `lang` or
|
|
8
|
+
// `dir` on the document — so a right-to-left language rendered left to right.
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'mwnf.language'
|
|
11
|
+
|
|
12
|
+
export const RTL_LANGUAGES = new Set([
|
|
13
|
+
'ar', 'arc', 'ckb', 'dv', 'fa', 'he', 'ku', 'ps', 'sd', 'ug', 'ur', 'yi',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
export function isRtl(code) {
|
|
17
|
+
return RTL_LANGUAGES.has(String(code ?? '').toLowerCase().split('-')[0])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The URL first, then the visitor's own earlier choice, then what their
|
|
22
|
+
* browser asks for, then English. A language that is not offered is ignored at
|
|
23
|
+
* every step, so nothing here can select a language the website does not have.
|
|
24
|
+
*/
|
|
25
|
+
export function negotiateLanguage(offered, { requested, stored, preferred = [] } = {}) {
|
|
26
|
+
const list = offered?.length ? offered : ['en']
|
|
27
|
+
const match = (code) => {
|
|
28
|
+
if (!code) return undefined
|
|
29
|
+
const wanted = String(code).toLowerCase()
|
|
30
|
+
return (
|
|
31
|
+
list.find((candidate) => candidate.toLowerCase() === wanted) ??
|
|
32
|
+
list.find((candidate) => candidate.toLowerCase().split('-')[0] === wanted.split('-')[0])
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return (
|
|
36
|
+
match(requested) ??
|
|
37
|
+
match(stored) ??
|
|
38
|
+
preferred.map(match).find(Boolean) ??
|
|
39
|
+
match('en') ??
|
|
40
|
+
list[0]
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readStoredLanguage() {
|
|
45
|
+
try {
|
|
46
|
+
return globalThis.localStorage?.getItem(STORAGE_KEY) ?? null
|
|
47
|
+
} catch {
|
|
48
|
+
// A browser refusing storage is not a reason to fail to render a page.
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function storeLanguage(code) {
|
|
54
|
+
try {
|
|
55
|
+
globalThis.localStorage?.setItem(STORAGE_KEY, code)
|
|
56
|
+
} catch {
|
|
57
|
+
/* see readStoredLanguage */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function applyDocumentLanguage(code) {
|
|
62
|
+
if (typeof document === 'undefined') return
|
|
63
|
+
document.documentElement.setAttribute('lang', code)
|
|
64
|
+
document.documentElement.setAttribute('dir', isRtl(code) ? 'rtl' : 'ltr')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function browserLanguages() {
|
|
68
|
+
if (typeof navigator === 'undefined') return []
|
|
69
|
+
return navigator.languages ?? (navigator.language ? [navigator.language] : [])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The language to start in, before the router has resolved a route. The URL is
|
|
74
|
+
* deliberately not read here — the router owns it, and reading it twice is how
|
|
75
|
+
* the two disagree.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveInitialLanguage(offered) {
|
|
78
|
+
return negotiateLanguage(offered, {
|
|
79
|
+
stored: readStoredLanguage(),
|
|
80
|
+
preferred: browserLanguages(),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* On a website offering more than one language the URL is the source of truth:
|
|
86
|
+
* `?lang=` carries it, so a page can be linked, bookmarked and shared in the
|
|
87
|
+
* language it was read in. A single-language website never gets the parameter.
|
|
88
|
+
*
|
|
89
|
+
* It is a query parameter rather than a path segment because the legacy routes
|
|
90
|
+
* these websites reproduce are matched by path: prefixing them would break
|
|
91
|
+
* every URL that has ever been published.
|
|
92
|
+
*/
|
|
93
|
+
export function connectLanguageToRouter({ locale, offered, router }) {
|
|
94
|
+
const multilingual = offered.length > 1
|
|
95
|
+
|
|
96
|
+
router.beforeEach((to) => {
|
|
97
|
+
const next = negotiateLanguage(offered, {
|
|
98
|
+
requested: multilingual ? to.query.lang : undefined,
|
|
99
|
+
stored: readStoredLanguage(),
|
|
100
|
+
preferred: browserLanguages(),
|
|
101
|
+
})
|
|
102
|
+
if (locale.value !== next) locale.value = next
|
|
103
|
+
if (multilingual && to.query.lang !== next) {
|
|
104
|
+
// Resolves on the second pass, when the query already says `next`.
|
|
105
|
+
return { ...to, query: { ...to.query, lang: next }, replace: true }
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// A change of language from the switcher rewrites the URL. Not before the
|
|
111
|
+
// first navigation has resolved, though: the router is then still at its
|
|
112
|
+
// start location, `/`, and a replace from there cancels the navigation the
|
|
113
|
+
// visitor arrived with — every deep link landed on the home page. The guard
|
|
114
|
+
// above puts the language in the URL of that first navigation itself.
|
|
115
|
+
watch(
|
|
116
|
+
locale,
|
|
117
|
+
(code) => {
|
|
118
|
+
applyDocumentLanguage(code)
|
|
119
|
+
storeLanguage(code)
|
|
120
|
+
const current = router.currentRoute.value
|
|
121
|
+
if (multilingual && current !== START_LOCATION && current.query.lang !== code) {
|
|
122
|
+
router.replace({ query: { ...current.query, lang: code } })
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{ immediate: true }
|
|
126
|
+
)
|
|
127
|
+
}
|