@glossarist/concept-browser 0.7.0 → 0.7.4

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 CHANGED
@@ -81,6 +81,9 @@ Environment:
81
81
  const { config } = loadSiteConfig(named.site ? [named.site] : []);
82
82
 
83
83
  if (cmd === 'build' || cmd === 'site') {
84
+ if (!process.env.BASE_PATH && config?.basePath) {
85
+ process.env.BASE_PATH = config.basePath;
86
+ }
84
87
  for (const step of ['fetch', 'generate', 'edges']) {
85
88
  console.log(`\n=== ${step.toUpperCase()} ===\n`);
86
89
  await commands[step]();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.0",
3
+ "version": "0.7.4",
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": {
@@ -667,6 +667,7 @@ function processDataset(dir, register, opts) {
667
667
 
668
668
  const summary = concepts.map(c => ({
669
669
  id: c.id,
670
+ designations: c.designations,
670
671
  eng: c.designations.eng || Object.values(c.designations)[0] || '',
671
672
  status: c.status,
672
673
  }));
@@ -770,6 +771,7 @@ function processDataset(dir, register, opts) {
770
771
  hasImages: opts.hasImages,
771
772
  };
772
773
  if (opts.languageOrder) manifest.languageOrder = opts.languageOrder;
774
+ if (opts.ref) manifest.ref = opts.ref;
773
775
  writeJson(path.join(DATA, register, 'manifest.json'), manifest);
774
776
 
775
777
  // Copy bibliography.yaml → bibliography.json
@@ -834,6 +836,7 @@ for (let i = 0; i < config.datasets.length; i++) {
834
836
  languages: dsLanguages,
835
837
  sourceRepo: ds.sourceRepo,
836
838
  languageOrder: ds.languageOrder,
839
+ ref: ds.ref,
837
840
  tags: ds.tags,
838
841
  color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
839
842
  datasetUri: ds.uri,
@@ -885,6 +888,14 @@ async function processLogo(logoConfig, filename) {
885
888
  await processLogo(config.branding?.logo, `${config.id}-logo.svg`);
886
889
  await processLogo(config.branding?.footerLogo, `${config.id}-footer-logo.svg`);
887
890
 
891
+ // Process light/dark logo variants
892
+ if (config.branding?.logo?.localLight) {
893
+ await processLogo({ localPath: config.branding.logo.localLight }, `${config.id}-logo-light.svg`);
894
+ }
895
+ if (config.branding?.logo?.localDark) {
896
+ await processLogo({ localPath: config.branding.logo.localDark }, `${config.id}-logo-dark.svg`);
897
+ }
898
+
888
899
  // === Page processors ===
889
900
 
890
901
  function processNewsPage(config, page) {
@@ -1107,9 +1118,14 @@ const basePathPrefix = process.env.BASE_PATH?.replace(/\/+$/, '') || '';
1107
1118
  for (const key of ['logo', 'footerLogo']) {
1108
1119
  const suffix = key === 'logo' ? 'logo.svg' : 'footer-logo.svg';
1109
1120
  if (siteBranding[key]) {
1110
- siteBranding[key] = { ...siteBranding[key], path: `${basePathPrefix}/logos/${config.id}-${suffix}` };
1111
- delete siteBranding[key].localPath;
1112
- delete siteBranding[key].remoteUrl;
1121
+ const updated = { ...siteBranding[key], path: `${basePathPrefix}/logos/${config.id}-${suffix}` };
1122
+ if (siteBranding[key].localLight) updated.light = `${basePathPrefix}/logos/${config.id}-${suffix.replace('.svg', '-light.svg')}`;
1123
+ if (siteBranding[key].localDark) updated.dark = `${basePathPrefix}/logos/${config.id}-${suffix.replace('.svg', '-dark.svg')}`;
1124
+ delete updated.localPath;
1125
+ delete updated.remoteUrl;
1126
+ delete updated.localLight;
1127
+ delete updated.localDark;
1128
+ siteBranding[key] = updated;
1113
1129
  }
1114
1130
  }
1115
1131
 
@@ -33,6 +33,6 @@ describe('AppFooter', () => {
33
33
  const wrapper = mountFooter();
34
34
  const link = wrapper.findAll('a').find(a => a.text().includes('Glossarist Concept Browser'));
35
35
  expect(link).toBeDefined();
36
- expect(link!.attributes('href')).toContain('github.com');
36
+ expect(link!.attributes('href')).toContain('glossarist.org');
37
37
  });
38
38
  });
@@ -151,9 +151,4 @@ describe('AppSidebar', () => {
151
151
  expect(wrapper.text()).toContain('Test test');
152
152
  });
153
153
 
154
- it('shows powered by link', async () => {
155
- seedStore();
156
- const wrapper = await mountSidebar();
157
- expect(wrapper.text()).toContain('Glossarist Concept Browser');
158
- });
159
154
  });
@@ -57,6 +57,7 @@ export interface Manifest {
57
57
  color?: string;
58
58
  shortname?: string;
59
59
  languageOrder?: string[];
60
+ ref?: string;
60
61
  languageStats?: Record<string, { terms: number; definitions: number }>;
61
62
  availableFormats?: string[];
62
63
  bulkFormats?: { file: string; format: string; size: number }[];
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><svg id="uuid-e2e4f56e-6766-4eaf-8ba1-80c056213369" xmlns="http://www.w3.org/2000/svg" width="183.1" height="172.48" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 183.1 172.48"><defs><linearGradient id="uuid-526bfbac-cffb-4a71-b92d-c02fefae5080" x1="51.31" y1="161.8" x2="102.32" y2="73.02" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3fb6b0"/><stop offset=".07" stop-color="#5cc5be"/><stop offset=".15" stop-color="#78d3cc"/><stop offset=".24" stop-color="#8fe0d8"/><stop offset=".34" stop-color="#a1e9e1"/><stop offset=".47" stop-color="#aef0e7"/><stop offset=".63" stop-color="#b5f3ea"/><stop offset="1" stop-color="#b8f5ec"/></linearGradient><linearGradient id="uuid-1b1e62d8-3f5c-498e-8428-628e5706abf6" x1="0" y1="59.58" x2="183.1" y2="59.58" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2d4164"/><stop offset=".47" stop-color="#2d4164" stop-opacity=".94"/><stop offset="1" stop-color="#2d4164" stop-opacity=".8"/></linearGradient><linearGradient id="uuid-b44d9113-c74c-48d0-8db3-7071ba63ee91" x1="71.68" y1="31.06" x2="111.41" y2="31.06" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2d4164"/><stop offset=".28" stop-color="#2c4365"/><stop offset="1" stop-color="#588ac1"/></linearGradient><linearGradient id="uuid-ef83f354-a6ba-455c-a262-42ad1af8aab0" x1="76.66" y1="69.37" x2="183.1" y2="69.37" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#34798a" stop-opacity=".7"/><stop offset="1" stop-color="#62c4dd"/></linearGradient><linearGradient id="uuid-9c4f3d11-41f0-40bc-8383-a054e148eefb" x1="81.4" y1="129.46" x2="127.22" y2="50.28" gradientUnits="userSpaceOnUse"><stop offset=".2" stop-color="#456399"/><stop offset="1" stop-color="#3a5c80" stop-opacity=".7"/></linearGradient><linearGradient id="uuid-75c138e5-3eb7-4084-88c8-ea7b28bec669" x1="88.72" y1="146.48" x2="54.34" y2="146.48" xlink:href="#uuid-b44d9113-c74c-48d0-8db3-7071ba63ee91"/><linearGradient id="uuid-648153de-b6a2-4cdb-bf68-e5b0950e981e" x1="86.16" y1="99.24" x2="47.06" y2="25.85" gradientUnits="userSpaceOnUse"><stop offset=".2" stop-color="#456399"/><stop offset="1" stop-color="#3a5c80" stop-opacity=".7"/></linearGradient><linearGradient id="uuid-e1874ccf-ad77-48dc-8e9c-b19bc34aaaf5" x1="89.58" y1="107.94" x2="63.88" y2="57.26" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#456399" stop-opacity=".5"/><stop offset="1" stop-color="#97a4b7" stop-opacity=".8"/></linearGradient></defs><path d="m87.41,59.48l-61.18,106.11c-1.63,2.85.6,6.85,3.98,6.88h37.88l63.56-112.75-44.23-.25Z" style="fill:url(#uuid-526bfbac-cffb-4a71-b92d-c02fefae5080); stroke-width:0px;"/><path d="m40.39,141.03c.29.28,1.01,2.7,1.61,4.91.67,2.49,2.77,4.38,5.33,4.68,21.2,2.5,21.4,20.71,21.4,20.71l22.5-39.92c-23.71,14.02-41.88,6.43-40.64-8.09-2.71,4.71-7.5,12.99-10.21,17.7Z" style="fill:#3a5c80; stroke-width:0px;"/><path d="m75.5,80.4s-14.96,25.41-18.41,31.59c8.71-8.42,17.69,2.79,24.11-8.66,8.13-14.5-5.7-22.93-5.7-22.93Z" style="fill:#60abbc; stroke-width:0px;"/><path d="m113.82,172.48h38.28c4.52-.01,6.66-3.57,4.43-7.48-6.55-11.51-59.96-105.41-59.96-105.41l-43.04-.37,60.29,113.27Z" style="fill:#b8f5ec; stroke-width:0px;"/><path d="m106.47,91.98c-9.24,2.3-34.69,12.42-21.32,20.21,12.57,7.31,28.11.2,32.05-11.73,1.79-4.66-2.29-10.67-10.74-8.47Z" style="fill:#3fb6b0; stroke-width:0px;"/><path d="m137.32,131.2l-13.09-23.07c.37,24.63-35.5,17.21-35.5,17.21l24.95,46.87c-1-25.77,10.54-35.8,23.65-41Z" style="fill:#7ee0d4; stroke-width:0px;"/><path d="m57.35,132.11s0-.01.01-.02c0,0,0,0,0,0" style="fill:#3a5c80; stroke-width:0px;"/><path d="m183.1,52.52c-.1-3.25-2.07-5.56-6-5.59-13.09-.07-163.15,0-163.15,0L0,72.26h183.1v-19.75Z" style="fill:url(#uuid-1b1e62d8-3f5c-498e-8428-628e5706abf6); stroke-width:0px;"/><path d="m71.68,47s19.67-31.93,27.9-31.9c10.36,0,15.93,31.9,8.24,31.83l-36.15.07Z" style="fill:url(#uuid-b44d9113-c74c-48d0-8db3-7071ba63ee91); stroke-width:0px;"/><path d="m183.1,59.91l-.13,10.94c.07,6.28-2.04,8.39-8.33,8.31h-97.99l10.97-19.59,95.47.33Z" style="fill:url(#uuid-ef83f354-a6ba-455c-a262-42ad1af8aab0); opacity:.78; stroke-width:0px;"/><polygon points="7.1 59.58 0 72.26 80.52 72.26 87.63 59.58 7.1 59.58" style="fill:#3a5c80; opacity:.8; stroke-width:0px;"/><path d="m117.54,47s-65.24,112.78-69.85,120.82c-1.1,1.92.38,4.63,2.65,4.65.71,0,17.74,0,17.74,0l70.73-125.47h-21.27Z" style="fill:url(#uuid-9c4f3d11-41f0-40bc-8383-a054e148eefb); opacity:.8; stroke-width:0px;"/><path d="m84.52,142.79c.55-.64.79-1.48,1.27-2.18,10.98-16.02-12.03-16.74-17.61-8.28l-12.79,23.17c-.14.24-.26.48-.38.72-3.39,7.29,6.71,13.38,11.92,7.26l17.59-20.69Z" style="fill:url(#uuid-75c138e5-3eb7-4084-88c8-ea7b28bec669); opacity:.8; stroke-width:0px;"/><path d="m124.71,126.57s0-.01-.01-.02c0,0,0,0,0,0,0,0,0,.02,0,.02Z" style="fill:#2e4365; stroke-width:0px;"/><polygon points="136.69 172.48 113.82 172.48 53.53 59.58 73.24 59.58 136.69 172.48" style="fill:url(#uuid-648153de-b6a2-4cdb-bf68-e5b0950e981e); opacity:.75; stroke-width:0px;"/><path d="m73.39,59.58l33.44,59.63c2.82,7.65-7.17,26.67-12.28,17.06L53.53,59.58h19.86Z" style="fill:url(#uuid-e1874ccf-ad77-48dc-8e9c-b19bc34aaaf5); stroke-width:0px;"/><path d="m115.87,135.34l18.22,32.5s-36.61-14.66-18.22-32.5Z" style="fill:#4977a4; stroke-width:0px;"/></svg>
@@ -2,13 +2,14 @@
2
2
  import { computed } from 'vue';
3
3
  import { useSiteConfig } from '../config/use-site-config';
4
4
  import { useI18n } from '../i18n';
5
+ import glossaristLogo from '../assets/glossarist-logo.svg';
5
6
 
6
7
  const { config } = useSiteConfig();
7
8
  const { t } = useI18n();
8
9
 
9
10
  const poweredBy = computed(() => {
10
11
  const pb = config.value?.features?.poweredBy as { message?: string; url?: string } | undefined;
11
- return { message: pb?.message || t('footer.builtWith'), url: pb?.url || 'https://github.com/glossarist/concept-browser' };
12
+ return { message: pb?.message || t('footer.builtWith'), url: pb?.url || 'https://glossarist.org' };
12
13
  });
13
14
 
14
15
  const socialLinks = computed(() => {
@@ -56,7 +57,8 @@ const ownerUrl = computed(() => config.value?.branding?.ownerUrl || '/');
56
57
  class="hover:text-ink-700 transition-colors"
57
58
  >{{ link.label }}</a>
58
59
  <span class="text-ink-200">|</span>
59
- <span class="text-xs">
60
+ <span class="text-xs inline-flex items-center gap-1.5">
61
+ <img :src="glossaristLogo" alt="" class="w-4 h-4 opacity-80" />
60
62
  <a :href="poweredBy.url" target="_blank" rel="noopener" class="concept-link">{{ poweredBy.message }}</a>
61
63
  </span>
62
64
  </div>
@@ -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"
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue';
2
+ import { computed, ref } from 'vue';
3
3
  import { useVocabularyStore } from '../stores/vocabulary';
4
4
  import { useUiStore } from '../stores/ui';
5
5
  import { useRoute, useRouter } from 'vue-router';
@@ -86,6 +86,22 @@ const datasetEntries = computed(() => {
86
86
  const currentManifest = computed(() => store.manifests.get(currentDataset.value));
87
87
  const showDatasetNav = computed(() => !!currentManifest.value || !!siteConfig.value?.defaultDataset);
88
88
 
89
+ const provenance = computed(() => {
90
+ const manifest = currentManifest.value;
91
+ return {
92
+ owner: manifest?.owner || (siteConfig.value as any)?.branding?.ownerName,
93
+ ownerUrl: (siteConfig.value as any)?.branding?.ownerUrl,
94
+ ref: manifest?.ref,
95
+ status: manifest?.status,
96
+ lastUpdated: manifest?.lastUpdated,
97
+ conceptCount: manifest?.conceptCount,
98
+ languageCount: manifest?.languages?.length,
99
+ sourceRepo: manifest?.sourceRepo,
100
+ };
101
+ });
102
+
103
+ const ontologyExpanded = ref(true);
104
+
89
105
  function closeMobile() { ui.sidebarOpen = false; }
90
106
 
91
107
  function goToDataset(id: string) {
@@ -107,7 +123,10 @@ function isActive(page: { route: string; datasetScoped?: boolean }): boolean {
107
123
  if (page.datasetScoped) return route.name === 'dataset' || route.name === 'concept';
108
124
  return route.name === 'home';
109
125
  }
110
- return route.name === page.route;
126
+ const target = pageRoute(page);
127
+ if (route.path === target) return true;
128
+ if (page.datasetScoped) return route.name === page.route;
129
+ return route.name === page.route || route.name === `${page.route}-global`;
111
130
  }
112
131
 
113
132
  function selectClass(id: string) {
@@ -162,7 +181,14 @@ function navTitle(page: { route: string }): string {
162
181
  </router-link>
163
182
 
164
183
  <!-- Ontology entity sections nested under Ontology nav item -->
165
- <div v-if="page.route === 'ontology' && isOntologyRoute" class="ml-3 mt-1 mb-2 space-y-0.5">
184
+ <div v-if="page.route === 'ontology' && isOntologyRoute" class="ml-3 mt-1 mb-2">
185
+ <button @click="ontologyExpanded = !ontologyExpanded"
186
+ class="w-full flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] uppercase tracking-wide text-ink-400 hover:text-ink-600 hover:bg-ink-50 transition-colors mb-1"
187
+ >
188
+ <span class="w-3 text-[10px]">{{ ontologyExpanded ? '▾' : '▸' }}</span>
189
+ <span class="flex-1 text-left">{{ t('nav.ontology') }}</span>
190
+ </button>
191
+ <div v-if="ontologyExpanded" class="space-y-0.5">
166
192
  <!-- Search input -->
167
193
  <div class="relative mb-1.5">
168
194
  <input
@@ -425,6 +451,7 @@ function navTitle(page: { route: string }): string {
425
451
  </div>
426
452
  </div>
427
453
  </template>
454
+ </div>
428
455
  </div>
429
456
  </template>
430
457
  </nav>
@@ -469,15 +496,42 @@ function navTitle(page: { route: string }): string {
469
496
  </button>
470
497
  </nav>
471
498
 
472
- <!-- Powered by -->
473
- <div class="mt-6 pt-4 border-t border-ink-100/60">
474
- <div class="text-[11px] text-ink-300">
475
- <a
476
- :href="(siteConfig?.features?.poweredBy as any)?.url || 'https://github.com/glossarist/concept-browser'"
477
- target="_blank"
478
- rel="noopener"
479
- class="concept-link"
480
- >{{ (siteConfig?.features?.poweredBy as any)?.message || 'Built with the Glossarist Concept Browser' }}</a>
499
+ <!-- Dataset provenance -->
500
+ <div v-if="provenance.owner" class="mt-6 pt-4 border-t border-ink-100/60">
501
+ <div class="text-[11px] text-ink-300 space-y-1.5">
502
+ <div class="font-medium text-ink-400">{{ t('sidebar.provenance') }}</div>
503
+
504
+ <div v-if="provenance.ref" class="text-xs font-semibold text-ink-700">
505
+ {{ provenance.ref }}
506
+ </div>
507
+
508
+ <div class="flex items-center gap-1">
509
+ <span class="text-ink-400">{{ t('sidebar.publishedBy') }}</span>
510
+ <a v-if="provenance.ownerUrl" :href="provenance.ownerUrl" target="_blank" rel="noopener" class="concept-link font-medium">{{ provenance.owner }}</a>
511
+ <span v-else class="text-ink-600 font-medium">{{ provenance.owner }}</span>
512
+ </div>
513
+
514
+ <div v-if="provenance.status" class="flex items-center gap-1.5">
515
+ <span class="inline-block w-1.5 h-1.5 rounded-full" :class="provenance.status === 'valid' ? 'bg-emerald-500' : 'bg-amber-400'"></span>
516
+ <span class="text-[10px] uppercase tracking-wide font-medium" :class="provenance.status === 'valid' ? 'text-emerald-600' : 'text-amber-600'">
517
+ {{ provenance.status }}
518
+ </span>
519
+ </div>
520
+
521
+ <div v-if="provenance.lastUpdated" class="text-ink-300">
522
+ {{ t('sidebar.updated') }} {{ provenance.lastUpdated }}
523
+ </div>
524
+
525
+ <div v-if="provenance.conceptCount" class="text-ink-400">
526
+ {{ provenance.conceptCount.toLocaleString() }} {{ t('sidebar.concepts').toLowerCase() }}
527
+ <template v-if="provenance.languageCount">
528
+ · {{ provenance.languageCount }} {{ t('sidebar.languages').toLowerCase() }}
529
+ </template>
530
+ </div>
531
+
532
+ <div v-if="provenance.sourceRepo">
533
+ <a :href="provenance.sourceRepo" target="_blank" rel="noopener" class="concept-link">{{ t('sidebar.viewSource') }}</a>
534
+ </div>
481
535
  </div>
482
536
  </div>
483
537
  </div>
@@ -15,7 +15,7 @@ const props = defineProps<{
15
15
  const router = useRouter();
16
16
  const { getColor } = useDsStyle();
17
17
  const store = useVocabularyStore();
18
- const { locale } = useI18n();
18
+ const { locale, t } = useI18n();
19
19
 
20
20
  function viewConcept() {
21
21
  router.push({
@@ -69,7 +69,7 @@ const langCount = computed(() => {
69
69
  </div>
70
70
  <!-- Language coverage -->
71
71
  <div class="flex items-center gap-1.5 mt-2.5">
72
- <span class="text-[11px] text-ink-300">{{ langCount }} lang</span>
72
+ <span class="text-[11px] text-ink-300">{{ langCount }} {{ t('concept.lang') }}</span>
73
73
  <div class="flex gap-0.5">
74
74
  <span
75
75
  v-for="lang in manifestLanguages"
@@ -23,7 +23,7 @@ import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
23
23
  import CitationDisplay from './CitationDisplay.vue';
24
24
  import { useI18n } from '../i18n';
25
25
 
26
- const { t } = useI18n();
26
+ const { t, locale } = useI18n();
27
27
 
28
28
  const props = defineProps<{
29
29
  concept: Concept;
@@ -36,7 +36,7 @@ const props = defineProps<{
36
36
  const router = useRouter();
37
37
  const store = useVocabularyStore();
38
38
  const { getColor } = useDsStyle();
39
- const { config: siteConfig } = useSiteConfig();
39
+ const { config: siteConfig, localizedDatasetField } = useSiteConfig();
40
40
  const factory = getFactory();
41
41
 
42
42
  const activeTab = ref<'rdf' | 'definition' | 'history'>('definition');
@@ -62,7 +62,15 @@ function copyUri() {
62
62
  }
63
63
 
64
64
  const languages = computed(() => {
65
- return sortLanguages(props.concept.languages, props.manifest.languageOrder);
65
+ const sorted = sortLanguages(props.concept.languages, props.manifest.languageOrder);
66
+ // Put current UI locale first
67
+ const current = locale.value;
68
+ const idx = sorted.indexOf(current);
69
+ if (idx > 0) {
70
+ sorted.splice(idx, 1);
71
+ sorted.unshift(current);
72
+ }
73
+ return sorted;
66
74
  });
67
75
 
68
76
  // Collapsible language sections — expand all with content, collapse those without
@@ -369,11 +377,11 @@ const nonVerbalReps = computed(() => {
369
377
  <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors whitespace-nowrap">{{ t('nav.home') }}</router-link>
370
378
  <span class="text-ink-200">/</span>
371
379
  <router-link :to="{ name: 'dataset', params: { registerId: manifest.id }}" class="hover:text-ink-700 transition-colors truncate max-w-[180px]">
372
- {{ manifest.title }}
380
+ {{ localizedDatasetField(manifest.id, 'title', manifest.title) }}
373
381
  </router-link>
374
382
  <span class="text-ink-200">/</span>
375
383
  <span class="text-ink-600 font-mono text-xs">{{ conceptId }}</span>
376
- <span v-if="conceptPosition" class="text-[10px] text-ink-300 tabular-nums ml-1 whitespace-nowrap">({{ conceptPosition.index }} of {{ conceptPosition.total.toLocaleString() }})</span>
384
+ <span v-if="conceptPosition" class="text-[10px] text-ink-300 tabular-nums ml-1 whitespace-nowrap">({{ conceptPosition.index }} {{ t('concept.of') }} {{ conceptPosition.total.toLocaleString() }})</span>
377
385
  </nav>
378
386
  <!-- Prev/Next navigation -->
379
387
  <div v-if="adjacent.prev || adjacent.next" class="flex items-center gap-1 flex-shrink-0">
@@ -381,7 +389,7 @@ const nonVerbalReps = computed(() => {
381
389
  v-if="adjacent.prev"
382
390
  @click="goAdjacent(adjacent.prev)"
383
391
  class="p-2.5 rounded-lg text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors min-w-[44px] min-h-[44px] flex items-center justify-center"
384
- title="Previous concept (←)"
392
+ :title="t('concept.previous') + ' (←)'"
385
393
  >
386
394
  <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="M15 19l-7-7 7-7"/></svg>
387
395
  </button>
@@ -389,7 +397,7 @@ const nonVerbalReps = computed(() => {
389
397
  v-if="adjacent.next"
390
398
  @click="goAdjacent(adjacent.next)"
391
399
  class="p-2.5 rounded-lg text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors min-w-[44px] min-h-[44px] flex items-center justify-center"
392
- title="Next concept (→)"
400
+ :title="t('concept.next') + ' (→)'"
393
401
  >
394
402
  <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="M9 5l7 7-7 7"/></svg>
395
403
  </button>
@@ -410,7 +418,7 @@ const nonVerbalReps = computed(() => {
410
418
  {{ entryStatusLabel(engConcept.entryStatus) }}
411
419
  </span>
412
420
  <span class="badge badge-gray" v-if="manifest.owner">{{ manifest.owner }}</span>
413
- <span class="badge badge-purple">{{ languages.length }} languages</span>
421
+ <span class="badge badge-purple">{{ languages.length }} {{ t('concept.languages') }}</span>
414
422
  </div>
415
423
  </div>
416
424
 
@@ -426,7 +434,7 @@ const nonVerbalReps = computed(() => {
426
434
  ? 'bg-blue-600 text-white shadow-sm md:bg-transparent md:text-blue-600 md:border-blue-500 md:shadow-none'
427
435
  : 'text-ink-500 hover:text-ink-700 md:text-ink-400 md:border-transparent md:hover:text-ink-600'"
428
436
  >
429
- Definition
437
+ {{ t('concept.definition') }}
430
438
  </button>
431
439
  <button
432
440
  role="tab"
@@ -437,7 +445,7 @@ const nonVerbalReps = computed(() => {
437
445
  ? 'bg-blue-600 text-white shadow-sm md:bg-transparent md:text-blue-600 md:border-blue-500 md:shadow-none'
438
446
  : 'text-ink-500 hover:text-ink-700 md:text-ink-400 md:border-transparent md:hover:text-ink-600'"
439
447
  >
440
- RDF
448
+ {{ t('concept.rdf') }}
441
449
  </button>
442
450
  <button
443
451
  role="tab"
@@ -448,7 +456,7 @@ const nonVerbalReps = computed(() => {
448
456
  ? 'bg-blue-600 text-white shadow-sm md:bg-transparent md:text-blue-600 md:border-blue-500 md:shadow-none'
449
457
  : 'text-ink-500 hover:text-ink-700 md:text-ink-400 md:border-transparent md:hover:text-ink-600'"
450
458
  >
451
- History
459
+ {{ t('concept.history') }}
452
460
  </button>
453
461
  </div>
454
462
 
@@ -456,9 +464,9 @@ const nonVerbalReps = computed(() => {
456
464
  <div v-if="activeTab === 'definition'" role="tabpanel">
457
465
  <!-- Expand/Collapse all toggle -->
458
466
  <div v-if="allLangContent.length > 1" class="flex items-center justify-between mb-3">
459
- <span class="text-xs text-ink-400">{{ languages.length }} languages</span>
467
+ <span class="text-xs text-ink-400">{{ languages.length }} {{ t('concept.languages') }}</span>
460
468
  <button @click="toggleAll" class="text-xs text-ink-400 hover:text-ink-600 transition-colors px-3 py-2">
461
- {{ allCollapsed ? 'Expand all' : 'Collapse all' }}
469
+ {{ allCollapsed ? t('concept.expandAll') : t('concept.collapseAll') }}
462
470
  </button>
463
471
  </div>
464
472
  <div class="lg:flex lg:gap-8">
@@ -487,7 +495,7 @@ const nonVerbalReps = computed(() => {
487
495
  <div v-else class="w-full flex items-center gap-2.5 px-3 sm:px-4 py-3">
488
496
  <span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(lc.lang) }}</span>
489
497
  <span class="font-medium text-ink-800 text-sm" v-html="renderMath(getTermForLang(lc.lang))"></span>
490
- <span class="text-xs text-ink-200 ml-2 italic">designation only</span>
498
+ <span class="text-xs text-ink-200 ml-2 italic">{{ t('concept.designationOnly') }}</span>
491
499
  <span v-if="lc.entryStatus" class="badge text-[10px] ml-auto" :class="entryStatusColor(lc.entryStatus)" :title="entryStatusDefinition(lc.entryStatus) ?? ''">{{ entryStatusLabel(lc.entryStatus) }}</span>
492
500
  </div>
493
501
  <!-- Collapsed preview -->
@@ -599,30 +607,30 @@ const nonVerbalReps = computed(() => {
599
607
 
600
608
  <!-- Ontological metadata -->
601
609
  <div v-if="lc.classification || lc.reviewType || lc.release || lc.lineageSourceSimilarity != null || lc.lcScript || lc.lcSystem" class="border-t border-ink-100/60 pt-2 mt-2">
602
- <div class="text-[10px] uppercase tracking-wide text-ink-300 font-medium mb-1.5">Ontological metadata</div>
610
+ <div class="text-[10px] uppercase tracking-wide text-ink-300 font-medium mb-1.5">{{ t('concept.ontologicalMetadata') }}</div>
603
611
  <dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
604
612
  <template v-if="lc.classification">
605
- <dt class="text-ink-300">Classification</dt>
613
+ <dt class="text-ink-300">{{ t('concept.classificationLabel') }}</dt>
606
614
  <dd class="text-ink-700">{{ lc.classification }}</dd>
607
615
  </template>
608
616
  <template v-if="lc.reviewType">
609
- <dt class="text-ink-300">Review type</dt>
617
+ <dt class="text-ink-300">{{ t('concept.reviewType') }}</dt>
610
618
  <dd class="text-ink-700">{{ lc.reviewType }}</dd>
611
619
  </template>
612
620
  <template v-if="lc.release">
613
- <dt class="text-ink-300">Release</dt>
621
+ <dt class="text-ink-300">{{ t('concept.release') }}</dt>
614
622
  <dd class="text-ink-700">{{ lc.release }}</dd>
615
623
  </template>
616
624
  <template v-if="lc.lineageSourceSimilarity != null">
617
- <dt class="text-ink-300">Lineage similarity</dt>
625
+ <dt class="text-ink-300">{{ t('concept.lineageSimilarity') }}</dt>
618
626
  <dd class="text-ink-700">{{ lc.lineageSourceSimilarity }}%</dd>
619
627
  </template>
620
628
  <template v-if="lc.lcScript">
621
- <dt class="text-ink-300">Script</dt>
629
+ <dt class="text-ink-300">{{ t('concept.script') }}</dt>
622
630
  <dd class="text-ink-700 font-mono">{{ lc.lcScript }}</dd>
623
631
  </template>
624
632
  <template v-if="lc.lcSystem">
625
- <dt class="text-ink-300">Conversion system</dt>
633
+ <dt class="text-ink-300">{{ t('concept.conversionSystem') }}</dt>
626
634
  <dd class="text-ink-700 font-mono">{{ lc.lcSystem }}</dd>
627
635
  </template>
628
636
  </dl>
@@ -727,7 +735,7 @@ const nonVerbalReps = computed(() => {
727
735
 
728
736
  <!-- Language quick-jump -->
729
737
  <div class="card p-5">
730
- <div class="section-label">Languages ({{ languages.length }})</div>
738
+ <div class="section-label">{{ t('concept.languagesSidebar', { count: languages.length }) }}</div>
731
739
  <div class="space-y-1 mt-3 max-h-80 overflow-y-auto">
732
740
  <button
733
741
  v-for="lang in languages"
@@ -740,7 +748,7 @@ const nonVerbalReps = computed(() => {
740
748
  <span
741
749
  class="w-1.5 h-1.5 rounded-full flex-shrink-0"
742
750
  :class="hasDefinition(lang) ? 'bg-emerald-400' : 'bg-ink-200'"
743
- :title="hasDefinition(lang) ? 'Has definition' : 'Designation only'"
751
+ :title="hasDefinition(lang) ? t('concept.hasDefinition') : t('concept.designationOnlyTitle')"
744
752
  ></span>
745
753
  <span class="text-sm font-medium text-ink-800 group-hover:text-ink-900 transition-colors" v-html="renderMath(getTermForLang(lang))"></span>
746
754
  </div>
@@ -763,24 +771,24 @@ const nonVerbalReps = computed(() => {
763
771
  <div class="section-label">{{ t('concept.metadata') }}</div>
764
772
  <dl class="space-y-2 text-xs mt-3">
765
773
  <div v-if="managedStatus">
766
- <dt class="text-ink-300">Status</dt>
774
+ <dt class="text-ink-300">{{ t('concept.status') }}</dt>
767
775
  <dd class="mt-0.5">
768
776
  <span class="badge text-[10px]" :class="conceptStatusColor(managedStatus)" :title="conceptStatusDefinition(managedStatus) ?? ''">{{ conceptStatusLabel(managedStatus) }}</span>
769
777
  </dd>
770
778
  </div>
771
779
  <div v-if="engConcept?.reviewDate">
772
- <dt class="text-ink-300">Review Date</dt>
780
+ <dt class="text-ink-300">{{ t('concept.reviewDate') }}</dt>
773
781
  <dd class="text-ink-700 mt-0.5">{{ engConcept.reviewDate.slice(0, 10) }}</dd>
774
782
  </div>
775
783
  <div v-if="engConcept?.reviewDecisionEvent">
776
- <dt class="text-ink-300">Decision</dt>
784
+ <dt class="text-ink-300">{{ t('concept.decision') }}</dt>
777
785
  <dd class="text-ink-700 mt-0.5">{{ engConcept.reviewDecisionEvent }}</dd>
778
786
  </div>
779
787
  <div>
780
- <dt class="text-ink-300">URI</dt>
788
+ <dt class="text-ink-300">{{ t('concept.uri') }}</dt>
781
789
  <dd class="font-mono text-ink-600 break-all mt-0.5 text-[11px] flex items-start gap-1.5">
782
790
  <span class="break-all">{{ conceptUriValue }}</span>
783
- <button @click="copyUri" class="flex-shrink-0 p-0.5 rounded text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors" :title="uriCopied ? 'Copied!' : 'Copy URI'" :aria-label="uriCopied ? 'URI copied' : 'Copy URI to clipboard'">
791
+ <button @click="copyUri" class="flex-shrink-0 p-0.5 rounded text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors" :title="uriCopied ? t('concept.uriCopied') : t('concept.copyUri')" :aria-label="uriCopied ? t('concept.uriCopied') : t('concept.copyUri')">
784
792
  <svg v-if="!uriCopied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10a2 2 0 01-2-2v-1m6 4v-3a2 2 0 00-2-2H8"/></svg>
785
793
  <svg v-else class="w-3.5 h-3.5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
786
794
  </button>
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue';
3
3
  import { FORMAT_REGISTRY } from '../utils/concept-formats';
4
+ import { useI18n } from '../i18n';
5
+
6
+ const { t } = useI18n();
4
7
 
5
8
  const props = defineProps<{
6
9
  registerId: string;
@@ -27,7 +30,7 @@ const links = computed<FormatLink[]>(() =>
27
30
 
28
31
  <template>
29
32
  <div v-if="links.length" class="space-y-2">
30
- <div class="section-label">Downloads</div>
33
+ <div class="section-label">{{ t('concept.downloads') }}</div>
31
34
  <div class="flex flex-wrap gap-2">
32
35
  <a
33
36
  v-for="link in links"
@@ -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 {
@@ -90,6 +94,7 @@ export interface DatasetConfig {
90
94
  color?: string;
91
95
  tags?: string[];
92
96
  languageOrder?: string[];
97
+ ref?: string;
93
98
  downloads?: string[];
94
99
  translations?: Record<string, { title?: string; description?: string }>;
95
100
  }
@@ -21,8 +21,8 @@ export interface RuntimeSiteConfig {
21
21
  header?: { family: string; source: string; weights?: number[]; url?: string };
22
22
  body?: { family: string; source: string; weights?: number[]; url?: string };
23
23
  };
24
- logo?: { path: string; alt: string; url?: string };
25
- 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 };
26
26
  ownerName?: string;
27
27
  ownerUrl?: string;
28
28
  };
@@ -64,6 +64,37 @@ concept.lifecycleDates: Lifecycle dates
64
64
  concept.conceptSources: Concept sources
65
65
  concept.incoming: Incoming
66
66
  concept.outgoing: Outgoing
67
+ concept.lang: lang
68
+ concept.rdf: RDF
69
+ concept.history: History
70
+ concept.languages: languages
71
+ concept.collapseAll: Collapse all
72
+ concept.expandAll: Expand all
73
+ concept.designationOnly: designation only
74
+ concept.previous: Previous concept
75
+ concept.next: Next concept
76
+ concept.of: of
77
+ concept.hasDefinition: Has definition
78
+ concept.designationOnlyTitle: Designation only
79
+ concept.languagesSidebar: "Languages ({count})"
80
+ concept.ontologicalMetadata: Ontological metadata
81
+ concept.classificationLabel: Classification
82
+ concept.reviewType: Review type
83
+ concept.release: Release
84
+ concept.lineageSimilarity: Lineage similarity
85
+ concept.script: Script
86
+ concept.conversionSystem: Conversion system
87
+ concept.status: Status
88
+ concept.reviewDate: Review Date
89
+ concept.decision: Decision
90
+ concept.uri: URI
91
+ concept.copyUri: Copy URI to clipboard
92
+ concept.uriCopied: URI copied
93
+ concept.downloads: Downloads
94
+ concept.failedToLoad: Failed to load concept
95
+ concept.notFound: Concept not found
96
+ concept.notFoundMsg: "The concept \"{id}\" does not exist in this dataset."
97
+ concept.backToDataset: Back to dataset
67
98
 
68
99
  # Sidebar
69
100
  sidebar.overview: Overview
@@ -126,3 +157,11 @@ graph.stubStatus: stub
126
157
  graph.collapseControls: Collapse controls
127
158
  graph.expandControls: Expand controls
128
159
  graph.loading: Loading graph data...
160
+
161
+ # Sidebar provenance
162
+ sidebar.provenance: Provenance
163
+ sidebar.publishedBy: published by
164
+ sidebar.updated: Updated
165
+ sidebar.concepts: concepts
166
+ sidebar.languages: languages
167
+ sidebar.viewSource: View source
@@ -64,6 +64,37 @@ concept.lifecycleDates: Dates du cycle de vie
64
64
  concept.conceptSources: Sources du concept
65
65
  concept.incoming: Entrantes
66
66
  concept.outgoing: Sortantes
67
+ concept.lang: lang.
68
+ concept.rdf: RDF
69
+ concept.history: Historique
70
+ concept.languages: langues
71
+ concept.collapseAll: Tout réduire
72
+ concept.expandAll: Tout développer
73
+ concept.designationOnly: désignation uniquement
74
+ concept.previous: Concept précédent
75
+ concept.next: Concept suivant
76
+ concept.of: de
77
+ concept.hasDefinition: A une définition
78
+ concept.designationOnlyTitle: Désignation uniquement
79
+ concept.languagesSidebar: "Langues ({count})"
80
+ concept.ontologicalMetadata: Métadonnées ontologiques
81
+ concept.classificationLabel: Classification
82
+ concept.reviewType: Type de révision
83
+ concept.release: Version
84
+ concept.lineageSimilarity: Similarité de lignée
85
+ concept.script: Écriture
86
+ concept.conversionSystem: Système de conversion
87
+ concept.status: Statut
88
+ concept.reviewDate: Date de révision
89
+ concept.decision: Décision
90
+ concept.uri: URI
91
+ concept.copyUri: Copier l'URI dans le presse-papiers
92
+ concept.uriCopied: URI copiée
93
+ concept.downloads: Téléchargements
94
+ concept.failedToLoad: Échec du chargement du concept
95
+ concept.notFound: Concept non trouvé
96
+ concept.notFoundMsg: "Le concept « {id} » n'existe pas dans ce jeu de données."
97
+ concept.backToDataset: Retour au jeu de données
67
98
 
68
99
  # Sidebar
69
100
  sidebar.overview: Vue d'ensemble
@@ -126,3 +157,11 @@ graph.stubStatus: non chargé
126
157
  graph.collapseControls: Réduire les contrôles
127
158
  graph.expandControls: Développer les contrôles
128
159
  graph.loading: Chargement des données du graphe...
160
+
161
+ # Sidebar provenance
162
+ sidebar.provenance: Provenance
163
+ sidebar.publishedBy: publié par
164
+ sidebar.updated: Mis à jour
165
+ sidebar.concepts: concepts
166
+ sidebar.languages: langues
167
+ sidebar.viewSource: Voir la source
package/src/style.css CHANGED
@@ -398,6 +398,12 @@
398
398
  .dark .placeholder\:text-ink-300::placeholder { color: #484a6e !important; }
399
399
 
400
400
  /* ── Semantic colors (badges, accent text) ── */
401
+ .dark .concept-link {
402
+ filter: brightness(1.5);
403
+ }
404
+ .dark .concept-link:hover {
405
+ filter: brightness(1.3) underline;
406
+ }
401
407
  .dark .bg-blue-50 { background-color: rgba(59, 130, 246, 0.15) !important; }
402
408
  .dark .text-blue-600 { color: #93bbfd !important; }
403
409
  .dark .text-blue-700 { color: #7aa8fb !important; }
package/src/utils/lang.ts CHANGED
@@ -1,28 +1,30 @@
1
- const LANG_NAMES: Record<string, string> = {
2
- eng: 'English',
3
- ara: 'Arabic',
4
- deu: 'German',
5
- fra: 'French',
6
- spa: 'Spanish',
7
- ita: 'Italian',
8
- jpn: 'Japanese',
9
- kor: 'Korean',
10
- pol: 'Polish',
11
- por: 'Portuguese',
12
- srp: 'Serbian',
13
- swe: 'Swedish',
14
- zho: 'Chinese',
15
- rus: 'Russian',
16
- fin: 'Finnish',
17
- dan: 'Danish',
18
- nld: 'Dutch',
19
- msa: 'Malay',
20
- nob: 'Norwegian Bokmål',
21
- nno: 'Norwegian Nynorsk',
1
+ import { locale } from '../i18n';
2
+
3
+ const LANG_NAMES: Record<string, Record<string, string>> = {
4
+ eng: { eng: 'English', fra: 'Anglais' },
5
+ ara: { eng: 'Arabic', fra: 'Arabe' },
6
+ deu: { eng: 'German', fra: 'Allemand' },
7
+ fra: { eng: 'French', fra: 'Français' },
8
+ spa: { eng: 'Spanish', fra: 'Espagnol' },
9
+ ita: { eng: 'Italian', fra: 'Italien' },
10
+ jpn: { eng: 'Japanese', fra: 'Japonais' },
11
+ kor: { eng: 'Korean', fra: 'Coréen' },
12
+ pol: { eng: 'Polish', fra: 'Polonais' },
13
+ por: { eng: 'Portuguese', fra: 'Portugais' },
14
+ srp: { eng: 'Serbian', fra: 'Serbe' },
15
+ swe: { eng: 'Swedish', fra: 'Suédois' },
16
+ zho: { eng: 'Chinese', fra: 'Chinois' },
17
+ rus: { eng: 'Russian', fra: 'Russe' },
18
+ fin: { eng: 'Finnish', fra: 'Finnois' },
19
+ dan: { eng: 'Danish', fra: 'Danois' },
20
+ nld: { eng: 'Dutch', fra: 'Néerlandais' },
21
+ msa: { eng: 'Malay', fra: 'Malais' },
22
+ nob: { eng: 'Norwegian Bokmål', fra: 'Norvégien Bokmål' },
23
+ nno: { eng: 'Norwegian Nynorsk', fra: 'Norvégien Nynorsk' },
22
24
  };
23
25
 
24
26
  export function langName(code: string): string {
25
- return LANG_NAMES[code] ?? code;
27
+ return LANG_NAMES[code]?.[locale.value] ?? LANG_NAMES[code]?.eng ?? code;
26
28
  }
27
29
 
28
30
  export function langLabel(code: string): string {
@@ -3,6 +3,9 @@ import { computed, watch, ref, onMounted, onUnmounted } from 'vue';
3
3
  import { useRouter } from 'vue-router';
4
4
  import { useVocabularyStore } from '../stores/vocabulary';
5
5
  import ConceptDetail from '../components/ConceptDetail.vue';
6
+ import { useI18n } from '../i18n';
7
+
8
+ const { t } = useI18n();
6
9
 
7
10
  const props = defineProps<{
8
11
  registerId: string;
@@ -108,12 +111,12 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown));
108
111
  </div>
109
112
  <div v-else-if="localError" class="max-w-xl mx-auto text-center py-20">
110
113
  <div class="card p-8 border-red-200 bg-red-50/50">
111
- <p class="text-red-700 font-medium mb-1">Failed to load concept</p>
114
+ <p class="text-red-700 font-medium mb-1">{{ t('concept.failedToLoad') }}</p>
112
115
  <p class="text-sm text-red-600/80 mb-4">{{ localError }}</p>
113
116
  <div class="flex gap-2 justify-center">
114
- <button @click="loadConcept(registerId, conceptId)" class="btn-primary">Retry</button>
117
+ <button @click="loadConcept(registerId, conceptId)" class="btn-primary">{{ t('dataset.retry') }}</button>
115
118
  <router-link :to="{ name: 'dataset', params: { registerId } }" class="btn-secondary">
116
- Back to dataset
119
+ {{ t('concept.backToDataset') }}
117
120
  </router-link>
118
121
  </div>
119
122
  </div>
@@ -121,10 +124,10 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown));
121
124
  <div v-else-if="!concept" class="max-w-xl mx-auto text-center py-20">
122
125
  <div class="card p-8">
123
126
  <div class="text-ink-200 text-5xl mb-3 font-serif">?</div>
124
- <h3 class="text-lg font-medium text-ink-700 mb-2">Concept not found</h3>
125
- <p class="text-sm text-ink-400 mb-4">The concept "{{ conceptId }}" does not exist in this dataset.</p>
127
+ <h3 class="text-lg font-medium text-ink-700 mb-2">{{ t('concept.notFound') }}</h3>
128
+ <p class="text-sm text-ink-400 mb-4">{{ t('concept.notFoundMsg', { id: conceptId }) }}</p>
126
129
  <router-link :to="{ name: 'dataset', params: { registerId } }" class="btn-primary">
127
- Back to dataset
130
+ {{ t('concept.backToDataset') }}
128
131
  </router-link>
129
132
  </div>
130
133
  </div>
@@ -7,15 +7,19 @@ import { FORMAT_LABELS } from '../config/types';
7
7
  import { langName, langLabel, sortLanguages } from '../utils/lang';
8
8
  import ConceptCard from '../components/ConceptCard.vue';
9
9
  import { useI18n } from '../i18n';
10
+ import { useSiteConfig } from '../config/use-site-config';
10
11
 
11
12
  const props = defineProps<{ registerId: string }>();
12
13
 
13
14
  const store = useVocabularyStore();
14
15
  const { getStyle } = useDsStyle();
15
- const { loading, localError, ensureLoaded } = useDatasetLoader(() => props.registerId);
16
+ const { ensureLoaded, loading, localError } = useDatasetLoader(() => props.registerId);
16
17
  const { t } = useI18n();
18
+ const { localizedDatasetField } = useSiteConfig();
17
19
 
18
20
  const manifest = computed(() => store.manifests.get(props.registerId));
21
+ const localizedTitle = computed(() => localizedDatasetField(props.registerId, 'title', manifest.value?.title));
22
+ const localizedDescription = computed(() => localizedDatasetField(props.registerId, 'description', manifest.value?.description));
19
23
  const adapter = computed(() => store.datasets.get(props.registerId));
20
24
  const chunkLoading = ref(false);
21
25
 
@@ -184,13 +188,13 @@ function goToPage(p: number) {
184
188
  <nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
185
189
  <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
186
190
  <span class="text-ink-200">/</span>
187
- <span class="text-ink-700">{{ manifest?.title || registerId }}</span>
191
+ <span class="text-ink-700">{{ localizedTitle }}</span>
188
192
  </nav>
189
193
 
190
194
  <!-- Header -->
191
195
  <div v-if="manifest" class="mb-8">
192
- <h1 class="font-serif text-3xl text-ink-800 mb-2">{{ manifest.title }}</h1>
193
- <p class="text-ink-400 leading-relaxed max-w-2xl">{{ manifest.description }}</p>
196
+ <h1 class="font-serif text-3xl text-ink-800 mb-2">{{ localizedTitle }}</h1>
197
+ <p class="text-ink-400 leading-relaxed max-w-2xl">{{ localizedDescription }}</p>
194
198
  <div class="flex flex-wrap gap-2 mt-4">
195
199
  <span class="badge" :style="{ backgroundColor: getStyle(registerId).light, color: getStyle(registerId).dark }">{{ manifest.conceptCount.toLocaleString() }} {{ t('dataset.concepts') }}</span>
196
200
  <span class="badge badge-gray">{{ manifest.languages.length }} {{ t('dataset.languages') }}</span>