@glossarist/concept-browser 0.5.1 → 0.7.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/cli/index.mjs +17 -0
- package/package.json +1 -1
- package/scripts/generate-data.mjs +49 -0
- package/src/App.vue +3 -0
- package/src/__tests__/markdown-lite.test.ts +26 -0
- package/src/components/AppFooter.vue +3 -1
- package/src/components/AppHeader.vue +48 -6
- package/src/components/AppSidebar.vue +17 -8
- package/src/components/ConceptCard.vue +5 -2
- package/src/components/ConceptDetail.vue +14 -11
- package/src/components/GraphPanel.vue +18 -16
- package/src/components/LanguageDetail.vue +11 -8
- package/src/components/SearchBar.vue +12 -11
- package/src/config/types.ts +2 -0
- package/src/config/use-site-config.ts +25 -1
- package/src/i18n/index.ts +51 -0
- package/src/i18n/locales/eng.yml +128 -0
- package/src/i18n/locales/fra.yml +128 -0
- package/src/shims/glossarist-tags.ts +10 -0
- package/src/style.css +12 -0
- package/src/utils/markdown-lite.ts +15 -0
- package/src/views/AboutView.vue +3 -1
- package/src/views/ContributorsView.vue +5 -1
- package/src/views/DatasetView.vue +22 -20
- package/src/views/GraphView.vue +6 -4
- package/src/views/HomeView.vue +23 -19
- package/src/views/NewsView.vue +3 -1
- package/src/views/PageView.vue +26 -11
- package/src/views/SearchView.vue +4 -2
- package/src/views/StatsView.vue +11 -9
- package/vite.config.ts +34 -1
|
@@ -6,12 +6,14 @@ import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
|
6
6
|
import { FORMAT_LABELS } from '../config/types';
|
|
7
7
|
import { langName, langLabel, sortLanguages } from '../utils/lang';
|
|
8
8
|
import ConceptCard from '../components/ConceptCard.vue';
|
|
9
|
+
import { useI18n } from '../i18n';
|
|
9
10
|
|
|
10
11
|
const props = defineProps<{ registerId: string }>();
|
|
11
12
|
|
|
12
13
|
const store = useVocabularyStore();
|
|
13
14
|
const { getStyle } = useDsStyle();
|
|
14
15
|
const { loading, localError, ensureLoaded } = useDatasetLoader(() => props.registerId);
|
|
16
|
+
const { t } = useI18n();
|
|
15
17
|
|
|
16
18
|
const manifest = computed(() => store.manifests.get(props.registerId));
|
|
17
19
|
const adapter = computed(() => store.datasets.get(props.registerId));
|
|
@@ -180,7 +182,7 @@ function goToPage(p: number) {
|
|
|
180
182
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
181
183
|
<!-- Breadcrumb -->
|
|
182
184
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
183
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
185
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
184
186
|
<span class="text-ink-200">/</span>
|
|
185
187
|
<span class="text-ink-700">{{ manifest?.title || registerId }}</span>
|
|
186
188
|
</nav>
|
|
@@ -190,21 +192,21 @@ function goToPage(p: number) {
|
|
|
190
192
|
<h1 class="font-serif text-3xl text-ink-800 mb-2">{{ manifest.title }}</h1>
|
|
191
193
|
<p class="text-ink-400 leading-relaxed max-w-2xl">{{ manifest.description }}</p>
|
|
192
194
|
<div class="flex flex-wrap gap-2 mt-4">
|
|
193
|
-
<span class="badge" :style="{ backgroundColor: getStyle(registerId).light, color: getStyle(registerId).dark }">{{ manifest.conceptCount.toLocaleString() }} concepts</span>
|
|
194
|
-
<span class="badge badge-gray">{{ manifest.languages.length }} languages</span>
|
|
195
|
+
<span class="badge" :style="{ backgroundColor: getStyle(registerId).light, color: getStyle(registerId).dark }">{{ manifest.conceptCount.toLocaleString() }} {{ t('dataset.concepts') }}</span>
|
|
196
|
+
<span class="badge badge-gray">{{ manifest.languages.length }} {{ t('dataset.languages') }}</span>
|
|
195
197
|
<span class="badge badge-green">{{ manifest.owner }}</span>
|
|
196
198
|
<router-link :to="{ name: 'stats', params: { registerId } }" class="badge badge-blue hover:opacity-80 transition-opacity">
|
|
197
|
-
|
|
199
|
+
{{ t('nav.stats') }}
|
|
198
200
|
</router-link>
|
|
199
201
|
<router-link :to="{ name: 'about', params: { registerId } }" class="badge badge-purple hover:opacity-80 transition-opacity">
|
|
200
|
-
|
|
202
|
+
{{ t('nav.about') }}
|
|
201
203
|
</router-link>
|
|
202
204
|
</div>
|
|
203
205
|
</div>
|
|
204
206
|
|
|
205
207
|
<!-- Downloads -->
|
|
206
208
|
<div v-if="bulkDownloads.length" class="card p-4 mb-6">
|
|
207
|
-
<h3 class="text-xs font-semibold text-ink-400 uppercase tracking-wide mb-3">
|
|
209
|
+
<h3 class="text-xs font-semibold text-ink-400 uppercase tracking-wide mb-3">{{ t('dataset.download') }}</h3>
|
|
208
210
|
<div class="flex flex-wrap gap-2">
|
|
209
211
|
<a
|
|
210
212
|
v-for="dl in bulkDownloads"
|
|
@@ -237,11 +239,11 @@ function goToPage(p: number) {
|
|
|
237
239
|
<!-- Error state -->
|
|
238
240
|
<div v-else-if="localError" class="max-w-xl mx-auto text-center py-20">
|
|
239
241
|
<div class="card p-8 border-red-200 bg-red-50/50">
|
|
240
|
-
<p class="text-red-700 font-medium mb-1">
|
|
242
|
+
<p class="text-red-700 font-medium mb-1">{{ t('dataset.failedToLoad') }}</p>
|
|
241
243
|
<p class="text-sm text-red-600/80 mb-4">{{ localError }}</p>
|
|
242
244
|
<div class="flex gap-2 justify-center">
|
|
243
|
-
<button @click="ensureLoaded" class="btn-primary">
|
|
244
|
-
<router-link :to="{ name: 'home' }" class="btn-secondary">
|
|
245
|
+
<button @click="ensureLoaded" class="btn-primary">{{ t('dataset.retry') }}</button>
|
|
246
|
+
<router-link :to="{ name: 'home' }" class="btn-secondary">{{ t('dataset.backToHome') }}</router-link>
|
|
245
247
|
</div>
|
|
246
248
|
</div>
|
|
247
249
|
</div>
|
|
@@ -255,7 +257,7 @@ function goToPage(p: number) {
|
|
|
255
257
|
v-model="filter"
|
|
256
258
|
type="text"
|
|
257
259
|
aria-label="Filter concepts"
|
|
258
|
-
placeholder="Filter concepts...
|
|
260
|
+
placeholder="Filter concepts..."
|
|
259
261
|
class="pl-9 pr-3 py-2 text-sm bg-surface border border-ink-100 rounded-lg focus:ring-2 focus:ring-ink-200 focus:border-ink-400 outline-none placeholder:text-ink-300 transition-all w-full sm:w-64"
|
|
260
262
|
/>
|
|
261
263
|
<svg class="absolute left-3 top-2.5 w-4 h-4 text-ink-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -264,16 +266,16 @@ function goToPage(p: number) {
|
|
|
264
266
|
</div>
|
|
265
267
|
<span class="text-sm text-ink-400">
|
|
266
268
|
<template v-if="selectedLang">
|
|
267
|
-
{{ filtered.length.toLocaleString() }} of {{ totalConceptCount.toLocaleString() }} concepts in {{ langName(selectedLang) }}
|
|
269
|
+
{{ filtered.length.toLocaleString() }} {{ t('dataset.of') }} {{ totalConceptCount.toLocaleString() }} {{ t('dataset.concepts') }} {{ t('dataset.in') }} {{ langName(selectedLang) }}
|
|
268
270
|
</template>
|
|
269
271
|
<template v-else-if="filter.trim()">
|
|
270
|
-
{{ filtered.length.toLocaleString() }} of {{ totalConceptCount.toLocaleString() }} concepts
|
|
272
|
+
{{ filtered.length.toLocaleString() }} {{ t('dataset.of') }} {{ totalConceptCount.toLocaleString() }} {{ t('dataset.concepts') }}
|
|
271
273
|
</template>
|
|
272
274
|
<template v-else-if="totalPages > 1">
|
|
273
|
-
{{ ((page - 1) * perPage + 1).toLocaleString() }}–{{ Math.min(page * perPage, totalConceptCount).toLocaleString() }} of {{ totalConceptCount.toLocaleString() }} concepts
|
|
275
|
+
{{ ((page - 1) * perPage + 1).toLocaleString() }}–{{ Math.min(page * perPage, totalConceptCount).toLocaleString() }} {{ t('dataset.of') }} {{ totalConceptCount.toLocaleString() }} {{ t('dataset.concepts') }}
|
|
274
276
|
</template>
|
|
275
277
|
<template v-else>
|
|
276
|
-
{{ totalConceptCount.toLocaleString() }} concepts
|
|
278
|
+
{{ totalConceptCount.toLocaleString() }} {{ t('dataset.concepts') }}
|
|
277
279
|
</template>
|
|
278
280
|
</span>
|
|
279
281
|
</div>
|
|
@@ -289,7 +291,7 @@ function goToPage(p: number) {
|
|
|
289
291
|
]"
|
|
290
292
|
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
|
291
293
|
>
|
|
292
|
-
|
|
294
|
+
{{ t('dataset.all') }} {{ totalConceptCount.toLocaleString() }}
|
|
293
295
|
</button>
|
|
294
296
|
<button
|
|
295
297
|
v-for="lang in languageOptions"
|
|
@@ -333,11 +335,11 @@ function goToPage(p: number) {
|
|
|
333
335
|
<div v-else class="text-center py-20">
|
|
334
336
|
<div class="text-ink-200 text-5xl mb-4 font-serif">∅</div>
|
|
335
337
|
<template v-if="filter.trim()">
|
|
336
|
-
<p class="text-ink-500 font-medium mb-1">
|
|
337
|
-
<button @click="filter = ''" class="text-sm concept-link mt-1">
|
|
338
|
+
<p class="text-ink-500 font-medium mb-1">{{ t('dataset.noMatch') }}</p>
|
|
339
|
+
<button @click="filter = ''" class="text-sm concept-link mt-1">{{ t('dataset.clearFilter') }}</button>
|
|
338
340
|
</template>
|
|
339
341
|
<template v-else>
|
|
340
|
-
<p class="text-ink-500 font-medium mb-1">
|
|
342
|
+
<p class="text-ink-500 font-medium mb-1">{{ t('dataset.noConcepts') }}</p>
|
|
341
343
|
</template>
|
|
342
344
|
</div>
|
|
343
345
|
|
|
@@ -347,7 +349,7 @@ function goToPage(p: number) {
|
|
|
347
349
|
:disabled="page <= 1"
|
|
348
350
|
@click="goToPage(page - 1)"
|
|
349
351
|
class="btn-secondary disabled:opacity-30 text-xs"
|
|
350
|
-
>←
|
|
352
|
+
>← {{ t('dataset.prev') }}</button>
|
|
351
353
|
<template v-for="p in visiblePages" :key="p">
|
|
352
354
|
<span v-if="p < 0" class="text-ink-300 px-0.5">…</span>
|
|
353
355
|
<button
|
|
@@ -361,7 +363,7 @@ function goToPage(p: number) {
|
|
|
361
363
|
:disabled="page >= totalPages"
|
|
362
364
|
@click="goToPage(page + 1)"
|
|
363
365
|
class="btn-secondary disabled:opacity-30 text-xs"
|
|
364
|
-
>
|
|
366
|
+
>{{ t('dataset.next') }} →</button>
|
|
365
367
|
</div>
|
|
366
368
|
</template>
|
|
367
369
|
</div>
|
package/src/views/GraphView.vue
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { computed, onMounted, ref } from 'vue';
|
|
3
3
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
4
4
|
import GraphPanel from '../components/GraphPanel.vue';
|
|
5
|
+
import { useI18n } from '../i18n';
|
|
5
6
|
|
|
6
7
|
const store = useVocabularyStore();
|
|
8
|
+
const { t } = useI18n();
|
|
7
9
|
const graphLoading = ref(true);
|
|
8
10
|
|
|
9
11
|
// Depend on graphVersion so computed re-evaluates when edges are added
|
|
@@ -44,19 +46,19 @@ onMounted(async () => {
|
|
|
44
46
|
<div class="flex flex-col" style="height: calc(100vh - 56px)">
|
|
45
47
|
<div class="px-4 sm:px-6 py-3 border-b border-ink-100/60 bg-surface-raised flex items-center gap-3 flex-shrink-0">
|
|
46
48
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400">
|
|
47
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
49
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
48
50
|
<span class="text-ink-200">/</span>
|
|
49
|
-
<span class="text-ink-700">
|
|
51
|
+
<span class="text-ink-700">{{ t('nav.graph') }}</span>
|
|
50
52
|
</nav>
|
|
51
53
|
<span class="text-xs text-ink-300 ml-1">
|
|
52
|
-
{{ graphEdges.length.toLocaleString() }} edges
|
|
54
|
+
{{ graphEdges.length.toLocaleString() }} {{ t('graph.edges') }}
|
|
53
55
|
</span>
|
|
54
56
|
</div>
|
|
55
57
|
<div class="flex-1 min-h-0">
|
|
56
58
|
<div v-if="graphLoading" class="flex items-center justify-center h-full">
|
|
57
59
|
<div class="text-center">
|
|
58
60
|
<div class="w-8 h-8 border-2 border-ink-200 border-t-ink-600 rounded-full animate-spin mx-auto mb-3"></div>
|
|
59
|
-
<p class="text-sm text-ink-400">
|
|
61
|
+
<p class="text-sm text-ink-400">{{ t('graph.loading') }}</p>
|
|
60
62
|
</div>
|
|
61
63
|
</div>
|
|
62
64
|
<GraphPanel v-else :nodes="graphNodes" :edges="graphEdges" :registers="registers" />
|
package/src/views/HomeView.vue
CHANGED
|
@@ -4,11 +4,13 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
4
4
|
import { useRouter } from 'vue-router';
|
|
5
5
|
import { useDsStyle } from '../utils/dataset-style';
|
|
6
6
|
import { useSiteConfig } from '../config/use-site-config';
|
|
7
|
+
import { useI18n } from '../i18n';
|
|
7
8
|
|
|
8
9
|
const store = useVocabularyStore();
|
|
9
10
|
const router = useRouter();
|
|
10
11
|
const { getStyle } = useDsStyle();
|
|
11
|
-
const { config: siteConfig } = useSiteConfig();
|
|
12
|
+
const { config: siteConfig, localizedTitle, localizedSubtitle, localizedDescription } = useSiteConfig();
|
|
13
|
+
const { t } = useI18n();
|
|
12
14
|
const exploring = ref(false);
|
|
13
15
|
|
|
14
16
|
async function exploreRandom() {
|
|
@@ -61,29 +63,31 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
61
63
|
<div class="mb-10 sm:mb-14">
|
|
62
64
|
<div class="flex items-center gap-2 mb-4">
|
|
63
65
|
<span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300">{{ siteConfig?.branding?.ownerName || 'Glossarist' }}</span>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
+
<template v-if="siteConfig?.subtitle">
|
|
67
|
+
<span class="w-4 sm:w-6 h-px bg-ink-200"></span>
|
|
68
|
+
<span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300 hidden sm:inline">{{ localizedSubtitle }}</span>
|
|
69
|
+
</template>
|
|
66
70
|
</div>
|
|
67
71
|
<h1 class="font-serif text-[2rem] sm:text-[2.75rem] text-ink-800 leading-[1.1] mb-4 tracking-tight">
|
|
68
|
-
{{
|
|
69
|
-
<template v-if="
|
|
72
|
+
{{ localizedTitle }}
|
|
73
|
+
<template v-if="localizedSubtitle"><br class="hidden sm:block" /> {{ localizedSubtitle }}</template>
|
|
70
74
|
</h1>
|
|
71
75
|
<p class="text-base text-ink-400 max-w-lg leading-relaxed">
|
|
72
|
-
{{
|
|
76
|
+
{{ localizedDescription || 'Explore standardized terminology datasets from ISO and IEC technical committees. Browse concepts, definitions, and cross-references across multilingual vocabularies.' }}
|
|
73
77
|
</p>
|
|
74
78
|
<div class="flex flex-wrap gap-3 mt-7">
|
|
75
79
|
<button @click="goToSearch" class="btn-primary flex items-center gap-2">
|
|
76
80
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
|
77
|
-
|
|
81
|
+
{{ t('home.search') }}
|
|
78
82
|
</button>
|
|
79
83
|
<button @click="goToGraph" class="btn-secondary flex items-center gap-2">
|
|
80
84
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
|
81
|
-
|
|
85
|
+
{{ t('home.graphView') }}
|
|
82
86
|
</button>
|
|
83
87
|
<button @click="exploreRandom" :disabled="exploring" class="btn-secondary flex items-center gap-2">
|
|
84
88
|
<svg v-if="!exploring" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
85
89
|
<svg v-else class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
|
86
|
-
{{ exploring ? '
|
|
90
|
+
{{ exploring ? t('home.exploring') : t('home.surpriseMe') }}
|
|
87
91
|
</button>
|
|
88
92
|
</div>
|
|
89
93
|
</div>
|
|
@@ -92,15 +96,15 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
92
96
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-px bg-ink-100/60 rounded-xl overflow-hidden mb-10 sm:mb-14">
|
|
93
97
|
<div class="bg-surface-raised px-4 sm:px-6 py-5 animate-entrance" style="animation-delay: 80ms">
|
|
94
98
|
<div class="text-3xl font-serif text-ink-800 tabular-nums">{{ filteredDatasets.length }}</div>
|
|
95
|
-
<div class="text-sm text-ink-400 mt-1">
|
|
99
|
+
<div class="text-sm text-ink-400 mt-1">{{ t('home.datasets') }}</div>
|
|
96
100
|
</div>
|
|
97
101
|
<div class="bg-surface-raised px-6 py-5 animate-entrance" style="animation-delay: 140ms">
|
|
98
102
|
<div class="text-3xl font-serif text-ink-800 tabular-nums">{{ totalConcepts.toLocaleString() }}</div>
|
|
99
|
-
<div class="text-sm text-ink-400 mt-1">
|
|
103
|
+
<div class="text-sm text-ink-400 mt-1">{{ t('home.concepts') }}</div>
|
|
100
104
|
</div>
|
|
101
105
|
<div class="bg-surface-raised px-6 py-5 animate-entrance" style="animation-delay: 200ms">
|
|
102
106
|
<div class="text-3xl font-serif text-ink-800 tabular-nums">{{ totalLanguages }}</div>
|
|
103
|
-
<div class="text-sm text-ink-400 mt-1">
|
|
107
|
+
<div class="text-sm text-ink-400 mt-1">{{ t('home.languages') }}</div>
|
|
104
108
|
</div>
|
|
105
109
|
</div>
|
|
106
110
|
|
|
@@ -122,14 +126,14 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
122
126
|
</p>
|
|
123
127
|
<div class="flex items-center gap-3 pl-6 mb-4">
|
|
124
128
|
<span :style="{ color: getStyle(filteredDatasets[0].id).color }" class="text-sm font-semibold tabular-nums">{{ filteredDatasets[0].manifest.conceptCount.toLocaleString() }}</span>
|
|
125
|
-
<span class="text-xs text-ink-300">concepts</span>
|
|
129
|
+
<span class="text-xs text-ink-300">{{ t('home.concepts').toLowerCase() }}</span>
|
|
126
130
|
<span class="text-ink-200 text-xs">·</span>
|
|
127
131
|
<span class="text-sm text-ink-500 tabular-nums">{{ filteredDatasets[0].manifest.languages.length }}</span>
|
|
128
|
-
<span class="text-xs text-ink-300">languages</span>
|
|
132
|
+
<span class="text-xs text-ink-300">{{ t('home.languages').toLowerCase() }}</span>
|
|
129
133
|
</div>
|
|
130
134
|
<div class="pl-6">
|
|
131
135
|
<span class="btn-primary inline-flex items-center gap-2">
|
|
132
|
-
|
|
136
|
+
{{ t('home.browseConcepts') }}
|
|
133
137
|
<svg class="w-4 h-4 group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
134
138
|
</span>
|
|
135
139
|
</div>
|
|
@@ -137,8 +141,8 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
137
141
|
</template>
|
|
138
142
|
<template v-else-if="filteredDatasets.length > 1">
|
|
139
143
|
<div class="flex items-center justify-between mb-5">
|
|
140
|
-
<div class="section-label mb-0">
|
|
141
|
-
<span class="text-xs text-ink-300">
|
|
144
|
+
<div class="section-label mb-0">{{ t('home.availableDatasets') }}</div>
|
|
145
|
+
<span class="text-xs text-ink-300">{{ t('home.clickToBrowse') }}</span>
|
|
142
146
|
</div>
|
|
143
147
|
<div :class="[
|
|
144
148
|
filteredDatasets.length === 2 ? 'max-w-3xl' : '',
|
|
@@ -166,10 +170,10 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
166
170
|
|
|
167
171
|
<div class="flex items-center gap-3 pl-[22px] mb-3">
|
|
168
172
|
<span :style="{ color: getStyle(ds.id).color }" class="text-sm font-semibold tabular-nums">{{ ds.manifest.conceptCount.toLocaleString() }}</span>
|
|
169
|
-
<span class="text-xs text-ink-300">concepts</span>
|
|
173
|
+
<span class="text-xs text-ink-300">{{ t('home.concepts').toLowerCase() }}</span>
|
|
170
174
|
<span class="text-ink-200 text-xs">·</span>
|
|
171
175
|
<span class="text-sm text-ink-500 tabular-nums">{{ ds.manifest.languages.length }}</span>
|
|
172
|
-
<span class="text-xs text-ink-300">languages</span>
|
|
176
|
+
<span class="text-xs text-ink-300">{{ t('home.languages').toLowerCase() }}</span>
|
|
173
177
|
</div>
|
|
174
178
|
|
|
175
179
|
<div class="flex flex-wrap gap-1.5 pl-[22px] mb-3">
|
package/src/views/NewsView.vue
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { ref, onMounted } from 'vue';
|
|
3
3
|
import { useSiteConfig } from '../config/use-site-config';
|
|
4
4
|
import { renderAsciiDocLite } from '../utils/asciidoc-lite';
|
|
5
|
+
import { useI18n } from '../i18n';
|
|
5
6
|
|
|
6
7
|
interface NewsPost {
|
|
7
8
|
slug: string;
|
|
@@ -13,6 +14,7 @@ interface NewsPost {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const { config } = useSiteConfig();
|
|
17
|
+
const { t } = useI18n();
|
|
16
18
|
const posts = ref<NewsPost[]>([]);
|
|
17
19
|
const loading = ref(true);
|
|
18
20
|
const error = ref<string | null>(null);
|
|
@@ -80,7 +82,7 @@ function formatDate(dateStr: string) {
|
|
|
80
82
|
<template>
|
|
81
83
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
82
84
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
83
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
85
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
84
86
|
<span class="text-ink-200">/</span>
|
|
85
87
|
<span class="text-ink-700">News</span>
|
|
86
88
|
</nav>
|
package/src/views/PageView.vue
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted, computed } from 'vue';
|
|
2
|
+
import { ref, onMounted, computed, watch } from 'vue';
|
|
3
3
|
import { useRoute } from 'vue-router';
|
|
4
|
+
import { useI18n } from '../i18n';
|
|
4
5
|
|
|
5
6
|
interface PageData {
|
|
6
7
|
title: string;
|
|
@@ -8,6 +9,7 @@ interface PageData {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const route = useRoute();
|
|
12
|
+
const { locale, t } = useI18n();
|
|
11
13
|
const registerId = computed(() => route.params.registerId as string | undefined);
|
|
12
14
|
const pageName = computed(() => {
|
|
13
15
|
if (route.params.page) return route.params.page as string;
|
|
@@ -19,14 +21,24 @@ const data = ref<PageData | null>(null);
|
|
|
19
21
|
const loading = ref(true);
|
|
20
22
|
const notFound = ref(false);
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
async function fetchPage() {
|
|
25
|
+
loading.value = true;
|
|
26
|
+
notFound.value = false;
|
|
27
|
+
data.value = null;
|
|
28
|
+
|
|
23
29
|
const page = pageName.value;
|
|
24
30
|
const dsId = registerId.value;
|
|
25
|
-
|
|
26
31
|
const base = import.meta.env.BASE_URL;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
|
|
33
|
+
// Build candidate URLs: try localized first, then default
|
|
34
|
+
const langSuffix = locale.value !== 'eng' ? `.${locale.value}` : '';
|
|
35
|
+
const urls: string[] = [];
|
|
36
|
+
if (dsId) {
|
|
37
|
+
if (langSuffix) urls.push(`${base}pages/${dsId}-${page}${langSuffix}.json`);
|
|
38
|
+
urls.push(`${base}pages/${dsId}-${page}.json`);
|
|
39
|
+
}
|
|
40
|
+
if (langSuffix) urls.push(`${base}pages/${page}${langSuffix}.json`);
|
|
41
|
+
urls.push(`${base}pages/${page}.json`);
|
|
30
42
|
|
|
31
43
|
for (const url of urls) {
|
|
32
44
|
try {
|
|
@@ -40,13 +52,16 @@ onMounted(async () => {
|
|
|
40
52
|
}
|
|
41
53
|
notFound.value = true;
|
|
42
54
|
loading.value = false;
|
|
43
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMounted(fetchPage);
|
|
58
|
+
watch([pageName, locale], fetchPage);
|
|
44
59
|
</script>
|
|
45
60
|
|
|
46
61
|
<template>
|
|
47
62
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
48
63
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
49
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
64
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
50
65
|
<template v-if="registerId">
|
|
51
66
|
<span class="text-ink-200">/</span>
|
|
52
67
|
<router-link :to="{ name: 'dataset', params: { registerId } }" class="hover:text-ink-700 transition-colors">{{ registerId }}</router-link>
|
|
@@ -66,9 +81,9 @@ onMounted(async () => {
|
|
|
66
81
|
|
|
67
82
|
<template v-else-if="notFound">
|
|
68
83
|
<div class="card p-8 text-center">
|
|
69
|
-
<h1 class="font-serif text-2xl text-ink-800 mb-2">
|
|
70
|
-
<p class="text-ink-500 mb-4">
|
|
71
|
-
<router-link :to="{ name: 'home' }" class="btn-primary">
|
|
84
|
+
<h1 class="font-serif text-2xl text-ink-800 mb-2">{{ t('page.notFound') }}</h1>
|
|
85
|
+
<p class="text-ink-500 mb-4">{{ t('page.notFoundMsg', { name: pageName }) }}</p>
|
|
86
|
+
<router-link :to="{ name: 'home' }" class="btn-primary">{{ t('page.goHome') }}</router-link>
|
|
72
87
|
</div>
|
|
73
88
|
</template>
|
|
74
89
|
|
package/src/views/SearchView.vue
CHANGED
|
@@ -3,10 +3,12 @@ import { ref, onMounted, watch } from 'vue';
|
|
|
3
3
|
import { useRoute, useRouter } from 'vue-router';
|
|
4
4
|
import SearchBar from '../components/SearchBar.vue';
|
|
5
5
|
import { useUiStore } from '../stores/ui';
|
|
6
|
+
import { useI18n } from '../i18n';
|
|
6
7
|
|
|
7
8
|
const route = useRoute();
|
|
8
9
|
const router = useRouter();
|
|
9
10
|
const ui = useUiStore();
|
|
11
|
+
const { t } = useI18n();
|
|
10
12
|
|
|
11
13
|
onMounted(() => {
|
|
12
14
|
if (route.query.q && typeof route.query.q === 'string') {
|
|
@@ -24,9 +26,9 @@ watch(() => route.query.q, (q) => {
|
|
|
24
26
|
<template>
|
|
25
27
|
<div class="px-4 sm:px-6 lg:px-8 py-8">
|
|
26
28
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
27
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
29
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
28
30
|
<span class="text-ink-200">/</span>
|
|
29
|
-
<span class="text-ink-700">
|
|
31
|
+
<span class="text-ink-700">{{ t('nav.search') }}</span>
|
|
30
32
|
</nav>
|
|
31
33
|
<SearchBar />
|
|
32
34
|
</div>
|
package/src/views/StatsView.vue
CHANGED
|
@@ -4,12 +4,14 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
4
4
|
import { useDsStyle } from '../utils/dataset-style';
|
|
5
5
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
6
6
|
import { langName, langLabel } from '../utils/lang';
|
|
7
|
+
import { useI18n } from '../i18n';
|
|
7
8
|
|
|
8
9
|
const props = defineProps<{ registerId?: string }>();
|
|
9
10
|
|
|
10
11
|
const store = useVocabularyStore();
|
|
11
12
|
const { getColor } = useDsStyle();
|
|
12
13
|
const { loading, localError, ensureLoaded, resolvedId } = useDatasetLoader(() => props.registerId);
|
|
14
|
+
const { t } = useI18n();
|
|
13
15
|
|
|
14
16
|
const manifest = computed(() => store.manifests.get(resolvedId.value));
|
|
15
17
|
|
|
@@ -71,11 +73,11 @@ function coverageColor(ratio: number): string {
|
|
|
71
73
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
72
74
|
<!-- Breadcrumb -->
|
|
73
75
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
74
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
76
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
75
77
|
<span class="text-ink-200">/</span>
|
|
76
78
|
<router-link :to="{ name: 'dataset', params: { registerId: resolvedId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title || resolvedId }}</router-link>
|
|
77
79
|
<span class="text-ink-200">/</span>
|
|
78
|
-
<span class="text-ink-700">
|
|
80
|
+
<span class="text-ink-700">{{ t('stats.title') }}</span>
|
|
79
81
|
</nav>
|
|
80
82
|
|
|
81
83
|
<template v-if="loading">
|
|
@@ -89,15 +91,15 @@ function coverageColor(ratio: number): string {
|
|
|
89
91
|
</template>
|
|
90
92
|
<template v-else-if="localError">
|
|
91
93
|
<div class="card p-8 border-red-200 bg-red-50/50 text-center">
|
|
92
|
-
<p class="text-red-700 font-medium mb-1">
|
|
94
|
+
<p class="text-red-700 font-medium mb-1">{{ t('stats.failedToLoad') }}</p>
|
|
93
95
|
<p class="text-sm text-red-600/80 mb-4">{{ localError }}</p>
|
|
94
|
-
<button @click="ensureLoaded" class="btn-primary">
|
|
96
|
+
<button @click="ensureLoaded" class="btn-primary">{{ t('stats.retry') }}</button>
|
|
95
97
|
</div>
|
|
96
98
|
</template>
|
|
97
99
|
<template v-else-if="manifest">
|
|
98
|
-
<h1 class="font-serif text-3xl text-ink-800 mb-2">
|
|
100
|
+
<h1 class="font-serif text-3xl text-ink-800 mb-2">{{ t('stats.title') }}</h1>
|
|
99
101
|
<p class="text-ink-400 mb-8">
|
|
100
|
-
{{ stats.total.toLocaleString()
|
|
102
|
+
{{ t('stats.summary', { count: stats.total.toLocaleString(), langCount: String(manifest.languages.length) }) }}
|
|
101
103
|
</p>
|
|
102
104
|
|
|
103
105
|
<!-- Language stats table -->
|
|
@@ -105,9 +107,9 @@ function coverageColor(ratio: number): string {
|
|
|
105
107
|
<table class="w-full text-sm">
|
|
106
108
|
<thead>
|
|
107
109
|
<tr class="border-b border-ink-100/60 bg-ink-50">
|
|
108
|
-
<th class="text-left px-5 py-3 text-ink-600 font-medium text-xs uppercase tracking-wide">
|
|
109
|
-
<th class="text-right px-5 py-3 text-ink-600 font-medium text-xs uppercase tracking-wide">
|
|
110
|
-
<th class="text-right px-5 py-3 text-ink-600 font-medium text-xs uppercase tracking-wide">
|
|
110
|
+
<th class="text-left px-5 py-3 text-ink-600 font-medium text-xs uppercase tracking-wide">{{ t('stats.language') }}</th>
|
|
111
|
+
<th class="text-right px-5 py-3 text-ink-600 font-medium text-xs uppercase tracking-wide">{{ t('stats.terms') }}</th>
|
|
112
|
+
<th class="text-right px-5 py-3 text-ink-600 font-medium text-xs uppercase tracking-wide">{{ t('stats.definitions') }}</th>
|
|
111
113
|
<th class="px-5 py-3 text-ink-600 font-medium w-40"></th>
|
|
112
114
|
</tr>
|
|
113
115
|
</thead>
|
package/vite.config.ts
CHANGED
|
@@ -2,12 +2,24 @@ import { defineConfig } from 'vite'
|
|
|
2
2
|
import vue from '@vitejs/plugin-vue'
|
|
3
3
|
import { resolve, dirname } from 'path'
|
|
4
4
|
import { fileURLToPath } from 'url'
|
|
5
|
+
import yaml from 'js-yaml'
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
8
|
const cwd = process.cwd()
|
|
8
9
|
|
|
9
10
|
const isTest = process.env.VITEST !== undefined
|
|
10
11
|
|
|
12
|
+
function yamlPlugin() {
|
|
13
|
+
return {
|
|
14
|
+
name: 'yaml-transform',
|
|
15
|
+
transform(code: string, id: string) {
|
|
16
|
+
if (!id.endsWith('.yml') && !id.endsWith('.yaml')) return
|
|
17
|
+
const data = yaml.load(code)
|
|
18
|
+
return { code: `export default ${JSON.stringify(data)}`, map: null }
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
function faviconPlugin() {
|
|
12
24
|
return {
|
|
13
25
|
name: 'favicon-inject',
|
|
@@ -29,6 +41,27 @@ function faviconPlugin() {
|
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
function brandingPlugin() {
|
|
45
|
+
return {
|
|
46
|
+
name: 'branding-inject',
|
|
47
|
+
transformIndexHtml(html: string) {
|
|
48
|
+
let result = html
|
|
49
|
+
const title = process.env.SITE_TITLE
|
|
50
|
+
if (title) {
|
|
51
|
+
result = result.replace(/<title>[^<]*<\/title>/, `<title>${title}</title>`)
|
|
52
|
+
}
|
|
53
|
+
const fontsUrl = process.env.SITE_FONTS_URL
|
|
54
|
+
if (fontsUrl) {
|
|
55
|
+
result = result.replace(
|
|
56
|
+
/<link[^>]*href="https:\/\/fonts\.googleapis\.com\/css2\?[^"]*"[^>]*>/,
|
|
57
|
+
`<link href="${fontsUrl}" rel="stylesheet">`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
export default defineConfig({
|
|
33
66
|
base: process.env.BASE_PATH || '/',
|
|
34
67
|
root: __dirname,
|
|
@@ -37,7 +70,7 @@ export default defineConfig({
|
|
|
37
70
|
outDir: resolve(cwd, 'dist'),
|
|
38
71
|
emptyOutDir: true,
|
|
39
72
|
},
|
|
40
|
-
plugins: [faviconPlugin(), vue()],
|
|
73
|
+
plugins: [yamlPlugin(), faviconPlugin(), brandingPlugin(), vue()],
|
|
41
74
|
resolve: {
|
|
42
75
|
alias: {
|
|
43
76
|
'@': resolve(__dirname, 'src'),
|