@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.
- package/README.md +32 -0
- package/cli/index.mjs +21 -0
- package/package.json +1 -1
- package/scripts/generate-data.mjs +43 -1
- package/scripts/generate-ontology-schema.mjs +312 -10
- package/src/App.vue +3 -0
- package/src/__tests__/concept-card.test.ts +16 -2
- package/src/__tests__/markdown-lite.test.ts +26 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -0
- package/src/adapters/ontology-schema.ts +89 -4
- package/src/adapters/types.ts +1 -0
- package/src/components/AppFooter.vue +3 -1
- package/src/components/AppHeader.vue +46 -4
- package/src/components/AppSidebar.vue +286 -46
- package/src/components/ConceptCard.vue +16 -4
- package/src/components/ConceptDetail.vue +42 -35
- package/src/components/ConceptRdfView.vue +3 -3
- package/src/components/ConceptTimeline.vue +2 -14
- package/src/components/GraphPanel.vue +19 -0
- package/src/components/LanguageDetail.vue +11 -8
- package/src/composables/use-ontology-nav.ts +183 -13
- package/src/composables/use-render-options.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/config/use-site-config.ts +3 -2
- package/src/data/ontology-schema.json +1721 -153
- package/src/i18n/index.ts +49 -0
- package/src/i18n/locales/eng.yml +66 -0
- package/src/i18n/locales/fra.yml +66 -0
- package/src/router/index.ts +10 -0
- package/src/shims/glossarist-tags.ts +10 -0
- package/src/stores/vocabulary.ts +1 -1
- package/src/style.css +12 -0
- package/src/utils/lang.ts +13 -0
- package/src/utils/markdown-lite.ts +15 -0
- package/src/views/AboutView.vue +1 -1
- package/src/views/ContributorsView.vue +1 -1
- package/src/views/DatasetView.vue +77 -6
- package/src/views/HomeView.vue +21 -17
- package/src/views/NewsView.vue +2 -2
- package/src/views/OntologySchemaView.vue +331 -14
- package/src/views/PageView.vue +27 -11
- package/src/views/ResolveView.vue +1 -1
- package/src/views/SearchView.vue +4 -2
- package/src/views/StatsView.vue +1 -1
- 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
|
package/src/router/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/stores/vocabulary.ts
CHANGED
|
@@ -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(
|
|
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++;
|
package/src/views/AboutView.vue
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
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="
|
|
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
|
/>
|
package/src/views/HomeView.vue
CHANGED
|
@@ -4,11 +4,13 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
4
4
|
import { useRouter } from 'vue-router';
|
|
5
5
|
import { useDsStyle } from '../utils/dataset-style';
|
|
6
6
|
import { useSiteConfig } from '../config/use-site-config';
|
|
7
|
+
import { useI18n } from '../i18n';
|
|
7
8
|
|
|
8
9
|
const store = useVocabularyStore();
|
|
9
10
|
const router = useRouter();
|
|
10
11
|
const { getStyle } = useDsStyle();
|
|
11
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
|
-
<
|
|
65
|
-
|
|
66
|
+
<template v-if="siteConfig?.subtitle">
|
|
67
|
+
<span class="w-4 sm:w-6 h-px bg-ink-200"></span>
|
|
68
|
+
<span class="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-300 hidden sm:inline">{{ 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' }}
|
|
69
|
-
<template v-if="
|
|
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
|
-
|
|
81
|
+
{{ t('home.search') }}
|
|
78
82
|
</button>
|
|
79
83
|
<button @click="goToGraph" class="btn-secondary flex items-center gap-2">
|
|
80
84
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
|
81
|
-
|
|
85
|
+
{{ t('home.graphView') }}
|
|
82
86
|
</button>
|
|
83
87
|
<button @click="exploreRandom" :disabled="exploring" class="btn-secondary flex items-center gap-2">
|
|
84
88
|
<svg v-if="!exploring" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
85
89
|
<svg v-else class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
|
86
|
-
{{ exploring ? '
|
|
90
|
+
{{ exploring ? t('home.exploring') : t('home.surpriseMe') }}
|
|
87
91
|
</button>
|
|
88
92
|
</div>
|
|
89
93
|
</div>
|
|
@@ -92,15 +96,15 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
92
96
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-px bg-ink-100/60 rounded-xl overflow-hidden mb-10 sm:mb-14">
|
|
93
97
|
<div class="bg-surface-raised px-4 sm:px-6 py-5 animate-entrance" style="animation-delay: 80ms">
|
|
94
98
|
<div class="text-3xl font-serif text-ink-800 tabular-nums">{{ filteredDatasets.length }}</div>
|
|
95
|
-
<div class="text-sm text-ink-400 mt-1">
|
|
99
|
+
<div class="text-sm text-ink-400 mt-1">{{ t('home.datasets') }}</div>
|
|
96
100
|
</div>
|
|
97
101
|
<div class="bg-surface-raised px-6 py-5 animate-entrance" style="animation-delay: 140ms">
|
|
98
102
|
<div class="text-3xl font-serif text-ink-800 tabular-nums">{{ totalConcepts.toLocaleString() }}</div>
|
|
99
|
-
<div class="text-sm text-ink-400 mt-1">
|
|
103
|
+
<div class="text-sm text-ink-400 mt-1">{{ t('home.concepts') }}</div>
|
|
100
104
|
</div>
|
|
101
105
|
<div class="bg-surface-raised px-6 py-5 animate-entrance" style="animation-delay: 200ms">
|
|
102
106
|
<div class="text-3xl font-serif text-ink-800 tabular-nums">{{ totalLanguages }}</div>
|
|
103
|
-
<div class="text-sm text-ink-400 mt-1">
|
|
107
|
+
<div class="text-sm text-ink-400 mt-1">{{ t('home.languages') }}</div>
|
|
104
108
|
</div>
|
|
105
109
|
</div>
|
|
106
110
|
|
|
@@ -122,14 +126,14 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
122
126
|
</p>
|
|
123
127
|
<div class="flex items-center gap-3 pl-6 mb-4">
|
|
124
128
|
<span :style="{ color: getStyle(filteredDatasets[0].id).color }" class="text-sm font-semibold tabular-nums">{{ filteredDatasets[0].manifest.conceptCount.toLocaleString() }}</span>
|
|
125
|
-
<span class="text-xs text-ink-300">concepts</span>
|
|
129
|
+
<span class="text-xs text-ink-300">{{ t('home.concepts').toLowerCase() }}</span>
|
|
126
130
|
<span class="text-ink-200 text-xs">·</span>
|
|
127
131
|
<span class="text-sm text-ink-500 tabular-nums">{{ filteredDatasets[0].manifest.languages.length }}</span>
|
|
128
|
-
<span class="text-xs text-ink-300">languages</span>
|
|
132
|
+
<span class="text-xs text-ink-300">{{ t('home.languages').toLowerCase() }}</span>
|
|
129
133
|
</div>
|
|
130
134
|
<div class="pl-6">
|
|
131
135
|
<span class="btn-primary inline-flex items-center gap-2">
|
|
132
|
-
|
|
136
|
+
{{ t('home.browseConcepts') }}
|
|
133
137
|
<svg class="w-4 h-4 group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
134
138
|
</span>
|
|
135
139
|
</div>
|
|
@@ -137,8 +141,8 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
137
141
|
</template>
|
|
138
142
|
<template v-else-if="filteredDatasets.length > 1">
|
|
139
143
|
<div class="flex items-center justify-between mb-5">
|
|
140
|
-
<div class="section-label mb-0">
|
|
141
|
-
<span class="text-xs text-ink-300">
|
|
144
|
+
<div class="section-label mb-0">{{ t('home.availableDatasets') }}</div>
|
|
145
|
+
<span class="text-xs text-ink-300">{{ t('home.clickToBrowse') }}</span>
|
|
142
146
|
</div>
|
|
143
147
|
<div :class="[
|
|
144
148
|
filteredDatasets.length === 2 ? 'max-w-3xl' : '',
|
|
@@ -166,10 +170,10 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
166
170
|
|
|
167
171
|
<div class="flex items-center gap-3 pl-[22px] mb-3">
|
|
168
172
|
<span :style="{ color: getStyle(ds.id).color }" class="text-sm font-semibold tabular-nums">{{ ds.manifest.conceptCount.toLocaleString() }}</span>
|
|
169
|
-
<span class="text-xs text-ink-300">concepts</span>
|
|
173
|
+
<span class="text-xs text-ink-300">{{ t('home.concepts').toLowerCase() }}</span>
|
|
170
174
|
<span class="text-ink-200 text-xs">·</span>
|
|
171
175
|
<span class="text-sm text-ink-500 tabular-nums">{{ ds.manifest.languages.length }}</span>
|
|
172
|
-
<span class="text-xs text-ink-300">languages</span>
|
|
176
|
+
<span class="text-xs text-ink-300">{{ t('home.languages').toLowerCase() }}</span>
|
|
173
177
|
</div>
|
|
174
178
|
|
|
175
179
|
<div class="flex flex-wrap gap-1.5 pl-[22px] mb-3">
|
package/src/views/NewsView.vue
CHANGED
|
@@ -23,7 +23,7 @@ const activeLoading = ref(false);
|
|
|
23
23
|
|
|
24
24
|
onMounted(async () => {
|
|
25
25
|
try {
|
|
26
|
-
const resp = await fetch(
|
|
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-
|
|
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>
|