@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
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
import { computed } from 'vue'
|
|
2
|
-
import { useDataPackage } from './useDataPackage.js'
|
|
3
|
-
import { byId as coreById, entityRef } from './useEntities.js'
|
|
4
|
-
import { md as coreMd, mdInline as coreMdInline, mdStrip as coreMdStrip } from '../i18n/markdown.js'
|
|
5
|
-
import { glossaryEntries } from '../record/recordSheet.js'
|
|
6
|
-
|
|
7
|
-
// The wrapper half of seven sites' data composables (2,376 lines across
|
|
8
|
-
// them), written once. Every one of them re-wrapped the same `tr`, the same
|
|
9
|
-
// memoised `loadEnglish`, the same one-shape label helper
|
|
10
|
-
// (`mdStrip(tr(entity, id).name ?? record.internal_name ?? id)`), and the
|
|
11
|
-
// same `md`/`mdInline`/`mdStrip` re-export, around whichever entities and
|
|
12
|
-
// visibility rules were its own — a per-build language filter, a hidden-
|
|
13
|
-
// partner rule, `display_status` — coded rather than declared. This is that
|
|
14
|
-
// wrapper. What survives in a site's own composable is what genuinely
|
|
15
|
-
// differs: routes, legacy key mappings, chrome images, sibling lists.
|
|
16
|
-
//
|
|
17
|
-
// Called once, at the top of a site's data composable module, and its
|
|
18
|
-
// return value re-exported from there — the way `entityRef`/`byId` are
|
|
19
|
-
// already read at module scope, so importing the module loads nothing and a
|
|
20
|
-
// page pays only for the entity it declares. `entity`/`index` read the same
|
|
21
|
-
// shared `entityRef`/`byId` this file does, so a second call would only
|
|
22
|
-
// duplicate the memoisation `loadEnglish` and the glossary binding carry —
|
|
23
|
-
// one instance per site, not one per component.
|
|
24
|
-
const BASE_LANGUAGE = 'en'
|
|
25
|
-
|
|
26
|
-
// A term's spellings and definition in one language, falling back to
|
|
27
|
-
// English, then to the term's own headword — the same row shape
|
|
28
|
-
// `useRecordSheet`'s `termRow` builds, but for the whole glossary in
|
|
29
|
-
// `defaultLanguage` rather than one record's `glossary_ids` in the record's
|
|
30
|
-
// own language, which is why it is not reused: a catalogue list or a label
|
|
31
|
-
// is always shown in `defaultLanguage`, a record sheet's text is not.
|
|
32
|
-
function glossaryRows(language, translations) {
|
|
33
|
-
const rows = translations('glossary', language)
|
|
34
|
-
const fallback = translations('glossary', BASE_LANGUAGE)
|
|
35
|
-
const terms = []
|
|
36
|
-
for (const term of entityRef('glossary').value ?? []) {
|
|
37
|
-
const t = rows[term.id] ?? fallback[term.id] ?? {}
|
|
38
|
-
const spellings = (t.spellings ?? []).map((s) => String(s).trim()).filter(Boolean)
|
|
39
|
-
const word = term.word ?? spellings[0] ?? ''
|
|
40
|
-
if (!word && spellings.length === 0) continue
|
|
41
|
-
terms.push({ id: term.id, word, definition: t.definition ?? '', spellings: spellings.length ? spellings : [word] })
|
|
42
|
-
}
|
|
43
|
-
return terms
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The wrapper half of a website's data composable: `tr`, `md`/`mdInline`/
|
|
48
|
-
* `mdStrip` bound to the site's glossary, `loadEnglish`, `labelOf`, the
|
|
49
|
-
* visible form of an entity and its index, and the package re-exports.
|
|
50
|
-
*
|
|
51
|
-
* `eager` names the entities `loadEnglish` loads — every entity the package
|
|
52
|
-
* carries by default. `defaultLanguage` is the language `tr`, the labels and
|
|
53
|
-
* the glossary binding read (`'en'` for every site so far). `visible` names,
|
|
54
|
-
* per entity, the predicate `entity()`/`index()` apply — a per-build
|
|
55
|
-
* language filter, a hidden-partner rule, a display-status check are all
|
|
56
|
-
* "which records this build shows", declared here once instead of coded
|
|
57
|
-
* into every page that lists that entity. `glossary` turns off the default
|
|
58
|
-
* binding on `md`/`mdInline` (a caller may still pass its own `glossary` to
|
|
59
|
-
* either, on or off).
|
|
60
|
-
*/
|
|
61
|
-
export function useCatalogueData({ eager, defaultLanguage = BASE_LANGUAGE, visible = {}, glossary = true } = {}) {
|
|
62
|
-
const pkg = useDataPackage()
|
|
63
|
-
const { availableLanguages, loadTranslations, translations } = pkg
|
|
64
|
-
const eagerEntities = eager ?? pkg.entityNames
|
|
65
|
-
|
|
66
|
-
/** One record's translated fields, falling back to `defaultLanguage` then `{}`. */
|
|
67
|
-
function tr(entity, id, lang = defaultLanguage) {
|
|
68
|
-
return pkg.tr(entity, id, lang, defaultLanguage)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const defaultGlossary = computed(() =>
|
|
72
|
-
glossary ? glossaryEntries(glossaryRows(defaultLanguage, translations)) : [],
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
// A caller's own `glossary` — a record's own terms, not every term in the
|
|
76
|
-
// package — always wins, whether the default binding is on or off; `md`
|
|
77
|
-
// and `mdInline` differ only in which core renderer they wrap, `mdStrip`
|
|
78
|
-
// has nothing to bind, since plain text carries no highlighting.
|
|
79
|
-
function bindGlossary(renderer) {
|
|
80
|
-
return (text, opts = {}) =>
|
|
81
|
-
renderer(text, { ...opts, glossary: 'glossary' in opts ? opts.glossary : defaultGlossary.value })
|
|
82
|
-
}
|
|
83
|
-
const md = bindGlossary(coreMd)
|
|
84
|
-
const mdInline = bindGlossary(coreMdInline)
|
|
85
|
-
const mdStrip = coreMdStrip
|
|
86
|
-
|
|
87
|
-
/** `mdStrip(tr(entity, id).name ?? record[fallback] ?? id)` — the one label shape every site wrote. */
|
|
88
|
-
function labelOf(entity, id, { fallback = 'internal_name' } = {}) {
|
|
89
|
-
if (!id) return ''
|
|
90
|
-
const name = tr(entity, id).name
|
|
91
|
-
if (name) return mdStrip(name)
|
|
92
|
-
const record = coreById(entity).value.get(id)
|
|
93
|
-
return mdStrip(record?.[fallback] ?? id)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
let englishReady = null
|
|
97
|
-
/** Loads `defaultLanguage` for every entity in `eager`, once. */
|
|
98
|
-
function loadEnglish() {
|
|
99
|
-
if (!englishReady) {
|
|
100
|
-
englishReady = Promise.all(eagerEntities.map((name) => loadTranslations(name, defaultLanguage)))
|
|
101
|
-
}
|
|
102
|
-
return englishReady
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Cached the way `byId` is: a site reads `entity('items')`/`index('items')`
|
|
106
|
-
// from more than one place, and each should see the same ref rather than
|
|
107
|
-
// a fresh filter pass and a fresh Map per call.
|
|
108
|
-
const entityCache = new Map()
|
|
109
|
-
/** `entityRef(name)`'s records, narrowed by `visible[name]` when one is declared; `null` until loaded. */
|
|
110
|
-
function entity(name) {
|
|
111
|
-
let ref = entityCache.get(name)
|
|
112
|
-
if (!ref) {
|
|
113
|
-
const source = entityRef(name)
|
|
114
|
-
const predicate = visible[name]
|
|
115
|
-
ref = computed(() => {
|
|
116
|
-
if (source.value === null) return null
|
|
117
|
-
return predicate ? source.value.filter(predicate) : source.value
|
|
118
|
-
})
|
|
119
|
-
entityCache.set(name, ref)
|
|
120
|
-
}
|
|
121
|
-
return ref
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const indexCache = new Map()
|
|
125
|
-
/** A `Map` of `entity(name)`'s records by `key` — the visible ones, unlike `byId`. */
|
|
126
|
-
function index(name, key = 'id') {
|
|
127
|
-
const cacheKey = `${name}.${key}`
|
|
128
|
-
let ref = indexCache.get(cacheKey)
|
|
129
|
-
if (!ref) {
|
|
130
|
-
const list = entity(name)
|
|
131
|
-
ref = computed(() => new Map((list.value ?? []).map((record) => [record[key], record])))
|
|
132
|
-
indexCache.set(cacheKey, ref)
|
|
133
|
-
}
|
|
134
|
-
return ref
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
tr, md, mdInline, mdStrip, loadEnglish, labelOf, entity, index,
|
|
139
|
-
availableLanguages, loadTranslations, translations,
|
|
140
|
-
}
|
|
141
|
-
}
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import { useDataPackage } from './useDataPackage.js'
|
|
3
|
+
import { byId as coreById, entityRef } from './useEntities.js'
|
|
4
|
+
import { md as coreMd, mdInline as coreMdInline, mdStrip as coreMdStrip } from '../i18n/markdown.js'
|
|
5
|
+
import { glossaryEntries } from '../record/recordSheet.js'
|
|
6
|
+
|
|
7
|
+
// The wrapper half of seven sites' data composables (2,376 lines across
|
|
8
|
+
// them), written once. Every one of them re-wrapped the same `tr`, the same
|
|
9
|
+
// memoised `loadEnglish`, the same one-shape label helper
|
|
10
|
+
// (`mdStrip(tr(entity, id).name ?? record.internal_name ?? id)`), and the
|
|
11
|
+
// same `md`/`mdInline`/`mdStrip` re-export, around whichever entities and
|
|
12
|
+
// visibility rules were its own — a per-build language filter, a hidden-
|
|
13
|
+
// partner rule, `display_status` — coded rather than declared. This is that
|
|
14
|
+
// wrapper. What survives in a site's own composable is what genuinely
|
|
15
|
+
// differs: routes, legacy key mappings, chrome images, sibling lists.
|
|
16
|
+
//
|
|
17
|
+
// Called once, at the top of a site's data composable module, and its
|
|
18
|
+
// return value re-exported from there — the way `entityRef`/`byId` are
|
|
19
|
+
// already read at module scope, so importing the module loads nothing and a
|
|
20
|
+
// page pays only for the entity it declares. `entity`/`index` read the same
|
|
21
|
+
// shared `entityRef`/`byId` this file does, so a second call would only
|
|
22
|
+
// duplicate the memoisation `loadEnglish` and the glossary binding carry —
|
|
23
|
+
// one instance per site, not one per component.
|
|
24
|
+
const BASE_LANGUAGE = 'en'
|
|
25
|
+
|
|
26
|
+
// A term's spellings and definition in one language, falling back to
|
|
27
|
+
// English, then to the term's own headword — the same row shape
|
|
28
|
+
// `useRecordSheet`'s `termRow` builds, but for the whole glossary in
|
|
29
|
+
// `defaultLanguage` rather than one record's `glossary_ids` in the record's
|
|
30
|
+
// own language, which is why it is not reused: a catalogue list or a label
|
|
31
|
+
// is always shown in `defaultLanguage`, a record sheet's text is not.
|
|
32
|
+
function glossaryRows(language, translations) {
|
|
33
|
+
const rows = translations('glossary', language)
|
|
34
|
+
const fallback = translations('glossary', BASE_LANGUAGE)
|
|
35
|
+
const terms = []
|
|
36
|
+
for (const term of entityRef('glossary').value ?? []) {
|
|
37
|
+
const t = rows[term.id] ?? fallback[term.id] ?? {}
|
|
38
|
+
const spellings = (t.spellings ?? []).map((s) => String(s).trim()).filter(Boolean)
|
|
39
|
+
const word = term.word ?? spellings[0] ?? ''
|
|
40
|
+
if (!word && spellings.length === 0) continue
|
|
41
|
+
terms.push({ id: term.id, word, definition: t.definition ?? '', spellings: spellings.length ? spellings : [word] })
|
|
42
|
+
}
|
|
43
|
+
return terms
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The wrapper half of a website's data composable: `tr`, `md`/`mdInline`/
|
|
48
|
+
* `mdStrip` bound to the site's glossary, `loadEnglish`, `labelOf`, the
|
|
49
|
+
* visible form of an entity and its index, and the package re-exports.
|
|
50
|
+
*
|
|
51
|
+
* `eager` names the entities `loadEnglish` loads — every entity the package
|
|
52
|
+
* carries by default. `defaultLanguage` is the language `tr`, the labels and
|
|
53
|
+
* the glossary binding read (`'en'` for every site so far). `visible` names,
|
|
54
|
+
* per entity, the predicate `entity()`/`index()` apply — a per-build
|
|
55
|
+
* language filter, a hidden-partner rule, a display-status check are all
|
|
56
|
+
* "which records this build shows", declared here once instead of coded
|
|
57
|
+
* into every page that lists that entity. `glossary` turns off the default
|
|
58
|
+
* binding on `md`/`mdInline` (a caller may still pass its own `glossary` to
|
|
59
|
+
* either, on or off).
|
|
60
|
+
*/
|
|
61
|
+
export function useCatalogueData({ eager, defaultLanguage = BASE_LANGUAGE, visible = {}, glossary = true } = {}) {
|
|
62
|
+
const pkg = useDataPackage()
|
|
63
|
+
const { availableLanguages, loadTranslations, translations } = pkg
|
|
64
|
+
const eagerEntities = eager ?? pkg.entityNames
|
|
65
|
+
|
|
66
|
+
/** One record's translated fields, falling back to `defaultLanguage` then `{}`. */
|
|
67
|
+
function tr(entity, id, lang = defaultLanguage) {
|
|
68
|
+
return pkg.tr(entity, id, lang, defaultLanguage)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const defaultGlossary = computed(() =>
|
|
72
|
+
glossary ? glossaryEntries(glossaryRows(defaultLanguage, translations)) : [],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// A caller's own `glossary` — a record's own terms, not every term in the
|
|
76
|
+
// package — always wins, whether the default binding is on or off; `md`
|
|
77
|
+
// and `mdInline` differ only in which core renderer they wrap, `mdStrip`
|
|
78
|
+
// has nothing to bind, since plain text carries no highlighting.
|
|
79
|
+
function bindGlossary(renderer) {
|
|
80
|
+
return (text, opts = {}) =>
|
|
81
|
+
renderer(text, { ...opts, glossary: 'glossary' in opts ? opts.glossary : defaultGlossary.value })
|
|
82
|
+
}
|
|
83
|
+
const md = bindGlossary(coreMd)
|
|
84
|
+
const mdInline = bindGlossary(coreMdInline)
|
|
85
|
+
const mdStrip = coreMdStrip
|
|
86
|
+
|
|
87
|
+
/** `mdStrip(tr(entity, id).name ?? record[fallback] ?? id)` — the one label shape every site wrote. */
|
|
88
|
+
function labelOf(entity, id, { fallback = 'internal_name' } = {}) {
|
|
89
|
+
if (!id) return ''
|
|
90
|
+
const name = tr(entity, id).name
|
|
91
|
+
if (name) return mdStrip(name)
|
|
92
|
+
const record = coreById(entity).value.get(id)
|
|
93
|
+
return mdStrip(record?.[fallback] ?? id)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let englishReady = null
|
|
97
|
+
/** Loads `defaultLanguage` for every entity in `eager`, once. */
|
|
98
|
+
function loadEnglish() {
|
|
99
|
+
if (!englishReady) {
|
|
100
|
+
englishReady = Promise.all(eagerEntities.map((name) => loadTranslations(name, defaultLanguage)))
|
|
101
|
+
}
|
|
102
|
+
return englishReady
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Cached the way `byId` is: a site reads `entity('items')`/`index('items')`
|
|
106
|
+
// from more than one place, and each should see the same ref rather than
|
|
107
|
+
// a fresh filter pass and a fresh Map per call.
|
|
108
|
+
const entityCache = new Map()
|
|
109
|
+
/** `entityRef(name)`'s records, narrowed by `visible[name]` when one is declared; `null` until loaded. */
|
|
110
|
+
function entity(name) {
|
|
111
|
+
let ref = entityCache.get(name)
|
|
112
|
+
if (!ref) {
|
|
113
|
+
const source = entityRef(name)
|
|
114
|
+
const predicate = visible[name]
|
|
115
|
+
ref = computed(() => {
|
|
116
|
+
if (source.value === null) return null
|
|
117
|
+
return predicate ? source.value.filter(predicate) : source.value
|
|
118
|
+
})
|
|
119
|
+
entityCache.set(name, ref)
|
|
120
|
+
}
|
|
121
|
+
return ref
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const indexCache = new Map()
|
|
125
|
+
/** A `Map` of `entity(name)`'s records by `key` — the visible ones, unlike `byId`. */
|
|
126
|
+
function index(name, key = 'id') {
|
|
127
|
+
const cacheKey = `${name}.${key}`
|
|
128
|
+
let ref = indexCache.get(cacheKey)
|
|
129
|
+
if (!ref) {
|
|
130
|
+
const list = entity(name)
|
|
131
|
+
ref = computed(() => new Map((list.value ?? []).map((record) => [record[key], record])))
|
|
132
|
+
indexCache.set(cacheKey, ref)
|
|
133
|
+
}
|
|
134
|
+
return ref
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
tr, md, mdInline, mdStrip, loadEnglish, labelOf, entity, index,
|
|
139
|
+
availableLanguages, loadTranslations, translations,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
import { ref } from 'vue'
|
|
2
|
-
|
|
3
|
-
// package.json is npm metadata, not an entity — exclude it from the glob.
|
|
4
|
-
const entityModules = import.meta.glob(['@inventory-data/*.json', '!**/package.json'])
|
|
5
|
-
const manifestModules = import.meta.glob('@inventory-data/manifest.json', { eager: true })
|
|
6
|
-
|
|
7
|
-
// One file per entity per language; a file is simply absent when that entity
|
|
8
|
-
// has no translation in that language. Left lazy (unlike entityModules'
|
|
9
|
-
// eager-in-spirit-but-actually-per-call loaders, this one also needs a
|
|
10
|
-
// reactive cache — see below) so a build only bundles what a page visit
|
|
11
|
-
// actually requests, not every language of every entity.
|
|
12
|
-
const translationModules = import.meta.glob('@inventory-data/translations/*.json')
|
|
13
|
-
|
|
14
|
-
const manifest = Object.values(manifestModules)[0]?.default ?? {}
|
|
15
|
-
|
|
16
|
-
const entityLoaders = {}
|
|
17
|
-
for (const [path, load] of Object.entries(entityModules)) {
|
|
18
|
-
const name = path.split('/').pop().replace(/\.json$/, '')
|
|
19
|
-
if (name !== 'manifest') {
|
|
20
|
-
entityLoaders[name] = load
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function translationLoader(entity, lang) {
|
|
25
|
-
const suffix = `/translations/${entity}.${lang}.json`
|
|
26
|
-
const path = Object.keys(translationModules).find((k) => k.endsWith(suffix))
|
|
27
|
-
return path ? translationModules[path] : null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// `${entity}.${lang}` → record map, or `{}` once a miss has been resolved.
|
|
31
|
-
// A plain `await import()` would already be deduped by the module system;
|
|
32
|
-
// this cache exists so a component reading through `translations`/`tr` -
|
|
33
|
-
// synchronous, not awaited - re-renders once a lazy `loadTranslations` call
|
|
34
|
-
// elsewhere resolves.
|
|
35
|
-
const translationCache = ref({})
|
|
36
|
-
|
|
37
|
-
export function useDataPackage() {
|
|
38
|
-
return {
|
|
39
|
-
manifest,
|
|
40
|
-
languages: manifest.languages ?? ['en'],
|
|
41
|
-
entityNames: Object.keys(entityLoaders).sort(),
|
|
42
|
-
async loadEntity(name) {
|
|
43
|
-
const load = entityLoaders[name]
|
|
44
|
-
if (!load) {
|
|
45
|
-
throw new Error(`Unknown entity "${name}" in the data package`)
|
|
46
|
-
}
|
|
47
|
-
const module = await load()
|
|
48
|
-
return module.default ?? module
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
/** Languages `translations/<entity>.<lang>.json` actually exists for. */
|
|
52
|
-
availableLanguages(entity) {
|
|
53
|
-
const prefix = `/translations/${entity}.`
|
|
54
|
-
return Object.keys(translationModules)
|
|
55
|
-
.filter((k) => k.includes(prefix))
|
|
56
|
-
.map((k) => k.slice(k.lastIndexOf(prefix) + prefix.length, -'.json'.length))
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Lazy-loads `translations/<entity>.<lang>.json` (a map of record id to
|
|
61
|
-
* that record's translated fields); `{}` when the file does not exist.
|
|
62
|
-
* Cached, so calling it again for the same pair is free.
|
|
63
|
-
*/
|
|
64
|
-
async loadTranslations(entity, lang) {
|
|
65
|
-
const key = `${entity}.${lang}`
|
|
66
|
-
if (translationCache.value[key]) return translationCache.value[key]
|
|
67
|
-
const load = translationLoader(entity, lang)
|
|
68
|
-
let data = {}
|
|
69
|
-
if (load) {
|
|
70
|
-
try {
|
|
71
|
-
data = (await load()).default ?? {}
|
|
72
|
-
} catch {
|
|
73
|
-
data = {}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
translationCache.value = { ...translationCache.value, [key]: data }
|
|
77
|
-
return data
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
/** Already-loaded translations for one entity/lang; `{}` if not loaded yet (or absent). */
|
|
81
|
-
translations(entity, lang) {
|
|
82
|
-
return translationCache.value[`${entity}.${lang}`] ?? {}
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* One record's translated fields, falling back to `fallbackLang` (default
|
|
87
|
-
* `'en'`) then to `{}`. Reads only what is already loaded — call
|
|
88
|
-
* `loadTranslations` first for each language a page may need.
|
|
89
|
-
*/
|
|
90
|
-
tr(entity, id, lang, fallbackLang = 'en') {
|
|
91
|
-
return (
|
|
92
|
-
translationCache.value[`${entity}.${lang}`]?.[id] ??
|
|
93
|
-
translationCache.value[`${entity}.${fallbackLang}`]?.[id] ??
|
|
94
|
-
{}
|
|
95
|
-
)
|
|
96
|
-
},
|
|
97
|
-
}
|
|
98
|
-
}
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
// package.json is npm metadata, not an entity — exclude it from the glob.
|
|
4
|
+
const entityModules = import.meta.glob(['@inventory-data/*.json', '!**/package.json'])
|
|
5
|
+
const manifestModules = import.meta.glob('@inventory-data/manifest.json', { eager: true })
|
|
6
|
+
|
|
7
|
+
// One file per entity per language; a file is simply absent when that entity
|
|
8
|
+
// has no translation in that language. Left lazy (unlike entityModules'
|
|
9
|
+
// eager-in-spirit-but-actually-per-call loaders, this one also needs a
|
|
10
|
+
// reactive cache — see below) so a build only bundles what a page visit
|
|
11
|
+
// actually requests, not every language of every entity.
|
|
12
|
+
const translationModules = import.meta.glob('@inventory-data/translations/*.json')
|
|
13
|
+
|
|
14
|
+
const manifest = Object.values(manifestModules)[0]?.default ?? {}
|
|
15
|
+
|
|
16
|
+
const entityLoaders = {}
|
|
17
|
+
for (const [path, load] of Object.entries(entityModules)) {
|
|
18
|
+
const name = path.split('/').pop().replace(/\.json$/, '')
|
|
19
|
+
if (name !== 'manifest') {
|
|
20
|
+
entityLoaders[name] = load
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function translationLoader(entity, lang) {
|
|
25
|
+
const suffix = `/translations/${entity}.${lang}.json`
|
|
26
|
+
const path = Object.keys(translationModules).find((k) => k.endsWith(suffix))
|
|
27
|
+
return path ? translationModules[path] : null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// `${entity}.${lang}` → record map, or `{}` once a miss has been resolved.
|
|
31
|
+
// A plain `await import()` would already be deduped by the module system;
|
|
32
|
+
// this cache exists so a component reading through `translations`/`tr` -
|
|
33
|
+
// synchronous, not awaited - re-renders once a lazy `loadTranslations` call
|
|
34
|
+
// elsewhere resolves.
|
|
35
|
+
const translationCache = ref({})
|
|
36
|
+
|
|
37
|
+
export function useDataPackage() {
|
|
38
|
+
return {
|
|
39
|
+
manifest,
|
|
40
|
+
languages: manifest.languages ?? ['en'],
|
|
41
|
+
entityNames: Object.keys(entityLoaders).sort(),
|
|
42
|
+
async loadEntity(name) {
|
|
43
|
+
const load = entityLoaders[name]
|
|
44
|
+
if (!load) {
|
|
45
|
+
throw new Error(`Unknown entity "${name}" in the data package`)
|
|
46
|
+
}
|
|
47
|
+
const module = await load()
|
|
48
|
+
return module.default ?? module
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/** Languages `translations/<entity>.<lang>.json` actually exists for. */
|
|
52
|
+
availableLanguages(entity) {
|
|
53
|
+
const prefix = `/translations/${entity}.`
|
|
54
|
+
return Object.keys(translationModules)
|
|
55
|
+
.filter((k) => k.includes(prefix))
|
|
56
|
+
.map((k) => k.slice(k.lastIndexOf(prefix) + prefix.length, -'.json'.length))
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lazy-loads `translations/<entity>.<lang>.json` (a map of record id to
|
|
61
|
+
* that record's translated fields); `{}` when the file does not exist.
|
|
62
|
+
* Cached, so calling it again for the same pair is free.
|
|
63
|
+
*/
|
|
64
|
+
async loadTranslations(entity, lang) {
|
|
65
|
+
const key = `${entity}.${lang}`
|
|
66
|
+
if (translationCache.value[key]) return translationCache.value[key]
|
|
67
|
+
const load = translationLoader(entity, lang)
|
|
68
|
+
let data = {}
|
|
69
|
+
if (load) {
|
|
70
|
+
try {
|
|
71
|
+
data = (await load()).default ?? {}
|
|
72
|
+
} catch {
|
|
73
|
+
data = {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
translationCache.value = { ...translationCache.value, [key]: data }
|
|
77
|
+
return data
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/** Already-loaded translations for one entity/lang; `{}` if not loaded yet (or absent). */
|
|
81
|
+
translations(entity, lang) {
|
|
82
|
+
return translationCache.value[`${entity}.${lang}`] ?? {}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* One record's translated fields, falling back to `fallbackLang` (default
|
|
87
|
+
* `'en'`) then to `{}`. Reads only what is already loaded — call
|
|
88
|
+
* `loadTranslations` first for each language a page may need.
|
|
89
|
+
*/
|
|
90
|
+
tr(entity, id, lang, fallbackLang = 'en') {
|
|
91
|
+
return (
|
|
92
|
+
translationCache.value[`${entity}.${lang}`]?.[id] ??
|
|
93
|
+
translationCache.value[`${entity}.${fallbackLang}`]?.[id] ??
|
|
94
|
+
{}
|
|
95
|
+
)
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|