@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.
Files changed (43) hide show
  1. package/LICENSE.md +44 -44
  2. package/README.md +658 -635
  3. package/package.json +50 -50
  4. package/src/AppRoot.vue +58 -58
  5. package/src/catalogue/dates.js +209 -209
  6. package/src/catalogue/facets.js +61 -61
  7. package/src/catalogue/keywordIndex.js +289 -289
  8. package/src/catalogue/listQuery.js +100 -100
  9. package/src/catalogue/pagination.js +57 -57
  10. package/src/catalogue/searchLanguage.js +32 -32
  11. package/src/catalogue/timeline.js +220 -220
  12. package/src/composables/useCatalogueData.js +141 -141
  13. package/src/composables/useDataPackage.js +98 -98
  14. package/src/composables/useEntities.js +100 -100
  15. package/src/composables/useRecordLanguage.js +71 -71
  16. package/src/conventions.js +252 -148
  17. package/src/createViewer.js +29 -29
  18. package/src/i18n/I18nText.vue +17 -17
  19. package/src/i18n/I18nTextInline.vue +17 -17
  20. package/src/i18n/index.js +88 -88
  21. package/src/i18n/language.js +127 -127
  22. package/src/i18n/markdown.js +265 -265
  23. package/src/index.js +51 -50
  24. package/src/injectionKeys.js +1 -1
  25. package/src/languages.js +49 -49
  26. package/src/legacy/index.js +58 -58
  27. package/src/record/collectionTree.js +307 -307
  28. package/src/record/derivations.js +154 -154
  29. package/src/record/partners.js +87 -87
  30. package/src/record/recordSheet.js +231 -231
  31. package/src/rights.js +53 -53
  32. package/src/router/index.js +138 -138
  33. package/src/siteConfig.js +37 -37
  34. package/src/styles/base.css +49 -49
  35. package/src/testing/checkOfferedLanguages.js +67 -67
  36. package/src/testing/index.js +3 -3
  37. package/src/testing/smoke.js +103 -103
  38. package/src/testing/viteConfig.js +90 -90
  39. package/src/views/DetailView.vue +53 -53
  40. package/src/views/HomeView.vue +20 -20
  41. package/src/views/ListView.vue +61 -61
  42. package/src/views/NotFoundView.vue +8 -8
  43. 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
+ }
@@ -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
+ }