@glossarist/concept-browser 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +32 -0
  2. package/cli/index.mjs +21 -0
  3. package/package.json +1 -1
  4. package/scripts/generate-data.mjs +43 -1
  5. package/scripts/generate-ontology-schema.mjs +312 -10
  6. package/src/App.vue +3 -0
  7. package/src/__tests__/concept-card.test.ts +16 -2
  8. package/src/__tests__/markdown-lite.test.ts +26 -0
  9. package/src/adapters/factory.ts +3 -2
  10. package/src/adapters/model-bridge.ts +2 -0
  11. package/src/adapters/ontology-schema.ts +89 -4
  12. package/src/adapters/types.ts +1 -0
  13. package/src/components/AppFooter.vue +3 -1
  14. package/src/components/AppHeader.vue +46 -4
  15. package/src/components/AppSidebar.vue +286 -46
  16. package/src/components/ConceptCard.vue +16 -4
  17. package/src/components/ConceptDetail.vue +42 -35
  18. package/src/components/ConceptRdfView.vue +3 -3
  19. package/src/components/ConceptTimeline.vue +2 -14
  20. package/src/components/GraphPanel.vue +19 -0
  21. package/src/components/LanguageDetail.vue +11 -8
  22. package/src/composables/use-ontology-nav.ts +183 -13
  23. package/src/composables/use-render-options.ts +2 -2
  24. package/src/config/types.ts +1 -0
  25. package/src/config/use-site-config.ts +3 -2
  26. package/src/data/ontology-schema.json +1721 -153
  27. package/src/i18n/index.ts +49 -0
  28. package/src/i18n/locales/eng.yml +66 -0
  29. package/src/i18n/locales/fra.yml +66 -0
  30. package/src/router/index.ts +10 -0
  31. package/src/shims/glossarist-tags.ts +10 -0
  32. package/src/stores/vocabulary.ts +1 -1
  33. package/src/style.css +12 -0
  34. package/src/utils/lang.ts +13 -0
  35. package/src/utils/markdown-lite.ts +15 -0
  36. package/src/views/AboutView.vue +1 -1
  37. package/src/views/ContributorsView.vue +1 -1
  38. package/src/views/DatasetView.vue +77 -6
  39. package/src/views/HomeView.vue +21 -17
  40. package/src/views/NewsView.vue +2 -2
  41. package/src/views/OntologySchemaView.vue +331 -14
  42. package/src/views/PageView.vue +27 -11
  43. package/src/views/ResolveView.vue +1 -1
  44. package/src/views/SearchView.vue +4 -2
  45. package/src/views/StatsView.vue +1 -1
  46. package/vite.config.ts +34 -1
