@glossarist/concept-browser 0.6.0 → 0.7.1
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/package.json +1 -1
- package/scripts/generate-data.mjs +24 -3
- package/src/components/AppHeader.vue +9 -2
- package/src/components/AppSidebar.vue +12 -5
- package/src/components/ConceptCard.vue +5 -2
- package/src/components/ConceptDetail.vue +1 -1
- package/src/components/GraphPanel.vue +18 -16
- package/src/components/SearchBar.vue +12 -11
- package/src/config/types.ts +6 -0
- package/src/config/use-site-config.ts +26 -3
- package/src/i18n/index.ts +2 -0
- package/src/i18n/locales/eng.yml +63 -1
- package/src/i18n/locales/fra.yml +64 -2
- 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 +5 -5
- package/src/views/NewsView.vue +3 -1
- package/src/views/StatsView.vue +11 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -885,6 +885,14 @@ async function processLogo(logoConfig, filename) {
|
|
|
885
885
|
await processLogo(config.branding?.logo, `${config.id}-logo.svg`);
|
|
886
886
|
await processLogo(config.branding?.footerLogo, `${config.id}-footer-logo.svg`);
|
|
887
887
|
|
|
888
|
+
// Process light/dark logo variants
|
|
889
|
+
if (config.branding?.logo?.localLight) {
|
|
890
|
+
await processLogo({ localPath: config.branding.logo.localLight }, `${config.id}-logo-light.svg`);
|
|
891
|
+
}
|
|
892
|
+
if (config.branding?.logo?.localDark) {
|
|
893
|
+
await processLogo({ localPath: config.branding.logo.localDark }, `${config.id}-logo-dark.svg`);
|
|
894
|
+
}
|
|
895
|
+
|
|
888
896
|
// === Page processors ===
|
|
889
897
|
|
|
890
898
|
function processNewsPage(config, page) {
|
|
@@ -1107,19 +1115,32 @@ const basePathPrefix = process.env.BASE_PATH?.replace(/\/+$/, '') || '';
|
|
|
1107
1115
|
for (const key of ['logo', 'footerLogo']) {
|
|
1108
1116
|
const suffix = key === 'logo' ? 'logo.svg' : 'footer-logo.svg';
|
|
1109
1117
|
if (siteBranding[key]) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1118
|
+
const updated = { ...siteBranding[key], path: `${basePathPrefix}/logos/${config.id}-${suffix}` };
|
|
1119
|
+
if (siteBranding[key].localLight) updated.light = `${basePathPrefix}/logos/${config.id}-${suffix.replace('.svg', '-light.svg')}`;
|
|
1120
|
+
if (siteBranding[key].localDark) updated.dark = `${basePathPrefix}/logos/${config.id}-${suffix.replace('.svg', '-dark.svg')}`;
|
|
1121
|
+
delete updated.localPath;
|
|
1122
|
+
delete updated.remoteUrl;
|
|
1123
|
+
delete updated.localLight;
|
|
1124
|
+
delete updated.localDark;
|
|
1125
|
+
siteBranding[key] = updated;
|
|
1113
1126
|
}
|
|
1114
1127
|
}
|
|
1115
1128
|
|
|
1129
|
+
// Build dataset translations map
|
|
1130
|
+
const datasetTranslations = {};
|
|
1131
|
+
for (const d of config.datasets) {
|
|
1132
|
+
if (d.translations) datasetTranslations[d.id] = d.translations;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1116
1135
|
writeJson(path.join(PUBLIC, 'site-config.json'), {
|
|
1117
1136
|
id: config.id,
|
|
1118
1137
|
domain: config.domain,
|
|
1119
1138
|
title: config.title,
|
|
1120
1139
|
subtitle: config.subtitle,
|
|
1121
1140
|
description: config.description,
|
|
1141
|
+
translations: config.translations || undefined,
|
|
1122
1142
|
datasets: config.datasets.map(d => d.id),
|
|
1143
|
+
datasetTranslations: Object.keys(datasetTranslations).length ? datasetTranslations : undefined,
|
|
1123
1144
|
defaultDataset: config.datasets.length === 1 ? config.datasets[0].id : undefined,
|
|
1124
1145
|
uiLanguages: config.uiLanguages || undefined,
|
|
1125
1146
|
branding: siteBranding,
|
|
@@ -9,7 +9,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
|
|
9
9
|
const router = useRouter();
|
|
10
10
|
const ui = useUiStore();
|
|
11
11
|
const store = useVocabularyStore();
|
|
12
|
-
const { config: siteConfig } = useSiteConfig();
|
|
12
|
+
const { config: siteConfig, localizedTitle } = useSiteConfig();
|
|
13
13
|
const { locale, t, setLocale } = useI18n();
|
|
14
14
|
const searchInput = ref('');
|
|
15
15
|
const langOpen = ref(false);
|
|
@@ -64,6 +64,13 @@ onBeforeUnmount(() => document.removeEventListener('click', closeLangOnOutside))
|
|
|
64
64
|
<button @click="goHome" class="flex items-center gap-2 hover:opacity-80 transition flex-shrink-0 group">
|
|
65
65
|
<div v-if="siteConfig?.branding?.logo" class="h-8 flex items-center">
|
|
66
66
|
<img
|
|
67
|
+
v-if="siteConfig.branding.logo.light && siteConfig.branding.logo.dark"
|
|
68
|
+
:src="ui.isDark ? siteConfig.branding.logo.dark : siteConfig.branding.logo.light"
|
|
69
|
+
:alt="siteConfig.branding.logo.alt"
|
|
70
|
+
class="h-8 max-w-[48px] object-contain rounded"
|
|
71
|
+
/>
|
|
72
|
+
<img
|
|
73
|
+
v-else
|
|
67
74
|
:src="siteConfig.branding.logo.path"
|
|
68
75
|
:alt="siteConfig.branding.logo.alt"
|
|
69
76
|
class="h-8 max-w-[48px] object-contain rounded"
|
|
@@ -77,7 +84,7 @@ onBeforeUnmount(() => document.removeEventListener('click', closeLangOnOutside))
|
|
|
77
84
|
<line x1="9" y1="11" x2="15" y2="11"/>
|
|
78
85
|
</svg>
|
|
79
86
|
</div>
|
|
80
|
-
<span class="font-serif text-lg text-ink-800 leading-none hidden sm:inline">{{
|
|
87
|
+
<span class="font-serif text-lg text-ink-800 leading-none hidden sm:inline">{{ localizedTitle }}</span>
|
|
81
88
|
</button>
|
|
82
89
|
|
|
83
90
|
<!-- Search -->
|
|
@@ -14,7 +14,7 @@ const ui = useUiStore();
|
|
|
14
14
|
const router = useRouter();
|
|
15
15
|
const route = useRoute();
|
|
16
16
|
const { getColor } = useDsStyle();
|
|
17
|
-
const { globalPages, datasetPages, config: siteConfig } = useSiteConfig();
|
|
17
|
+
const { globalPages, datasetPages, config: siteConfig, localizedTitle, localizedDatasetField } = useSiteConfig();
|
|
18
18
|
const { t } = useI18n();
|
|
19
19
|
|
|
20
20
|
const currentDataset = computed(() => route.params.registerId as string ?? '');
|
|
@@ -127,6 +127,13 @@ function selectProperty(id: string) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const isSearching = computed(() => !!searchQuery.value.trim());
|
|
130
|
+
|
|
131
|
+
function navTitle(page: { route: string }): string {
|
|
132
|
+
const route = page.route || 'home';
|
|
133
|
+
const key = `nav.${route}`;
|
|
134
|
+
const translated = t(key);
|
|
135
|
+
return translated === key ? (page as any).title : translated;
|
|
136
|
+
}
|
|
130
137
|
</script>
|
|
131
138
|
|
|
132
139
|
<template>
|
|
@@ -151,7 +158,7 @@ const isSearching = computed(() => !!searchQuery.value.trim());
|
|
|
151
158
|
@click="closeMobile"
|
|
152
159
|
>
|
|
153
160
|
<NavIcon :name="page.icon" />
|
|
154
|
-
{{ page
|
|
161
|
+
{{ navTitle(page) }}
|
|
155
162
|
</router-link>
|
|
156
163
|
|
|
157
164
|
<!-- Ontology entity sections nested under Ontology nav item -->
|
|
@@ -424,7 +431,7 @@ const isSearching = computed(() => !!searchQuery.value.trim());
|
|
|
424
431
|
|
|
425
432
|
<!-- Dataset-level navigation (shown when viewing a dataset) -->
|
|
426
433
|
<div v-if="showDatasetNav" class="mb-6">
|
|
427
|
-
<div class="section-label">{{ currentManifest?.title || siteConfig?.title || 'Dataset' }}</div>
|
|
434
|
+
<div class="section-label">{{ localizedDatasetField(currentDataset, 'title', currentManifest?.title || siteConfig?.title || 'Dataset') }}</div>
|
|
428
435
|
<nav class="space-y-0.5">
|
|
429
436
|
<router-link
|
|
430
437
|
v-for="page in datasetPages"
|
|
@@ -435,7 +442,7 @@ const isSearching = computed(() => !!searchQuery.value.trim());
|
|
|
435
442
|
@click="closeMobile"
|
|
436
443
|
>
|
|
437
444
|
<NavIcon :name="page.icon" />
|
|
438
|
-
{{ page
|
|
445
|
+
{{ navTitle(page) }}
|
|
439
446
|
</router-link>
|
|
440
447
|
</nav>
|
|
441
448
|
</div>
|
|
@@ -455,7 +462,7 @@ const isSearching = computed(() => !!searchQuery.value.trim());
|
|
|
455
462
|
]"
|
|
456
463
|
:style="currentDataset === ds.id ? { borderLeftColor: getColor(ds.id), borderLeftWidth: '2px' } : {}"
|
|
457
464
|
>
|
|
458
|
-
<div class="font-medium truncate leading-snug">{{ ds.title }}</div>
|
|
465
|
+
<div class="font-medium truncate leading-snug">{{ localizedDatasetField(ds.id, 'title', ds.title) }}</div>
|
|
459
466
|
<div v-if="ds.loaded" class="text-xs mt-0.5" :class="currentDataset === ds.id ? 'text-ink-400' : 'text-ink-300'">
|
|
460
467
|
{{ ds.conceptCount.toLocaleString() }} {{ t('home.concepts').toLowerCase() }}
|
|
461
468
|
</div>
|
|
@@ -4,6 +4,7 @@ import { computed } from 'vue';
|
|
|
4
4
|
import { useRouter } from 'vue-router';
|
|
5
5
|
import { useDsStyle } from '../utils/dataset-style';
|
|
6
6
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
7
|
+
import { useI18n } from '../i18n';
|
|
7
8
|
|
|
8
9
|
const props = defineProps<{
|
|
9
10
|
entry: ConceptSummary;
|
|
@@ -14,6 +15,7 @@ const props = defineProps<{
|
|
|
14
15
|
const router = useRouter();
|
|
15
16
|
const { getColor } = useDsStyle();
|
|
16
17
|
const store = useVocabularyStore();
|
|
18
|
+
const { locale } = useI18n();
|
|
17
19
|
|
|
18
20
|
function viewConcept() {
|
|
19
21
|
router.push({
|
|
@@ -32,8 +34,9 @@ function statusColor(status: string): string {
|
|
|
32
34
|
const manifestLanguages = computed(() => store.manifests.get(props.registerId)?.languages ?? []);
|
|
33
35
|
|
|
34
36
|
const displayTitle = computed(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const lang = props.displayLang || locale.value;
|
|
38
|
+
if (props.entry.designations?.[lang]) {
|
|
39
|
+
return props.entry.designations[lang];
|
|
37
40
|
}
|
|
38
41
|
return props.entry.eng || props.entry.id;
|
|
39
42
|
});
|
|
@@ -366,7 +366,7 @@ const nonVerbalReps = computed(() => {
|
|
|
366
366
|
<!-- Breadcrumb + nav row -->
|
|
367
367
|
<div class="flex items-start gap-2 mb-3">
|
|
368
368
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 min-w-0 flex-1 flex-wrap">
|
|
369
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors whitespace-nowrap">
|
|
369
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors whitespace-nowrap">{{ t('nav.home') }}</router-link>
|
|
370
370
|
<span class="text-ink-200">/</span>
|
|
371
371
|
<router-link :to="{ name: 'dataset', params: { registerId: manifest.id }}" class="hover:text-ink-700 transition-colors truncate max-w-[180px]">
|
|
372
372
|
{{ manifest.title }}
|
|
@@ -4,6 +4,7 @@ import type { GraphNode, GraphEdge } from '../adapters/types';
|
|
|
4
4
|
import type { SimulationNodeDatum, SimulationLinkDatum } from 'd3';
|
|
5
5
|
import { useDsStyle } from '../utils/dataset-style';
|
|
6
6
|
import { useUiStore } from '../stores/ui';
|
|
7
|
+
import { useI18n } from '../i18n';
|
|
7
8
|
import {
|
|
8
9
|
forceSimulation,
|
|
9
10
|
forceLink,
|
|
@@ -53,6 +54,7 @@ watch(() => props.registers, (regs) => {
|
|
|
53
54
|
|
|
54
55
|
const { getColor } = useDsStyle();
|
|
55
56
|
const uiStore = useUiStore();
|
|
57
|
+
const { t } = useI18n();
|
|
56
58
|
|
|
57
59
|
const STUB_COLOR = '#b8b9cc'; // ink-200
|
|
58
60
|
const HIGHLIGHT_COLOR = '#1a1b2e'; // ink-800
|
|
@@ -466,11 +468,11 @@ function selectedNodeColor(): string {
|
|
|
466
468
|
<div class="bg-surface-raised/95 backdrop-blur rounded-xl border border-ink-100/60 overflow-hidden" style="box-shadow: 0 4px 12px rgba(26, 27, 46, 0.08);">
|
|
467
469
|
<button
|
|
468
470
|
@click="panelOpen = !panelOpen"
|
|
469
|
-
:aria-label="panelOpen ? '
|
|
471
|
+
:aria-label="panelOpen ? t('graph.collapseControls') : t('graph.expandControls')"
|
|
470
472
|
class="w-full px-4 py-2.5 flex items-center justify-between hover:bg-ink-50/50 transition-colors"
|
|
471
473
|
>
|
|
472
474
|
<span class="text-xs font-semibold text-ink-600 tracking-wide">
|
|
473
|
-
{{ nodeCount.toLocaleString() }} nodes · {{ edgeCount.toLocaleString() }} edges
|
|
475
|
+
{{ nodeCount.toLocaleString() }} {{ t('graph.nodes') }} · {{ edgeCount.toLocaleString() }} {{ t('graph.edges') }}
|
|
474
476
|
</span>
|
|
475
477
|
<svg class="w-3.5 h-3.5 text-ink-300 transition-transform" :class="panelOpen ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
476
478
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
@@ -481,9 +483,9 @@ function selectedNodeColor(): string {
|
|
|
481
483
|
<!-- Dataset toggles -->
|
|
482
484
|
<div class="mt-3 space-y-2">
|
|
483
485
|
<div class="flex items-center gap-2 mb-3">
|
|
484
|
-
<button @click="toggleAll(true)" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">
|
|
486
|
+
<button @click="toggleAll(true)" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">{{ t('graph.all') }}</button>
|
|
485
487
|
<span class="text-ink-200 text-xs">|</span>
|
|
486
|
-
<button @click="toggleAll(false)" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">
|
|
488
|
+
<button @click="toggleAll(false)" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">{{ t('graph.none') }}</button>
|
|
487
489
|
</div>
|
|
488
490
|
<div
|
|
489
491
|
v-for="reg in registers"
|
|
@@ -515,32 +517,32 @@ function selectedNodeColor(): string {
|
|
|
515
517
|
|
|
516
518
|
<!-- Actions -->
|
|
517
519
|
<div v-if="isCapped" class="text-[10px] text-amber-600 mt-2 leading-relaxed">
|
|
518
|
-
|
|
520
|
+
{{ t('graph.renderingWarning', { max: MAX_RENDER_NODES.toLocaleString(), total: nodeCount.toLocaleString() }) }}
|
|
519
521
|
</div>
|
|
520
522
|
|
|
521
523
|
<div v-if="nodeCount > 0" class="flex gap-4 mt-3 pt-3 border-t border-ink-100/40">
|
|
522
|
-
<button @click="resetZoom" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">
|
|
523
|
-
<button @click="rebuildGraph" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">
|
|
524
|
+
<button @click="resetZoom" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">{{ t('graph.resetZoom') }}</button>
|
|
525
|
+
<button @click="rebuildGraph" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">{{ t('graph.reLayout') }}</button>
|
|
524
526
|
</div>
|
|
525
527
|
|
|
526
528
|
<div v-if="nodeCount > 0" class="mt-3 pt-3 border-t border-ink-100/40">
|
|
527
|
-
<div class="text-[10px] font-semibold text-ink-400 uppercase tracking-wide mb-2">
|
|
529
|
+
<div class="text-[10px] font-semibold text-ink-400 uppercase tracking-wide mb-2">{{ t('graph.nodeLabels') }}</div>
|
|
528
530
|
<div class="flex gap-1">
|
|
529
531
|
<button
|
|
530
532
|
@click="labelMode = 'designation'; rebuildGraph()"
|
|
531
533
|
class="text-[10px] px-2 py-1 rounded font-medium transition-colors"
|
|
532
534
|
:class="labelMode === 'designation' ? 'bg-ink-800 text-white' : 'text-ink-500 hover:bg-ink-50'"
|
|
533
|
-
>
|
|
535
|
+
>{{ t('graph.designation') }}</button>
|
|
534
536
|
<button
|
|
535
537
|
@click="labelMode = 'identifier'; rebuildGraph()"
|
|
536
538
|
class="text-[10px] px-2 py-1 rounded font-medium transition-colors"
|
|
537
539
|
:class="labelMode === 'identifier' ? 'bg-ink-800 text-white' : 'text-ink-500 hover:bg-ink-50'"
|
|
538
|
-
>
|
|
540
|
+
>{{ t('graph.identifier') }}</button>
|
|
539
541
|
</div>
|
|
540
542
|
</div>
|
|
541
543
|
|
|
542
544
|
<div v-if="nodeCount === 0" class="text-xs text-ink-300 mt-3 leading-relaxed">
|
|
543
|
-
{{ props.edges.length > 0 ? '
|
|
545
|
+
{{ props.edges.length > 0 ? t('graph.enableDatasets') : t('graph.browseToPopulate') }}
|
|
544
546
|
</div>
|
|
545
547
|
</div>
|
|
546
548
|
</div>
|
|
@@ -549,7 +551,7 @@ function selectedNodeColor(): string {
|
|
|
549
551
|
<!-- Legend -->
|
|
550
552
|
<div v-if="nodeCount > 0" class="absolute top-4 right-4 z-10 bg-surface-raised/90 backdrop-blur rounded-lg px-3 py-2.5 border border-ink-100/60 text-xs" style="box-shadow: 0 2px 6px rgba(26, 27, 46, 0.04);">
|
|
551
553
|
<div v-if="registers.length > 1">
|
|
552
|
-
<div class="font-semibold text-ink-400 text-[10px] uppercase tracking-wide mb-2">
|
|
554
|
+
<div class="font-semibold text-ink-400 text-[10px] uppercase tracking-wide mb-2">{{ t('graph.datasets') }}</div>
|
|
553
555
|
<div v-for="reg in registers" :key="reg.id" class="flex items-center gap-2 mb-1.5 last:mb-0">
|
|
554
556
|
<span
|
|
555
557
|
class="w-2.5 h-2.5 rounded-full inline-block flex-shrink-0"
|
|
@@ -560,11 +562,11 @@ function selectedNodeColor(): string {
|
|
|
560
562
|
</div>
|
|
561
563
|
<div class="flex items-center gap-2 mt-2 pt-2 border-t border-ink-100/40">
|
|
562
564
|
<span class="w-2 h-2 rounded-full inline-block" :style="{ backgroundColor: STUB_COLOR }"></span>
|
|
563
|
-
<span class="text-ink-300">
|
|
565
|
+
<span class="text-ink-300">{{ t('graph.stubLabel') }}</span>
|
|
564
566
|
</div>
|
|
565
567
|
<div class="flex items-center gap-2 mt-2 pt-2 border-t border-ink-100/40">
|
|
566
568
|
<span class="w-4 h-2 rounded inline-block flex-shrink-0" style="background: #ede9fe; border: 1px solid #8b5cf6;"></span>
|
|
567
|
-
<span class="text-ink-300">
|
|
569
|
+
<span class="text-ink-300">{{ t('graph.domainLabel') }}</span>
|
|
568
570
|
</div>
|
|
569
571
|
</div>
|
|
570
572
|
|
|
@@ -590,7 +592,7 @@ function selectedNodeColor(): string {
|
|
|
590
592
|
></span>
|
|
591
593
|
<span class="text-[10px] text-ink-400 uppercase tracking-wide">
|
|
592
594
|
{{ registerTitle(selectedNode.register) }} ·
|
|
593
|
-
{{ selectedNode.loaded ? '
|
|
595
|
+
{{ selectedNode.loaded ? t('graph.loadedStatus') : t('graph.stubStatus') }}
|
|
594
596
|
</span>
|
|
595
597
|
</div>
|
|
596
598
|
</div>
|
|
@@ -603,7 +605,7 @@ function selectedNodeColor(): string {
|
|
|
603
605
|
:to="{ name: 'concept', params: { registerId: selectedNode.register, conceptId: selectedNode.conceptId } }"
|
|
604
606
|
class="btn-primary text-xs mt-4 inline-block"
|
|
605
607
|
>
|
|
606
|
-
|
|
608
|
+
{{ t('graph.viewConcept') }}
|
|
607
609
|
</router-link>
|
|
608
610
|
</div>
|
|
609
611
|
</div>
|
|
@@ -6,11 +6,13 @@ import { ref, watch, onMounted, computed, nextTick } from 'vue';
|
|
|
6
6
|
import type { SearchHit } from '../adapters/types';
|
|
7
7
|
import { langLabel, langName } from '../utils/lang';
|
|
8
8
|
import { useDsStyle } from '../utils/dataset-style';
|
|
9
|
+
import { useI18n } from '../i18n';
|
|
9
10
|
|
|
10
11
|
const router = useRouter();
|
|
11
12
|
const ui = useUiStore();
|
|
12
13
|
const store = useVocabularyStore();
|
|
13
14
|
const { getStyle } = useDsStyle();
|
|
15
|
+
const { t } = useI18n();
|
|
14
16
|
const query = ref('');
|
|
15
17
|
const results = ref<SearchHit[]>([]);
|
|
16
18
|
const searched = ref(false);
|
|
@@ -154,7 +156,6 @@ onMounted(() => {
|
|
|
154
156
|
@input="onInput"
|
|
155
157
|
@keydown="onKeydown"
|
|
156
158
|
type="text"
|
|
157
|
-
aria-label="Search terms across all datasets"
|
|
158
159
|
placeholder="Search terms across all datasets..."
|
|
159
160
|
class="w-full pl-9 pr-8 py-2.5 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"
|
|
160
161
|
autofocus
|
|
@@ -175,7 +176,7 @@ onMounted(() => {
|
|
|
175
176
|
<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="M6 18L18 6M6 6l12 12"/></svg>
|
|
176
177
|
</button>
|
|
177
178
|
</div>
|
|
178
|
-
<button type="submit" class="btn-primary" :disabled="loading">
|
|
179
|
+
<button type="submit" class="btn-primary" :disabled="loading">{{ t('search.button') }}</button>
|
|
179
180
|
</div>
|
|
180
181
|
</form>
|
|
181
182
|
|
|
@@ -184,23 +185,23 @@ onMounted(() => {
|
|
|
184
185
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
185
186
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
186
187
|
</svg>
|
|
187
|
-
<p class="text-sm text-ink-400">
|
|
188
|
+
<p class="text-sm text-ink-400">{{ t('search.searching') }}</p>
|
|
188
189
|
</div>
|
|
189
190
|
|
|
190
191
|
<div v-else-if="searchError" class="text-center py-16">
|
|
191
192
|
<div class="card p-8 border-red-200 bg-red-50/50 max-w-md mx-auto">
|
|
192
|
-
<p class="text-red-700 font-medium mb-1">
|
|
193
|
+
<p class="text-red-700 font-medium mb-1">{{ t('search.failed') }}</p>
|
|
193
194
|
<p class="text-sm text-red-600/80 mb-4">{{ searchError }}</p>
|
|
194
|
-
<button @click="doSearch" class="btn-primary">
|
|
195
|
+
<button @click="doSearch" class="btn-primary">{{ t('search.retry') }}</button>
|
|
195
196
|
</div>
|
|
196
197
|
</div>
|
|
197
198
|
|
|
198
199
|
<div v-else-if="searched">
|
|
199
|
-
<p class="text-sm text-ink-400 mb-4">{{ results.length
|
|
200
|
+
<p class="text-sm text-ink-400 mb-4">{{ results.length === 1 ? t('search.oneResultFor', { query: ui.searchQuery }) : t('search.manyResultsFor', { count: String(results.length), query: ui.searchQuery }) }}</p>
|
|
200
201
|
<div v-if="results.length === 0" class="text-center py-16">
|
|
201
202
|
<div class="text-ink-200 text-5xl mb-4 font-serif">∅</div>
|
|
202
|
-
<p class="text-ink-500 font-medium">
|
|
203
|
-
<p class="text-sm text-ink-300 mt-1">
|
|
203
|
+
<p class="text-ink-500 font-medium">{{ t('search.noResults') }}</p>
|
|
204
|
+
<p class="text-sm text-ink-300 mt-1">{{ t('search.tryDifferent') }}</p>
|
|
204
205
|
</div>
|
|
205
206
|
|
|
206
207
|
<!-- Grouped results -->
|
|
@@ -210,7 +211,7 @@ onMounted(() => {
|
|
|
210
211
|
<div class="flex items-center gap-2 mb-2">
|
|
211
212
|
<span class="w-2 h-2 rounded-full flex-shrink-0" :style="{ backgroundColor: group.style.color }"></span>
|
|
212
213
|
<span class="text-xs font-semibold text-ink-500 uppercase tracking-wide">{{ group.title }}</span>
|
|
213
|
-
<span class="text-xs text-ink-300">{{ group.hits.length }}
|
|
214
|
+
<span class="text-xs text-ink-300">{{ group.hits.length }} {{ group.hits.length === 1 ? t('search.result') : t('search.results') }}</span>
|
|
214
215
|
</div>
|
|
215
216
|
<!-- Hits -->
|
|
216
217
|
<div class="space-y-1.5">
|
|
@@ -227,7 +228,7 @@ onMounted(() => {
|
|
|
227
228
|
<span v-if="hit.snippet" class="block text-xs text-ink-300 mt-0.5 truncate">{{ hit.snippet }}</span>
|
|
228
229
|
</div>
|
|
229
230
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
230
|
-
<span v-if="hit.matchField === 'id'" class="badge badge-gray text-[10px]">
|
|
231
|
+
<span v-if="hit.matchField === 'id'" class="badge badge-gray text-[10px]">{{ t('search.idMatch') }}</span>
|
|
231
232
|
<span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(hit.language) }}</span>
|
|
232
233
|
</div>
|
|
233
234
|
</button>
|
|
@@ -236,7 +237,7 @@ onMounted(() => {
|
|
|
236
237
|
</div>
|
|
237
238
|
|
|
238
239
|
<div v-if="results.length > 100" class="text-center text-sm text-ink-300 mt-6 pt-4 border-t border-ink-100/60">
|
|
239
|
-
|
|
240
|
+
{{ t('search.showingFirst', { max: '100', total: String(results.length) }) }}
|
|
240
241
|
</div>
|
|
241
242
|
</div>
|
|
242
243
|
</div>
|
package/src/config/types.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface LogoConfig {
|
|
|
12
12
|
alt: string;
|
|
13
13
|
url?: string;
|
|
14
14
|
remoteUrl?: string;
|
|
15
|
+
light?: string;
|
|
16
|
+
dark?: string;
|
|
17
|
+
localLight?: string;
|
|
18
|
+
localDark?: string;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export interface SiteBranding {
|
|
@@ -91,6 +95,7 @@ export interface DatasetConfig {
|
|
|
91
95
|
tags?: string[];
|
|
92
96
|
languageOrder?: string[];
|
|
93
97
|
downloads?: string[];
|
|
98
|
+
translations?: Record<string, { title?: string; description?: string }>;
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
// === Contributors ===
|
|
@@ -141,6 +146,7 @@ export interface SiteConfig {
|
|
|
141
146
|
title: string;
|
|
142
147
|
subtitle?: string;
|
|
143
148
|
description?: string;
|
|
149
|
+
translations?: Record<string, { title?: string; subtitle?: string; description?: string }>;
|
|
144
150
|
datasets: DatasetConfig[];
|
|
145
151
|
routing: RoutingEntry[];
|
|
146
152
|
branding: SiteBranding;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ref, computed } from 'vue';
|
|
2
2
|
import type { PageConfig } from './types';
|
|
3
|
+
import { locale } from '../i18n';
|
|
3
4
|
|
|
4
5
|
export interface RuntimeSiteConfig {
|
|
5
6
|
id: string;
|
|
@@ -8,6 +9,8 @@ export interface RuntimeSiteConfig {
|
|
|
8
9
|
title: string;
|
|
9
10
|
subtitle?: string;
|
|
10
11
|
description?: string;
|
|
12
|
+
translations?: Record<string, { title?: string; subtitle?: string; description?: string }>;
|
|
13
|
+
datasetTranslations?: Record<string, Record<string, { title?: string; description?: string }>>;
|
|
11
14
|
datasets: string[];
|
|
12
15
|
defaultDataset?: string;
|
|
13
16
|
uiLanguages?: { code: string; label: string }[];
|
|
@@ -18,8 +21,8 @@ export interface RuntimeSiteConfig {
|
|
|
18
21
|
header?: { family: string; source: string; weights?: number[]; url?: string };
|
|
19
22
|
body?: { family: string; source: string; weights?: number[]; url?: string };
|
|
20
23
|
};
|
|
21
|
-
logo?: { path: string; alt: string; url?: string };
|
|
22
|
-
footerLogo?: { path: string; alt: string; url?: string };
|
|
24
|
+
logo?: { path: string; alt: string; url?: string; light?: string; dark?: string };
|
|
25
|
+
footerLogo?: { path: string; alt: string; url?: string; light?: string; dark?: string };
|
|
23
26
|
ownerName?: string;
|
|
24
27
|
ownerUrl?: string;
|
|
25
28
|
};
|
|
@@ -151,6 +154,26 @@ export function useSiteConfig() {
|
|
|
151
154
|
const config = computed(() => siteConfig.value);
|
|
152
155
|
const visibleDatasets = computed(() => siteConfig.value?.datasets ?? []);
|
|
153
156
|
|
|
157
|
+
const localizedTitle = computed(() => {
|
|
158
|
+
const tr = siteConfig.value?.translations?.[locale.value];
|
|
159
|
+
return tr?.title ?? siteConfig.value?.title ?? 'Glossarist';
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const localizedSubtitle = computed(() => {
|
|
163
|
+
const tr = siteConfig.value?.translations?.[locale.value];
|
|
164
|
+
return tr?.subtitle ?? siteConfig.value?.subtitle;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const localizedDescription = computed(() => {
|
|
168
|
+
const tr = siteConfig.value?.translations?.[locale.value];
|
|
169
|
+
return tr?.description ?? siteConfig.value?.description;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
function localizedDatasetField(datasetId: string, field: 'title' | 'description', fallback?: string): string {
|
|
173
|
+
const tr = siteConfig.value?.datasetTranslations?.[datasetId]?.[locale.value];
|
|
174
|
+
return tr?.[field] ?? fallback ?? '';
|
|
175
|
+
}
|
|
176
|
+
|
|
154
177
|
const globalPages = computed<PageConfig[]>(() =>
|
|
155
178
|
synthesizeGlobalPages(siteConfig.value?.features, siteConfig.value?.pages),
|
|
156
179
|
);
|
|
@@ -159,5 +182,5 @@ export function useSiteConfig() {
|
|
|
159
182
|
synthesizeDatasetPages(siteConfig.value?.features, siteConfig.value?.pages),
|
|
160
183
|
);
|
|
161
184
|
|
|
162
|
-
return { config, visibleDatasets, loadConfig, globalPages, datasetPages };
|
|
185
|
+
return { config, visibleDatasets, localizedTitle, localizedSubtitle, localizedDescription, localizedDatasetField, loadConfig, globalPages, datasetPages };
|
|
163
186
|
}
|
package/src/i18n/index.ts
CHANGED
|
@@ -20,6 +20,8 @@ const stored = typeof localStorage !== 'undefined'
|
|
|
20
20
|
: DEFAULT_LANG;
|
|
21
21
|
const locale = ref(stored);
|
|
22
22
|
|
|
23
|
+
export { locale };
|
|
24
|
+
|
|
23
25
|
export function useI18n() {
|
|
24
26
|
function t(key: string, params?: Record<string, string>): string {
|
|
25
27
|
let msg = messages[locale.value]?.[key] || messages[DEFAULT_LANG]?.[key] || key;
|
package/src/i18n/locales/eng.yml
CHANGED
|
@@ -7,7 +7,7 @@ nav.search: Search
|
|
|
7
7
|
nav.graph: Graph View
|
|
8
8
|
nav.ontology: Ontology
|
|
9
9
|
nav.concepts: Concepts
|
|
10
|
-
nav.
|
|
10
|
+
nav.stats: Statistics
|
|
11
11
|
nav.about: About
|
|
12
12
|
nav.navigation: Navigation
|
|
13
13
|
nav.datasets: Datasets
|
|
@@ -27,6 +27,19 @@ home.browseConcepts: Browse concepts
|
|
|
27
27
|
# Search
|
|
28
28
|
search.placeholder: Search...
|
|
29
29
|
search.conceptSearch: Search concepts
|
|
30
|
+
search.fullPlaceholder: Search terms across all datasets...
|
|
31
|
+
search.button: Search
|
|
32
|
+
search.searching: Searching across datasets...
|
|
33
|
+
search.failed: Search failed
|
|
34
|
+
search.retry: Retry
|
|
35
|
+
search.result: result
|
|
36
|
+
search.results: results
|
|
37
|
+
search.oneResultFor: '1 result for "{query}"'
|
|
38
|
+
search.manyResultsFor: '{count} results for "{query}"'
|
|
39
|
+
search.noResults: No concepts found matching your search
|
|
40
|
+
search.tryDifferent: Try a different term or check the spelling.
|
|
41
|
+
search.showingFirst: Showing first {max} of {total} results. Refine your search for more specific matches.
|
|
42
|
+
search.idMatch: ID match
|
|
30
43
|
|
|
31
44
|
# Page view
|
|
32
45
|
page.notFound: Page Not Found
|
|
@@ -64,3 +77,52 @@ header.datasets: datasets
|
|
|
64
77
|
|
|
65
78
|
# Language
|
|
66
79
|
lang.termOnlyIn: Term only in
|
|
80
|
+
|
|
81
|
+
# Dataset view
|
|
82
|
+
dataset.concepts: concepts
|
|
83
|
+
dataset.languages: languages
|
|
84
|
+
dataset.filterPlaceholder: "Filter concepts... (press /)"
|
|
85
|
+
dataset.of: of
|
|
86
|
+
dataset.in: in
|
|
87
|
+
dataset.all: All
|
|
88
|
+
dataset.prev: Prev
|
|
89
|
+
dataset.next: Next
|
|
90
|
+
dataset.noMatch: No concepts match your filter
|
|
91
|
+
dataset.clearFilter: Clear filter
|
|
92
|
+
dataset.noConcepts: This dataset has no concepts
|
|
93
|
+
dataset.failedToLoad: Failed to load dataset
|
|
94
|
+
dataset.retry: Retry
|
|
95
|
+
dataset.backToHome: Back to home
|
|
96
|
+
dataset.download: Download
|
|
97
|
+
|
|
98
|
+
# Statistics view
|
|
99
|
+
stats.title: Statistics
|
|
100
|
+
stats.summary: "{count} concepts across {langCount} languages."
|
|
101
|
+
stats.language: Language
|
|
102
|
+
stats.terms: Terms
|
|
103
|
+
stats.definitions: Definitions
|
|
104
|
+
stats.failedToLoad: Failed to load statistics
|
|
105
|
+
stats.retry: Retry
|
|
106
|
+
|
|
107
|
+
# Graph
|
|
108
|
+
graph.nodes: nodes
|
|
109
|
+
graph.edges: edges
|
|
110
|
+
graph.all: All
|
|
111
|
+
graph.none: None
|
|
112
|
+
graph.resetZoom: Reset zoom
|
|
113
|
+
graph.reLayout: Re-layout
|
|
114
|
+
graph.nodeLabels: Node labels
|
|
115
|
+
graph.designation: Designation
|
|
116
|
+
graph.identifier: Identifier
|
|
117
|
+
graph.viewConcept: View concept
|
|
118
|
+
graph.stubLabel: Stub (not loaded)
|
|
119
|
+
graph.domainLabel: Domain (standard)
|
|
120
|
+
graph.datasets: Datasets
|
|
121
|
+
graph.enableDatasets: Enable datasets to see their graph.
|
|
122
|
+
graph.browseToPopulate: Browse concepts with cross-references to populate the graph.
|
|
123
|
+
graph.renderingWarning: "Rendering first {max} of {total} nodes."
|
|
124
|
+
graph.loadedStatus: loaded
|
|
125
|
+
graph.stubStatus: stub
|
|
126
|
+
graph.collapseControls: Collapse controls
|
|
127
|
+
graph.expandControls: Expand controls
|
|
128
|
+
graph.loading: Loading graph data...
|
package/src/i18n/locales/fra.yml
CHANGED
|
@@ -7,7 +7,7 @@ nav.search: Recherche
|
|
|
7
7
|
nav.graph: Vue en graphe
|
|
8
8
|
nav.ontology: Ontologie
|
|
9
9
|
nav.concepts: Concepts
|
|
10
|
-
nav.
|
|
10
|
+
nav.stats: Statistiques
|
|
11
11
|
nav.about: À propos
|
|
12
12
|
nav.navigation: Navigation
|
|
13
13
|
nav.datasets: Jeux de données
|
|
@@ -27,11 +27,24 @@ home.browseConcepts: Parcourir les concepts
|
|
|
27
27
|
# Search
|
|
28
28
|
search.placeholder: Rechercher…
|
|
29
29
|
search.conceptSearch: Rechercher des concepts
|
|
30
|
+
search.fullPlaceholder: Rechercher des termes dans tous les jeux de données…
|
|
31
|
+
search.button: Rechercher
|
|
32
|
+
search.searching: Recherche dans les jeux de données...
|
|
33
|
+
search.failed: Échec de la recherche
|
|
34
|
+
search.retry: Réessayer
|
|
35
|
+
search.result: résultat
|
|
36
|
+
search.results: résultats
|
|
37
|
+
search.oneResultFor: '1 résultat pour « {query} »'
|
|
38
|
+
search.manyResultsFor: '{count} résultats pour « {query} »'
|
|
39
|
+
search.noResults: Aucun concept trouvé correspondant à votre recherche
|
|
40
|
+
search.tryDifferent: Essayez un autre terme ou vérifiez l'orthographe.
|
|
41
|
+
search.showingFirst: Affichage des {max} premiers résultats sur {total}. Affinez votre recherche.
|
|
42
|
+
search.idMatch: Correspondance d'identifiant
|
|
30
43
|
|
|
31
44
|
# Page view
|
|
32
45
|
page.notFound: Page non trouvée
|
|
33
46
|
page.notFoundMsg: 'La page « {name} » n''existe pas.'
|
|
34
|
-
page.goHome: Aller à l'accueil
|
|
47
|
+
page.goHome: Aller à l''accueil
|
|
35
48
|
|
|
36
49
|
# Concept detail
|
|
37
50
|
concept.definition: Définition
|
|
@@ -64,3 +77,52 @@ header.datasets: jeux de données
|
|
|
64
77
|
|
|
65
78
|
# Language
|
|
66
79
|
lang.termOnlyIn: Terme uniquement en
|
|
80
|
+
|
|
81
|
+
# Dataset view
|
|
82
|
+
dataset.concepts: concepts
|
|
83
|
+
dataset.languages: langues
|
|
84
|
+
dataset.filterPlaceholder: "Filtrer les concepts… (appuyez sur /)"
|
|
85
|
+
dataset.of: de
|
|
86
|
+
dataset.in: en
|
|
87
|
+
dataset.all: Tous
|
|
88
|
+
dataset.prev: Préc.
|
|
89
|
+
dataset.next: Suiv.
|
|
90
|
+
dataset.noMatch: Aucun concept ne correspond au filtre
|
|
91
|
+
dataset.clearFilter: Effacer le filtre
|
|
92
|
+
dataset.noConcepts: Ce jeu de données n'a aucun concept
|
|
93
|
+
dataset.failedToLoad: Échec du chargement du jeu de données
|
|
94
|
+
dataset.retry: Réessayer
|
|
95
|
+
dataset.backToHome: Retour à l'accueil
|
|
96
|
+
dataset.download: Télécharger
|
|
97
|
+
|
|
98
|
+
# Statistics view
|
|
99
|
+
stats.title: Statistiques
|
|
100
|
+
stats.summary: "{count} concepts dans {langCount} langues."
|
|
101
|
+
stats.language: Langue
|
|
102
|
+
stats.terms: Termes
|
|
103
|
+
stats.definitions: Définitions
|
|
104
|
+
stats.failedToLoad: Échec du chargement des statistiques
|
|
105
|
+
stats.retry: Réessayer
|
|
106
|
+
|
|
107
|
+
# Graph
|
|
108
|
+
graph.nodes: nœuds
|
|
109
|
+
graph.edges: arêtes
|
|
110
|
+
graph.all: Tous
|
|
111
|
+
graph.none: Aucun
|
|
112
|
+
graph.resetZoom: Réinitialiser le zoom
|
|
113
|
+
graph.reLayout: Réorganiser
|
|
114
|
+
graph.nodeLabels: Étiquettes des nœuds
|
|
115
|
+
graph.designation: Désignation
|
|
116
|
+
graph.identifier: Identifiant
|
|
117
|
+
graph.viewConcept: Voir le concept
|
|
118
|
+
graph.stubLabel: Non chargé
|
|
119
|
+
graph.domainLabel: Domaine (standard)
|
|
120
|
+
graph.datasets: Jeux de données
|
|
121
|
+
graph.enableDatasets: Activez les jeux de données pour voir leur graphe.
|
|
122
|
+
graph.browseToPopulate: Parcourez les concepts avec des renvois croisés pour remplir le graphe.
|
|
123
|
+
graph.renderingWarning: "Affichage des {max} premiers nœuds sur {total}."
|
|
124
|
+
graph.loadedStatus: chargé
|
|
125
|
+
graph.stubStatus: non chargé
|
|
126
|
+
graph.collapseControls: Réduire les contrôles
|
|
127
|
+
graph.expandControls: Développer les contrôles
|
|
128
|
+
graph.loading: Chargement des données du graphe...
|
package/src/views/AboutView.vue
CHANGED
|
@@ -4,10 +4,12 @@ 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();
|
|
12
|
+
const { t } = useI18n();
|
|
11
13
|
const { getColor } = useDsStyle();
|
|
12
14
|
const { loading, localError, ensureLoaded, resolvedId } = useDatasetLoader(() => props.registerId);
|
|
13
15
|
|
|
@@ -18,7 +20,7 @@ const manifest = computed(() => store.manifests.get(resolvedId.value));
|
|
|
18
20
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
19
21
|
<!-- Breadcrumb -->
|
|
20
22
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
21
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
23
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
22
24
|
<span class="text-ink-200">/</span>
|
|
23
25
|
<router-link :to="{ name: 'dataset', params: { registerId: resolvedId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title || resolvedId }}</router-link>
|
|
24
26
|
<span class="text-ink-200">/</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useSiteConfig } from '../config/use-site-config';
|
|
3
|
+
import { useI18n } from '../i18n';
|
|
3
4
|
|
|
4
5
|
interface Contributor {
|
|
5
6
|
name: string;
|
|
@@ -9,6 +10,9 @@ interface Contributor {
|
|
|
9
10
|
email?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
const { config: siteConfig } = useSiteConfig();
|
|
14
|
+
const { t } = useI18n();
|
|
15
|
+
|
|
12
16
|
const { config } = useSiteConfig();
|
|
13
17
|
const contributors = config.value?.contributors as Contributor[] | undefined;
|
|
14
18
|
</script>
|
|
@@ -16,7 +20,7 @@ const contributors = config.value?.contributors as Contributor[] | undefined;
|
|
|
16
20
|
<template>
|
|
17
21
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
18
22
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
19
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
23
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
20
24
|
<span class="text-ink-200">/</span>
|
|
21
25
|
<span class="text-ink-700">Contributors</span>
|
|
22
26
|
</nav>
|
|
@@ -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
|
@@ -9,7 +9,7 @@ import { useI18n } from '../i18n';
|
|
|
9
9
|
const store = useVocabularyStore();
|
|
10
10
|
const router = useRouter();
|
|
11
11
|
const { getStyle } = useDsStyle();
|
|
12
|
-
const { config: siteConfig } = useSiteConfig();
|
|
12
|
+
const { config: siteConfig, localizedTitle, localizedSubtitle, localizedDescription } = useSiteConfig();
|
|
13
13
|
const { t } = useI18n();
|
|
14
14
|
const exploring = ref(false);
|
|
15
15
|
|
|
@@ -65,15 +65,15 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
65
65
|
<span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300">{{ siteConfig?.branding?.ownerName || 'Glossarist' }}</span>
|
|
66
66
|
<template v-if="siteConfig?.subtitle">
|
|
67
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">{{
|
|
68
|
+
<span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300 hidden sm:inline">{{ localizedSubtitle }}</span>
|
|
69
69
|
</template>
|
|
70
70
|
</div>
|
|
71
71
|
<h1 class="font-serif text-[2rem] sm:text-[2.75rem] text-ink-800 leading-[1.1] mb-4 tracking-tight">
|
|
72
|
-
{{
|
|
73
|
-
<template v-if="
|
|
72
|
+
{{ localizedTitle }}
|
|
73
|
+
<template v-if="localizedSubtitle"><br class="hidden sm:block" /> {{ localizedSubtitle }}</template>
|
|
74
74
|
</h1>
|
|
75
75
|
<p class="text-base text-ink-400 max-w-lg leading-relaxed">
|
|
76
|
-
{{
|
|
76
|
+
{{ localizedDescription || 'Explore standardized terminology datasets from ISO and IEC technical committees. Browse concepts, definitions, and cross-references across multilingual vocabularies.' }}
|
|
77
77
|
</p>
|
|
78
78
|
<div class="flex flex-wrap gap-3 mt-7">
|
|
79
79
|
<button @click="goToSearch" class="btn-primary flex items-center gap-2">
|
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/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>
|