@@ -0,0 +1,49 @@
1
+ import { ref } from 'vue';
2
+
3
+ // Auto-discover all locale YAML files — adding a new .yml file is all that's needed
4
+ const localeModules = import.meta.glob<{ default: Record<string, string> }>('./locales/*.yml', { eager: true });
5
+
6
+ const messages: Record<string, Record<string, string>> = {};
7
+ const availableLocales: string[] = [];
8
+
9
+ for (const path of Object.keys(localeModules)) {
10
+ const code = path.match(/\/([^/]+)\.yml$/)?.[1];
11
+ if (code) {
12
+ messages[code] = localeModules[path].default || {};
13
+ availableLocales.push(code);
14
+ }
15
+ }
16
+
17
+ const DEFAULT_LANG = 'eng';
18
+ const stored = typeof localStorage !== 'undefined'
19
+ ? (localStorage.getItem('ui-lang') || DEFAULT_LANG)
20
+ : DEFAULT_LANG;
21
+ const locale = ref(stored);
22
+
23
+ export function useI18n() {
24
+ function t(key: string, params?: Record<string, string>): string {
25
+ let msg = messages[locale.value]?.[key] || messages[DEFAULT_LANG]?.[key] || key;
26
+ if (params) {
27
+ for (const [k, v] of Object.entries(params)) {
28
+ msg = msg.replace(`{${k}}`, v);
29
+ }
30
+ }
31
+ return msg;
32
+ }
33
+
34
+ function setLocale(lang: string) {
35
+ locale.value = lang;
36
+ localStorage.setItem('ui-lang', lang);
37
+ }
38
+
39
+ function initLocale(configuredDefault?: string) {
40
+ const storedLang = localStorage.getItem('ui-lang');
41
+ if (storedLang && availableLocales.includes(storedLang)) {
42
+ locale.value = storedLang;
43
+ } else if (configuredDefault && availableLocales.includes(configuredDefault)) {
44
+ locale.value = configuredDefault;
45
+ }
46
+ }
47
+
48
+ return { locale, t, setLocale, initLocale, availableLocales };
49
+ }
@@ -0,0 +1,66 @@
1
+ # English UI translations
2
+ # Adding a new language: copy this file, rename to {lang-code}.yml, translate values.
3
+
4
+ # Navigation
5
+ nav.home: Home
6
+ nav.search: Search
7
+ nav.graph: Graph View
8
+ nav.ontology: Ontology
9
+ nav.concepts: Concepts
10
+ nav.statistics: Statistics
11
+ nav.about: About
12
+ nav.navigation: Navigation
13
+ nav.datasets: Datasets
14
+
15
+ # Home page
16
+ home.search: Search
17
+ home.graphView: Graph View
18
+ home.surpriseMe: Surprise Me
19
+ home.exploring: Exploring…
20
+ home.datasets: Datasets
21
+ home.concepts: Concepts
22
+ home.languages: Languages
23
+ home.availableDatasets: Available Datasets
24
+ home.clickToBrowse: Click to browse
25
+ home.browseConcepts: Browse concepts
26
+
27
+ # Search
28
+ search.placeholder: Search...
29
+ search.conceptSearch: Search concepts
30
+
31
+ # Page view
32
+ page.notFound: Page Not Found
33
+ page.notFoundMsg: 'The page "{name}" does not exist.'
34
+ page.goHome: Go Home
35
+
36
+ # Concept detail
37
+ concept.definition: Definition
38
+ concept.notes: Notes
39
+ concept.note: Note
40
+ concept.examples: Examples
41
+ concept.example: Example
42
+ concept.sources: Sources
43
+ concept.designations: Designations
44
+ concept.relations: Relations
45
+ concept.classification: Classification
46
+ concept.domains: Domains
47
+ concept.tags: Tags
48
+ concept.metadata: Metadata
49
+ concept.noData: No data available for this language.
50
+ concept.lifecycleDates: Lifecycle dates
51
+ concept.conceptSources: Concept sources
52
+ concept.incoming: Incoming
53
+ concept.outgoing: Outgoing
54
+
55
+ # Sidebar
56
+ sidebar.overview: Overview
57
+ sidebar.dataset: Dataset
58
+
59
+ # Footer
60
+ footer.builtWith: Built with the Glossarist Concept Browser
61
+
62
+ # Header
63
+ header.datasets: datasets
64
+
65
+ # Language
66
+ lang.termOnlyIn: Term only in
@@ -0,0 +1,66 @@
1
+ # Traductions françaises de l'interface
2
+ # French UI translations
3
+
4
+ # Navigation
5
+ nav.home: Accueil
6
+ nav.search: Recherche
7
+ nav.graph: Vue en graphe
8
+ nav.ontology: Ontologie
9
+ nav.concepts: Concepts
10
+ nav.statistics: Statistiques
11
+ nav.about: À propos
12
+ nav.navigation: Navigation
13
+ nav.datasets: Jeux de données
14
+
15
+ # Home page
16
+ home.search: Rechercher
17
+ home.graphView: Vue en graphe
18
+ home.surpriseMe: Surprenez-moi
19
+ home.exploring: Exploration…
20
+ home.datasets: Jeux de données
21
+ home.concepts: Concepts
22
+ home.languages: Langues
23
+ home.availableDatasets: Jeux de données disponibles
24
+ home.clickToBrowse: Cliquez pour parcourir
25
+ home.browseConcepts: Parcourir les concepts
26
+
27
+ # Search
28
+ search.placeholder: Rechercher…
29
+ search.conceptSearch: Rechercher des concepts
30
+
31
+ # Page view
32
+ page.notFound: Page non trouvée
33
+ page.notFoundMsg: 'La page « {name} » n''existe pas.'
34
+ page.goHome: Aller à l'accueil
35
+
36
+ # Concept detail
37
+ concept.definition: Définition
38
+ concept.notes: Notes
39
+ concept.note: Note
40
+ concept.examples: Exemples
41
+ concept.example: Exemple
42
+ concept.sources: Sources
43
+ concept.designations: Désignations
44
+ concept.relations: Relations
45
+ concept.classification: Classification
46
+ concept.domains: Domaines
47
+ concept.tags: Étiquettes
48
+ concept.metadata: Métadonnées
49
+ concept.noData: Aucune donnée disponible pour cette langue.
50
+ concept.lifecycleDates: Dates du cycle de vie
51
+ concept.conceptSources: Sources du concept
52
+ concept.incoming: Entrantes
53
+ concept.outgoing: Sortantes
54
+
55
+ # Sidebar
56
+ sidebar.overview: Vue d'ensemble
57
+ sidebar.dataset: Jeu de données
58
+
59
+ # Footer
60
+ footer.builtWith: Construit avec le Glossarist Concept Browser
61
+
62
+ # Header
63
+ header.datasets: jeux de données
64
+
65
+ # Language
66
+ lang.termOnlyIn: Terme uniquement en
@@ -55,6 +55,16 @@ export const routes: RouteRecordRaw[] = [
55
55
  name: 'ontology-taxonomy',
56
56
  component: () => import('../views/OntologySchemaView.vue'),
57
57
  },
58
+ {
59
+ path: '/ontology/shape/:shapeId',
60
+ name: 'ontology-shape',
61
+ component: () => import('../views/OntologySchemaView.vue'),
62
+ },
63
+ {
64
+ path: '/ontology/property/:propertyId',
65
+ name: 'ontology-property',
66
+ component: () => import('../views/OntologySchemaView.vue'),
67
+ },
58
68
  {
59
69
  path: '/news',
60
70
  name: 'news',
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Module augmentation for glossarist Concept.tags.
3
+ * The JS runtime already supports tags (string[]) but the installed
4
+ * type declarations have not been updated yet. This shim bridges the gap.
5
+ */
6
+ declare module 'glossarist/models' {
7
+ interface Concept {
8
+ tags: string[];
9
+ }
10
+ }
@@ -52,7 +52,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
52
52
  loading.value = true;
53
53
  error.value = null;
54
54
  try {
55
- const adapters = await factory.discoverDatasets('/datasets.json');
55
+ const adapters = await factory.discoverDatasets(`${import.meta.env.BASE_URL}datasets.json`);
56
56
  for (const adapter of adapters) {
57
57
  datasets.value.set(adapter.registerId, adapter);
58
58
  if (adapter.manifest) {
package/src/style.css CHANGED
@@ -244,6 +244,18 @@
244
244
  .prose-page hr {
245
245
  @apply border-ink-100 my-6;
246
246
  }
247
+ .prose-page table,
248
+ .prose-news table {
249
+ @apply w-full mb-4 text-sm;
250
+ }
251
+ .prose-page th,
252
+ .prose-news th {
253
+ @apply text-left font-semibold text-ink-800 px-3 py-2 border-b-2 border-ink-200;
254
+ }
255
+ .prose-page td,
256
+ .prose-news td {
257
+ @apply px-3 py-2 border-b border-ink-100;
258
+ }
247
259
  }
248
260
 
249
261
  @layer utilities {
package/src/utils/lang.ts CHANGED
@@ -29,4 +29,17 @@ export function langLabel(code: string): string {
29
29
  return code.toUpperCase();
30
30
  }
31
31
 
32
+ const FALLBACK_LANG_ORDER = ['eng', 'fra'];
33
+
34
+ export function sortLanguages(languages: string[], order?: string[]): string[] {
35
+ const priority = order ?? FALLBACK_LANG_ORDER;
36
+ const index = new Map(priority.map((l, i) => [l, i]));
37
+ return [...languages].sort((a, b) => {
38
+ const ai = index.get(a) ?? priority.length;
39
+ const bi = index.get(b) ?? priority.length;
40
+ if (ai !== bi) return ai - bi;
41
+ return a.localeCompare(b);
42
+ });
43
+ }
44
+
32
45
  export const DEFAULT_LANG = 'eng';
@@ -83,6 +83,21 @@ export function renderMarkdown(input: string): string {
83
83
  continue;
84
84
  }
85
85
 
86
+ // Table
87
+ if (/^\|(.+)\|$/.test(line) && i + 1 < lines.length && /^\|[-:| ]+\|$/.test(lines[i + 1])) {
88
+ const headerCells = line.split('|').map(c => c.trim()).filter(Boolean);
89
+ i += 2; // skip header and separator
90
+ const rows: string[][] = [];
91
+ while (i < lines.length && /^\|(.+)\|$/.test(lines[i])) {
92
+ rows.push(lines[i].split('|').map(c => c.trim()).filter(Boolean));
93
+ i++;
94
+ }
95
+ const thCells = headerCells.map(c => `<th>${renderInline(c)}</th>`).join('');
96
+ const trRows = rows.map(r => `<tr>${r.map(c => `<td>${renderInline(c)}</td>`).join('')}</tr>`).join('');
97
+ blocks.push(`<table><thead><tr>${thCells}</tr></thead><tbody>${trRows}</tbody></table>`);
98
+ continue;
99
+ }
100
+
86
101
  // Blank line
87
102
  if (!line.trim()) {
88
103
  i++;
@@ -15,7 +15,7 @@ const manifest = computed(() => store.manifests.get(resolvedId.value));
15
15
  </script>
16
16
 
17
17
  <template>
18
- <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
18
+ <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
19
19
  <!-- Breadcrumb -->
20
20
  <nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
21
21
  <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
@@ -14,7 +14,7 @@ const contributors = config.value?.contributors as Contributor[] | undefined;
14
14
  </script>
15
15
 
16
16
  <template>
17
- <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
17
+ <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
18
18
  <nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
19
19
  <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
20
20
  <span class="text-ink-200">/</span>
@@ -4,6 +4,7 @@ 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 { FORMAT_LABELS } from '../config/types';
7
+ import { langName, langLabel, sortLanguages } from '../utils/lang';
7
8
  import ConceptCard from '../components/ConceptCard.vue';
8
9
 
9
10
  const props = defineProps<{ registerId: string }>();
@@ -38,6 +39,26 @@ const totalConceptCount = computed(() => adapter.value?.getConceptCount() ?? 0);
38
39
  const filter = ref('');
39
40
  const filterInput = ref<HTMLInputElement | null>(null);
40
41
  const allChunksLoaded = ref(false);
42
+ const selectedLang = ref<string | null>(null);
43
+
44
+ interface LangOption {
45
+ code: string;
46
+ name: string;
47
+ label: string;
48
+ termCount: number;
49
+ }
50
+
51
+ const languageOptions = computed<LangOption[]>(() => {
52
+ const m = manifest.value;
53
+ if (!m) return [];
54
+ const sorted = sortLanguages(m.languages, m.languageOrder);
55
+ return sorted.map(code => ({
56
+ code,
57
+ name: langName(code),
58
+ label: langLabel(code),
59
+ termCount: m.languageStats?.[code]?.terms ?? 0,
60
+ }));
61
+ });
41
62
 
42
63
  function onGlobalKeydown(e: KeyboardEvent) {
43
64
  if (e.key === '/' && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
@@ -65,6 +86,17 @@ watch(filter, async (q) => {
65
86
  }
66
87
  });
67
88
 
89
+ // When language filter changes, reset page and load all chunks
90
+ watch(selectedLang, async (lang) => {
91
+ page.value = 1;
92
+ if (lang && !allChunksLoaded.value && adapter.value) {
93
+ chunkLoading.value = true;
94
+ await adapter.value.ensureAllChunksLoaded();
95
+ allChunksLoaded.value = true;
96
+ chunkLoading.value = false;
97
+ }
98
+ });
99
+
68
100
  // Dense array: only loaded (non-undefined) entries
69
101
  const loadedConcepts = computed(() => {
70
102
  const arr = adapter.value?.getConcepts();
@@ -74,8 +106,10 @@ const loadedConcepts = computed(() => {
74
106
 
75
107
  const filtered = computed(() => {
76
108
  const q = filter.value.trim().toLowerCase();
77
- if (!q) return loadedConcepts.value;
109
+ const lang = selectedLang.value;
78
110
  return loadedConcepts.value.filter(c => {
111
+ if (lang && !(lang in (c.designations ?? {}))) return false;
112
+ if (!q) return true;
79
113
  return (c.eng || '').toLowerCase().includes(q) || c.id.toLowerCase().includes(q);
80
114
  });
81
115
  });
@@ -91,8 +125,8 @@ const pageLoaded = computed(() => {
91
125
  });
92
126
 
93
127
  const paged = computed(() => {
94
- // When filtering, paginate over filtered dense results (all chunks loaded)
95
- if (filter.value.trim()) {
128
+ // When filtering (text or language), paginate over filtered dense results (all chunks loaded)
129
+ if (filter.value.trim() || selectedLang.value) {
96
130
  const start = (page.value - 1) * perPage;
97
131
  return filtered.value.slice(start, start + perPage);
98
132
  }
@@ -104,7 +138,7 @@ const paged = computed(() => {
104
138
  });
105
139
 
106
140
  const totalPages = computed(() => {
107
- if (filter.value.trim()) {
141
+ if (filter.value.trim() || selectedLang.value) {
108
142
  return Math.max(1, Math.ceil(filtered.value.length / perPage));
109
143
  }
110
144
  return Math.max(1, Math.ceil(totalConceptCount.value / perPage));
@@ -112,7 +146,7 @@ const totalPages = computed(() => {
112
146
 
113
147
  // Load chunks needed for current page
114
148
  watch(page, async () => {
115
- if (!adapter.value || filter.value.trim()) return;
149
+ if (!adapter.value || filter.value.trim() || selectedLang.value) return;
116
150
  const start = (page.value - 1) * perPage;
117
151
  if (!adapter.value.isRangeLoaded(start, perPage)) {
118
152
  chunkLoading.value = true;
@@ -229,7 +263,10 @@ function goToPage(p: number) {
229
263
  </svg>
230
264
  </div>
231
265
  <span class="text-sm text-ink-400">
232
- <template v-if="filter.trim()">
266
+ <template v-if="selectedLang">
267
+ {{ filtered.length.toLocaleString() }} of {{ totalConceptCount.toLocaleString() }} concepts in {{ langName(selectedLang) }}
268
+ </template>
269
+ <template v-else-if="filter.trim()">
233
270
  {{ filtered.length.toLocaleString() }} of {{ totalConceptCount.toLocaleString() }} concepts
234
271
  </template>
235
272
  <template v-else-if="totalPages > 1">
@@ -241,6 +278,39 @@ function goToPage(p: number) {
241
278
  </span>
242
279
  </div>
243
280
 
281
+ <!-- Language filter -->
282
+ <div v-if="languageOptions.length > 1" class="flex flex-wrap gap-1.5 mb-5">
283
+ <button
284
+ @click="selectedLang = null"
285
+ :class="[
286
+ selectedLang === null
287
+ ? 'bg-ink-800 text-white'
288
+ : 'bg-surface-raised text-ink-600 hover:bg-ink-50 border border-ink-100'
289
+ ]"
290
+ class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
291
+ >
292
+ All {{ totalConceptCount.toLocaleString() }}
293
+ </button>
294
+ <button
295
+ v-for="lang in languageOptions"
296
+ :key="lang.code"
297
+ @click="selectedLang = selectedLang === lang.code ? null : lang.code"
298
+ :class="[
299
+ selectedLang === lang.code
300
+ ? 'bg-ink-800 text-white'
301
+ : 'bg-surface-raised text-ink-600 hover:bg-ink-50 border border-ink-100'
302
+ ]"
303
+ class="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors flex items-center gap-1.5"
304
+ >
305
+ <span
306
+ class="text-[10px] font-semibold px-1.5 py-0.5 rounded"
307
+ :class="selectedLang === lang.code ? 'bg-ink-700 text-ink-200' : 'bg-ink-50 text-ink-500'"
308
+ >{{ lang.label }}</span>
309
+ {{ lang.name }}
310
+ <span class="text-[10px] opacity-60">{{ lang.termCount }}</span>
311
+ </button>
312
+ </div>
313
+
244
314
  <!-- Chunk loading skeleton -->
245
315
  <div v-if="chunkLoading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
246
316
  <div v-for="i in 6" :key="i" class="skeleton h-20"></div>
@@ -253,6 +323,7 @@ function goToPage(p: number) {
253
323
  :key="entry.id"
254
324
  :entry="entry"
255
325
  :register-id="registerId"
326
+ :display-lang="selectedLang"
256
327
  class="animate-entrance"
257
328
  :style="{ animationDelay: `${Math.min(idx, 20) * 30}ms` }"
258
329
  />
@@ -4,11 +4,13 @@ import { useVocabularyStore } from '../stores/vocabulary';
4
4
  import { useRouter } from 'vue-router';
5
5
  import { useDsStyle } from '../utils/dataset-style';
6
6
  import { useSiteConfig } from '../config/use-site-config';
7
+ import { useI18n } from '../i18n';
7
8
 
8
9
  const store = useVocabularyStore();
9
10
  const router = useRouter();
10
11
  const { getStyle } = useDsStyle();
11
12
  const { config: siteConfig } = useSiteConfig();
13
+ const { t } = useI18n();
12
14
  const exploring = ref(false);
13
15
 
14
16
  async function exploreRandom() {
@@ -61,12 +63,14 @@ function goToGraph() { router.push({ name: 'graph' }); }
61
63
  <div class="mb-10 sm:mb-14">
62
64
  <div class="flex items-center gap-2 mb-4">
63
65
  <span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300">{{ siteConfig?.branding?.ownerName || 'Glossarist' }}</span>
64
- <span class="w-4 sm:w-6 h-px bg-ink-200"></span>
65
- <span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300 hidden sm:inline">{{ siteConfig?.subtitle || 'Terminology Register' }}</span>
66
+ <template v-if="siteConfig?.subtitle">
67
+ <span class="w-4 sm:w-6 h-px bg-ink-200"></span>
68
+ <span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300 hidden sm:inline">{{ siteConfig.subtitle }}</span>
69
+ </template>
66
70
  </div>
67
71
  <h1 class="font-serif text-[2rem] sm:text-[2.75rem] text-ink-800 leading-[1.1] mb-4 tracking-tight">
68
- {{ siteConfig?.title || 'Glossarist' }}<br class="hidden sm:block" /> <span v-if="siteConfig?.subtitle">{{ siteConfig.subtitle }}</span>
69
- <template v-if="!siteConfig?.subtitle">Terminology<br class="hidden sm:block" /> Register</template>
72
+ {{ siteConfig?.title || 'Glossarist' }}
73
+ <template v-if="siteConfig?.subtitle"><br class="hidden sm:block" /> {{ siteConfig.subtitle }}</template>
70
74
  </h1>
71
75
  <p class="text-base text-ink-400 max-w-lg leading-relaxed">
72
76
  {{ siteConfig?.description || 'Explore standardized terminology datasets from ISO and IEC technical committees. Browse concepts, definitions, and cross-references across multilingual vocabularies.' }}
@@ -74,16 +78,16 @@ function goToGraph() { router.push({ name: 'graph' }); }
74
78
  <div class="flex flex-wrap gap-3 mt-7">
75
79
  <button @click="goToSearch" class="btn-primary flex items-center gap-2">
76
80
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
77
- Search
81
+ {{ t('home.search') }}
78
82
  </button>
79
83
  <button @click="goToGraph" class="btn-secondary flex items-center gap-2">
80
84
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
81
- Graph View
85
+ {{ t('home.graphView') }}
82
86
  </button>
83
87
  <button @click="exploreRandom" :disabled="exploring" class="btn-secondary flex items-center gap-2">
84
88
  <svg v-if="!exploring" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
85
89
  <svg v-else class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
86
- {{ exploring ? 'Exploring…' : 'Surprise Me' }}
90
+ {{ exploring ? t('home.exploring') : t('home.surpriseMe') }}
87
91
  </button>
88
92
  </div>
89
93
  </div>
@@ -92,15 +96,15 @@ function goToGraph() { router.push({ name: 'graph' }); }
92
96
  <div class="grid grid-cols-1 sm:grid-cols-3 gap-px bg-ink-100/60 rounded-xl overflow-hidden mb-10 sm:mb-14">
93
97
  <div class="bg-surface-raised px-4 sm:px-6 py-5 animate-entrance" style="animation-delay: 80ms">
94
98
  <div class="text-3xl font-serif text-ink-800 tabular-nums">{{ filteredDatasets.length }}</div>
95
- <div class="text-sm text-ink-400 mt-1">Datasets</div>
99
+ <div class="text-sm text-ink-400 mt-1">{{ t('home.datasets') }}</div>
96
100
  </div>
97
101
  <div class="bg-surface-raised px-6 py-5 animate-entrance" style="animation-delay: 140ms">
98
102
  <div class="text-3xl font-serif text-ink-800 tabular-nums">{{ totalConcepts.toLocaleString() }}</div>
99
- <div class="text-sm text-ink-400 mt-1">Concepts</div>
103
+ <div class="text-sm text-ink-400 mt-1">{{ t('home.concepts') }}</div>
100
104
  </div>
101
105
  <div class="bg-surface-raised px-6 py-5 animate-entrance" style="animation-delay: 200ms">
102
106
  <div class="text-3xl font-serif text-ink-800 tabular-nums">{{ totalLanguages }}</div>
103
- <div class="text-sm text-ink-400 mt-1">Languages</div>
107
+ <div class="text-sm text-ink-400 mt-1">{{ t('home.languages') }}</div>
104
108
  </div>
105
109
  </div>
106
110
 
@@ -122,14 +126,14 @@ function goToGraph() { router.push({ name: 'graph' }); }
122
126
  </p>
123
127
  <div class="flex items-center gap-3 pl-6 mb-4">
124
128
  <span :style="{ color: getStyle(filteredDatasets[0].id).color }" class="text-sm font-semibold tabular-nums">{{ filteredDatasets[0].manifest.conceptCount.toLocaleString() }}</span>
125
- <span class="text-xs text-ink-300">concepts</span>
129
+ <span class="text-xs text-ink-300">{{ t('home.concepts').toLowerCase() }}</span>
126
130
  <span class="text-ink-200 text-xs">&middot;</span>
127
131
  <span class="text-sm text-ink-500 tabular-nums">{{ filteredDatasets[0].manifest.languages.length }}</span>
128
- <span class="text-xs text-ink-300">languages</span>
132
+ <span class="text-xs text-ink-300">{{ t('home.languages').toLowerCase() }}</span>
129
133
  </div>
130
134
  <div class="pl-6">
131
135
  <span class="btn-primary inline-flex items-center gap-2">
132
- Browse concepts
136
+ {{ t('home.browseConcepts') }}
133
137
  <svg class="w-4 h-4 group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
134
138
  </span>
135
139
  </div>
@@ -137,8 +141,8 @@ function goToGraph() { router.push({ name: 'graph' }); }
137
141
  </template>
138
142
  <template v-else-if="filteredDatasets.length > 1">
139
143
  <div class="flex items-center justify-between mb-5">
140
- <div class="section-label mb-0">Available Datasets</div>
141
- <span class="text-xs text-ink-300">Click to browse</span>
144
+ <div class="section-label mb-0">{{ t('home.availableDatasets') }}</div>
145
+ <span class="text-xs text-ink-300">{{ t('home.clickToBrowse') }}</span>
142
146
  </div>
143
147
  <div :class="[
144
148
  filteredDatasets.length === 2 ? 'max-w-3xl' : '',
@@ -166,10 +170,10 @@ function goToGraph() { router.push({ name: 'graph' }); }
166
170
 
167
171
  <div class="flex items-center gap-3 pl-[22px] mb-3">
168
172
  <span :style="{ color: getStyle(ds.id).color }" class="text-sm font-semibold tabular-nums">{{ ds.manifest.conceptCount.toLocaleString() }}</span>
169
- <span class="text-xs text-ink-300">concepts</span>
173
+ <span class="text-xs text-ink-300">{{ t('home.concepts').toLowerCase() }}</span>
170
174
  <span class="text-ink-200 text-xs">&middot;</span>
171
175
  <span class="text-sm text-ink-500 tabular-nums">{{ ds.manifest.languages.length }}</span>
172
- <span class="text-xs text-ink-300">languages</span>
176
+ <span class="text-xs text-ink-300">{{ t('home.languages').toLowerCase() }}</span>
173
177
  </div>
174
178
 
175
179
  <div class="flex flex-wrap gap-1.5 pl-[22px] mb-3">
@@ -23,7 +23,7 @@ const activeLoading = ref(false);
23
23
 
24
24
  onMounted(async () => {
25
25
  try {
26
- const resp = await fetch('/news.json');
26
+ const resp = await fetch(`${import.meta.env.BASE_URL}news.json`);
27
27
  if (resp.ok) posts.value = await resp.json();
28
28
  } catch (e: any) {
29
29
  error.value = e.message;
@@ -78,7 +78,7 @@ function formatDate(dateStr: string) {
78
78
  </script>
79
79
 
80
80
  <template>
81
- <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
81
+ <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
82
82
  <nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
83
83
  <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
84
84
  <span class="text-ink-200">/</span>