@glossarist/concept-browser 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,9 @@ import { useRouter } from 'vue-router';
11
11
  import { useVocabularyStore } from '../stores/vocabulary';
12
12
  import { getFactory } from '../adapters/factory';
13
13
  import CitationDisplay from './CitationDisplay.vue';
14
+ import { useI18n } from '../i18n';
15
+
16
+ const { t } = useI18n();
14
17
 
15
18
  const props = defineProps<{
16
19
  concept: Concept;
@@ -109,7 +112,7 @@ function handleContentClick(e: MouseEvent) {
109
112
 
110
113
  <!-- Designations -->
111
114
  <div v-if="designations.length > 0" class="card p-5">
112
- <div class="section-label">Designations</div>
115
+ <div class="section-label">{{ t('concept.designations') }}</div>
113
116
  <div class="space-y-2 mt-3">
114
117
  <div v-for="(d, i) in designations" :key="i" class="flex items-center gap-2 flex-wrap">
115
118
  <span class="font-medium text-ink-800 text-lg" v-html="renderMath(d.designation)"></span>
@@ -138,16 +141,16 @@ function handleContentClick(e: MouseEvent) {
138
141
 
139
142
  <!-- Definition -->
140
143
  <div v-if="definition" class="card p-5">
141
- <div class="section-label">Definition</div>
144
+ <div class="section-label">{{ t('concept.definition') }}</div>
142
145
  <div class="text-ink-800 leading-relaxed mt-3" v-html="renderMath(definition, renderOpts)"></div>
143
146
  </div>
144
147
 
145
148
  <!-- Notes -->
146
149
  <div v-if="notes.length" class="card p-5">
147
- <div class="section-label">Notes</div>
150
+ <div class="section-label">{{ t('concept.notes') }}</div>
148
151
  <div class="space-y-3 mt-3">
149
152
  <div v-for="(note, i) in notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
150
- <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">Note {{ i + 1 }}</span>
153
+ <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
151
154
  <div class="mt-1" v-html="renderMath(note, renderOpts)"></div>
152
155
  </div>
153
156
  </div>
@@ -155,10 +158,10 @@ function handleContentClick(e: MouseEvent) {
155
158
 
156
159
  <!-- Examples -->
157
160
  <div v-if="examples.length" class="card p-5">
158
- <div class="section-label">Examples</div>
161
+ <div class="section-label">{{ t('concept.examples') }}</div>
159
162
  <div class="space-y-3 mt-3">
160
163
  <div v-for="(ex, i) in examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
161
- <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">Example {{ i + 1 }}</span>
164
+ <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
162
165
  <div class="mt-1" v-html="renderMath(ex, renderOpts)"></div>
163
166
  </div>
164
167
  </div>
@@ -187,7 +190,7 @@ function handleContentClick(e: MouseEvent) {
187
190
  <span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langLabel(activeLang) }}</span>
188
191
  </div>
189
192
  <div>
190
- <p class="text-sm text-ink-600 font-medium">Term only in {{ langName(activeLang) }}</p>
193
+ <p class="text-sm text-ink-600 font-medium">{{ t('lang.termOnlyIn') }} {{ langName(activeLang) }}</p>
191
194
  <p class="text-xs text-ink-400 mt-1 leading-relaxed">
192
195
  This concept has a registered designation in {{ langName(activeLang) }} but no definition or notes.
193
196
  </p>
@@ -198,7 +201,7 @@ function handleContentClick(e: MouseEvent) {
198
201
 
199
202
  <!-- No data for this language -->
200
203
  <div v-else class="card p-5 text-center">
201
- <p class="text-sm text-ink-400">No data available for {{ langName(activeLang) }}.</p>
204
+ <p class="text-sm text-ink-400">{{ t('concept.noData') }}</p>
202
205
  </div>
203
206
  </div>
204
207
  </template>
@@ -6,11 +6,13 @@ import { ref, watch, onMounted, computed, nextTick } from 'vue';
6
6
  import type { SearchHit } from '../adapters/types';
7
7
  import { langLabel, langName } from '../utils/lang';
8
8
  import { useDsStyle } from '../utils/dataset-style';
9
+ import { useI18n } from '../i18n';
9
10
 
10
11
  const router = useRouter();
11
12
  const ui = useUiStore();
12
13
  const store = useVocabularyStore();
13
14
  const { getStyle } = useDsStyle();
15
+ const { t } = useI18n();
14
16
  const query = ref('');
15
17
  const results = ref<SearchHit[]>([]);
16
18
  const searched = ref(false);
@@ -154,7 +156,6 @@ onMounted(() => {
154
156
  @input="onInput"
155
157
  @keydown="onKeydown"
156
158
  type="text"
157
- aria-label="Search terms across all datasets"
158
159
  placeholder="Search terms across all datasets..."
159
160
  class="w-full pl-9 pr-8 py-2.5 text-sm bg-surface border border-ink-100 rounded-lg focus:ring-2 focus:ring-ink-200 focus:border-ink-400 outline-none placeholder:text-ink-300 transition-all"
160
161
  autofocus
@@ -175,7 +176,7 @@ onMounted(() => {
175
176
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
176
177
  </button>
177
178
  </div>
178
- <button type="submit" class="btn-primary" :disabled="loading">Search</button>
179
+ <button type="submit" class="btn-primary" :disabled="loading">{{ t('search.button') }}</button>
179
180
  </div>
180
181
  </form>
181
182
 
@@ -184,23 +185,23 @@ onMounted(() => {
184
185
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
185
186
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
186
187
  </svg>
187
- <p class="text-sm text-ink-400">Searching across datasets...</p>
188
+ <p class="text-sm text-ink-400">{{ t('search.searching') }}</p>
188
189
  </div>
189
190
 
190
191
  <div v-else-if="searchError" class="text-center py-16">
191
192
  <div class="card p-8 border-red-200 bg-red-50/50 max-w-md mx-auto">
192
- <p class="text-red-700 font-medium mb-1">Search failed</p>
193
+ <p class="text-red-700 font-medium mb-1">{{ t('search.failed') }}</p>
193
194
  <p class="text-sm text-red-600/80 mb-4">{{ searchError }}</p>
194
- <button @click="doSearch" class="btn-primary">Retry</button>
195
+ <button @click="doSearch" class="btn-primary">{{ t('search.retry') }}</button>
195
196
  </div>
196
197
  </div>
197
198
 
198
199
  <div v-else-if="searched">
199
- <p class="text-sm text-ink-400 mb-4">{{ results.length }} result{{ results.length === 1 ? '' : 's' }} for &ldquo;{{ ui.searchQuery }}&rdquo;</p>
200
+ <p class="text-sm text-ink-400 mb-4">{{ results.length === 1 ? t('search.oneResultFor', { query: ui.searchQuery }) : t('search.manyResultsFor', { count: String(results.length), query: ui.searchQuery }) }}</p>
200
201
  <div v-if="results.length === 0" class="text-center py-16">
201
202
  <div class="text-ink-200 text-5xl mb-4 font-serif">&empty;</div>
202
- <p class="text-ink-500 font-medium">No concepts found matching your search</p>
203
- <p class="text-sm text-ink-300 mt-1">Try a different term or check the spelling.</p>
203
+ <p class="text-ink-500 font-medium">{{ t('search.noResults') }}</p>
204
+ <p class="text-sm text-ink-300 mt-1">{{ t('search.tryDifferent') }}</p>
204
205
  </div>
205
206
 
206
207
  <!-- Grouped results -->
@@ -210,7 +211,7 @@ onMounted(() => {
210
211
  <div class="flex items-center gap-2 mb-2">
211
212
  <span class="w-2 h-2 rounded-full flex-shrink-0" :style="{ backgroundColor: group.style.color }"></span>
212
213
  <span class="text-xs font-semibold text-ink-500 uppercase tracking-wide">{{ group.title }}</span>
213
- <span class="text-xs text-ink-300">{{ group.hits.length }} result{{ group.hits.length === 1 ? '' : 's' }}</span>
214
+ <span class="text-xs text-ink-300">{{ group.hits.length }} {{ group.hits.length === 1 ? t('search.result') : t('search.results') }}</span>
214
215
  </div>
215
216
  <!-- Hits -->
216
217
  <div class="space-y-1.5">
@@ -227,7 +228,7 @@ onMounted(() => {
227
228
  <span v-if="hit.snippet" class="block text-xs text-ink-300 mt-0.5 truncate">{{ hit.snippet }}</span>
228
229
  </div>
229
230
  <div class="flex items-center gap-2 flex-shrink-0">
230
- <span v-if="hit.matchField === 'id'" class="badge badge-gray text-[10px]">ID match</span>
231
+ <span v-if="hit.matchField === 'id'" class="badge badge-gray text-[10px]">{{ t('search.idMatch') }}</span>
231
232
  <span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(hit.language) }}</span>
232
233
  </div>
233
234
  </button>
@@ -236,7 +237,7 @@ onMounted(() => {
236
237
  </div>
237
238
 
238
239
  <div v-if="results.length > 100" class="text-center text-sm text-ink-300 mt-6 pt-4 border-t border-ink-100/60">
239
- Showing first 100 of {{ results.length }} results. Refine your search for more specific matches.
240
+ {{ t('search.showingFirst', { max: '100', total: String(results.length) }) }}
240
241
  </div>
241
242
  </div>
242
243
  </div>
@@ -91,6 +91,7 @@ export interface DatasetConfig {
91
91
  tags?: string[];
92
92
  languageOrder?: string[];
93
93
  downloads?: string[];
94
+ translations?: Record<string, { title?: string; description?: string }>;
94
95
  }
95
96
 
96
97
  // === Contributors ===
@@ -141,6 +142,7 @@ export interface SiteConfig {
141
142
  title: string;
142
143
  subtitle?: string;
143
144
  description?: string;
145
+ translations?: Record<string, { title?: string; subtitle?: string; description?: string }>;
144
146
  datasets: DatasetConfig[];
145
147
  routing: RoutingEntry[];
146
148
  branding: SiteBranding;
@@ -1,5 +1,6 @@
1
1
  import { ref, computed } from 'vue';
2
2
  import type { PageConfig } from './types';
3
+ import { locale } from '../i18n';
3
4
 
4
5
  export interface RuntimeSiteConfig {
5
6
  id: string;
@@ -8,8 +9,11 @@ export interface RuntimeSiteConfig {
8
9
  title: string;
9
10
  subtitle?: string;
10
11
  description?: string;
12
+ translations?: Record<string, { title?: string; subtitle?: string; description?: string }>;
13
+ datasetTranslations?: Record<string, Record<string, { title?: string; description?: string }>>;
11
14
  datasets: string[];
12
15
  defaultDataset?: string;
16
+ uiLanguages?: { code: string; label: string }[];
13
17
  branding?: {
14
18
  primaryColor?: string;
15
19
  darkColor?: string;
@@ -150,6 +154,26 @@ export function useSiteConfig() {
150
154
  const config = computed(() => siteConfig.value);
151
155
  const visibleDatasets = computed(() => siteConfig.value?.datasets ?? []);
152
156
 
157
+ const localizedTitle = computed(() => {
158
+ const tr = siteConfig.value?.translations?.[locale.value];
159
+ return tr?.title ?? siteConfig.value?.title ?? 'Glossarist';
160
+ });
161
+
162
+ const localizedSubtitle = computed(() => {
163
+ const tr = siteConfig.value?.translations?.[locale.value];
164
+ return tr?.subtitle ?? siteConfig.value?.subtitle;
165
+ });
166
+
167
+ const localizedDescription = computed(() => {
168
+ const tr = siteConfig.value?.translations?.[locale.value];
169
+ return tr?.description ?? siteConfig.value?.description;
170
+ });
171
+
172
+ function localizedDatasetField(datasetId: string, field: 'title' | 'description', fallback?: string): string {
173
+ const tr = siteConfig.value?.datasetTranslations?.[datasetId]?.[locale.value];
174
+ return tr?.[field] ?? fallback ?? '';
175
+ }
176
+
153
177
  const globalPages = computed<PageConfig[]>(() =>
154
178
  synthesizeGlobalPages(siteConfig.value?.features, siteConfig.value?.pages),
155
179
  );
@@ -158,5 +182,5 @@ export function useSiteConfig() {
158
182
  synthesizeDatasetPages(siteConfig.value?.features, siteConfig.value?.pages),
159
183
  );
160
184
 
161
- return { config, visibleDatasets, loadConfig, globalPages, datasetPages };
185
+ return { config, visibleDatasets, localizedTitle, localizedSubtitle, localizedDescription, localizedDatasetField, loadConfig, globalPages, datasetPages };
162
186
  }
@@ -0,0 +1,51 @@
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 { locale };
24
+
25
+ export function useI18n() {
26
+ function t(key: string, params?: Record<string, string>): string {
27
+ let msg = messages[locale.value]?.[key] || messages[DEFAULT_LANG]?.[key] || key;
28
+ if (params) {
29
+ for (const [k, v] of Object.entries(params)) {
30
+ msg = msg.replace(`{${k}}`, v);
31
+ }
32
+ }
33
+ return msg;
34
+ }
35
+
36
+ function setLocale(lang: string) {
37
+ locale.value = lang;
38
+ localStorage.setItem('ui-lang', lang);
39
+ }
40
+
41
+ function initLocale(configuredDefault?: string) {
42
+ const storedLang = localStorage.getItem('ui-lang');
43
+ if (storedLang && availableLocales.includes(storedLang)) {
44
+ locale.value = storedLang;
45
+ } else if (configuredDefault && availableLocales.includes(configuredDefault)) {
46
+ locale.value = configuredDefault;
47
+ }
48
+ }
49
+
50
+ return { locale, t, setLocale, initLocale, availableLocales };
51
+ }
@@ -0,0 +1,128 @@
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.stats: 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
+ search.fullPlaceholder: Search terms across all datasets...
31
+ search.button: Search
32
+ search.searching: Searching across datasets...
33
+ search.failed: Search failed
34
+ search.retry: Retry
35
+ search.result: result
36
+ search.results: results
37
+ search.oneResultFor: '1 result for "{query}"'
38
+ search.manyResultsFor: '{count} results for "{query}"'
39
+ search.noResults: No concepts found matching your search
40
+ search.tryDifferent: Try a different term or check the spelling.
41
+ search.showingFirst: Showing first {max} of {total} results. Refine your search for more specific matches.
42
+ search.idMatch: ID match
43
+
44
+ # Page view
45
+ page.notFound: Page Not Found
46
+ page.notFoundMsg: 'The page "{name}" does not exist.'
47
+ page.goHome: Go Home
48
+
49
+ # Concept detail
50
+ concept.definition: Definition
51
+ concept.notes: Notes
52
+ concept.note: Note
53
+ concept.examples: Examples
54
+ concept.example: Example
55
+ concept.sources: Sources
56
+ concept.designations: Designations
57
+ concept.relations: Relations
58
+ concept.classification: Classification
59
+ concept.domains: Domains
60
+ concept.tags: Tags
61
+ concept.metadata: Metadata
62
+ concept.noData: No data available for this language.
63
+ concept.lifecycleDates: Lifecycle dates
64
+ concept.conceptSources: Concept sources
65
+ concept.incoming: Incoming
66
+ concept.outgoing: Outgoing
67
+
68
+ # Sidebar
69
+ sidebar.overview: Overview
70
+ sidebar.dataset: Dataset
71
+
72
+ # Footer
73
+ footer.builtWith: Built with the Glossarist Concept Browser
74
+
75
+ # Header
76
+ header.datasets: datasets
77
+
78
+ # Language
79
+ lang.termOnlyIn: Term only in
80
+
81
+ # Dataset view
82
+ dataset.concepts: concepts
83
+ dataset.languages: languages
84
+ dataset.filterPlaceholder: "Filter concepts... (press /)"
85
+ dataset.of: of
86
+ dataset.in: in
87
+ dataset.all: All
88
+ dataset.prev: Prev
89
+ dataset.next: Next
90
+ dataset.noMatch: No concepts match your filter
91
+ dataset.clearFilter: Clear filter
92
+ dataset.noConcepts: This dataset has no concepts
93
+ dataset.failedToLoad: Failed to load dataset
94
+ dataset.retry: Retry
95
+ dataset.backToHome: Back to home
96
+ dataset.download: Download
97
+
98
+ # Statistics view
99
+ stats.title: Statistics
100
+ stats.summary: "{count} concepts across {langCount} languages."
101
+ stats.language: Language
102
+ stats.terms: Terms
103
+ stats.definitions: Definitions
104
+ stats.failedToLoad: Failed to load statistics
105
+ stats.retry: Retry
106
+
107
+ # Graph
108
+ graph.nodes: nodes
109
+ graph.edges: edges
110
+ graph.all: All
111
+ graph.none: None
112
+ graph.resetZoom: Reset zoom
113
+ graph.reLayout: Re-layout
114
+ graph.nodeLabels: Node labels
115
+ graph.designation: Designation
116
+ graph.identifier: Identifier
117
+ graph.viewConcept: View concept
118
+ graph.stubLabel: Stub (not loaded)
119
+ graph.domainLabel: Domain (standard)
120
+ graph.datasets: Datasets
121
+ graph.enableDatasets: Enable datasets to see their graph.
122
+ graph.browseToPopulate: Browse concepts with cross-references to populate the graph.
123
+ graph.renderingWarning: "Rendering first {max} of {total} nodes."
124
+ graph.loadedStatus: loaded
125
+ graph.stubStatus: stub
126
+ graph.collapseControls: Collapse controls
127
+ graph.expandControls: Expand controls
128
+ graph.loading: Loading graph data...
@@ -0,0 +1,128 @@
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.stats: 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
+ search.fullPlaceholder: Rechercher des termes dans tous les jeux de données…
31
+ search.button: Rechercher
32
+ search.searching: Recherche dans les jeux de données...
33
+ search.failed: Échec de la recherche
34
+ search.retry: Réessayer
35
+ search.result: résultat
36
+ search.results: résultats
37
+ search.oneResultFor: '1 résultat pour « {query} »'
38
+ search.manyResultsFor: '{count} résultats pour « {query} »'
39
+ search.noResults: Aucun concept trouvé correspondant à votre recherche
40
+ search.tryDifferent: Essayez un autre terme ou vérifiez l'orthographe.
41
+ search.showingFirst: Affichage des {max} premiers résultats sur {total}. Affinez votre recherche.
42
+ search.idMatch: Correspondance d'identifiant
43
+
44
+ # Page view
45
+ page.notFound: Page non trouvée
46
+ page.notFoundMsg: 'La page « {name} » n''existe pas.'
47
+ page.goHome: Aller à l''accueil
48
+
49
+ # Concept detail
50
+ concept.definition: Définition
51
+ concept.notes: Notes
52
+ concept.note: Note
53
+ concept.examples: Exemples
54
+ concept.example: Exemple
55
+ concept.sources: Sources
56
+ concept.designations: Désignations
57
+ concept.relations: Relations
58
+ concept.classification: Classification
59
+ concept.domains: Domaines
60
+ concept.tags: Étiquettes
61
+ concept.metadata: Métadonnées
62
+ concept.noData: Aucune donnée disponible pour cette langue.
63
+ concept.lifecycleDates: Dates du cycle de vie
64
+ concept.conceptSources: Sources du concept
65
+ concept.incoming: Entrantes
66
+ concept.outgoing: Sortantes
67
+
68
+ # Sidebar
69
+ sidebar.overview: Vue d'ensemble
70
+ sidebar.dataset: Jeu de données
71
+
72
+ # Footer
73
+ footer.builtWith: Construit avec le Glossarist Concept Browser
74
+
75
+ # Header
76
+ header.datasets: jeux de données
77
+
78
+ # Language
79
+ lang.termOnlyIn: Terme uniquement en
80
+
81
+ # Dataset view
82
+ dataset.concepts: concepts
83
+ dataset.languages: langues
84
+ dataset.filterPlaceholder: "Filtrer les concepts… (appuyez sur /)"
85
+ dataset.of: de
86
+ dataset.in: en
87
+ dataset.all: Tous
88
+ dataset.prev: Préc.
89
+ dataset.next: Suiv.
90
+ dataset.noMatch: Aucun concept ne correspond au filtre
91
+ dataset.clearFilter: Effacer le filtre
92
+ dataset.noConcepts: Ce jeu de données n'a aucun concept
93
+ dataset.failedToLoad: Échec du chargement du jeu de données
94
+ dataset.retry: Réessayer
95
+ dataset.backToHome: Retour à l'accueil
96
+ dataset.download: Télécharger
97
+
98
+ # Statistics view
99
+ stats.title: Statistiques
100
+ stats.summary: "{count} concepts dans {langCount} langues."
101
+ stats.language: Langue
102
+ stats.terms: Termes
103
+ stats.definitions: Définitions
104
+ stats.failedToLoad: Échec du chargement des statistiques
105
+ stats.retry: Réessayer
106
+
107
+ # Graph
108
+ graph.nodes: nœuds
109
+ graph.edges: arêtes
110
+ graph.all: Tous
111
+ graph.none: Aucun
112
+ graph.resetZoom: Réinitialiser le zoom
113
+ graph.reLayout: Réorganiser
114
+ graph.nodeLabels: Étiquettes des nœuds
115
+ graph.designation: Désignation
116
+ graph.identifier: Identifiant
117
+ graph.viewConcept: Voir le concept
118
+ graph.stubLabel: Non chargé
119
+ graph.domainLabel: Domaine (standard)
120
+ graph.datasets: Jeux de données
121
+ graph.enableDatasets: Activez les jeux de données pour voir leur graphe.
122
+ graph.browseToPopulate: Parcourez les concepts avec des renvois croisés pour remplir le graphe.
123
+ graph.renderingWarning: "Affichage des {max} premiers nœuds sur {total}."
124
+ graph.loadedStatus: chargé
125
+ graph.stubStatus: non chargé
126
+ graph.collapseControls: Réduire les contrôles
127
+ graph.expandControls: Développer les contrôles
128
+ graph.loading: Chargement des données du graphe...
@@ -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/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 {
@@ -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++;
@@ -4,10 +4,12 @@ import { useVocabularyStore } from '../stores/vocabulary';
4
4
  import { useDsStyle } from '../utils/dataset-style';
5
5
  import { useDatasetLoader } from '../composables/use-dataset-loader';
6
6
  import { langName, langLabel } from '../utils/lang';
7
+ import { useI18n } from '../i18n';
7
8
 
8
9
  const props = defineProps<{ registerId?: string }>();
9
10
 
10
11
  const store = useVocabularyStore();
12
+ const { t } = useI18n();
11
13
  const { getColor } = useDsStyle();
12
14
  const { loading, localError, ensureLoaded, resolvedId } = useDatasetLoader(() => props.registerId);
13
15
 
@@ -18,7 +20,7 @@ const manifest = computed(() => store.manifests.get(resolvedId.value));
18
20
  <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
19
21
  <!-- Breadcrumb -->
20
22
  <nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
21
- <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
23
+ <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
22
24
  <span class="text-ink-200">/</span>
23
25
  <router-link :to="{ name: 'dataset', params: { registerId: resolvedId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title || resolvedId }}</router-link>
24
26
  <span class="text-ink-200">/</span>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useSiteConfig } from '../config/use-site-config';
3
+ import { useI18n } from '../i18n';
3
4
 
4
5
  interface Contributor {
5
6
  name: string;
@@ -9,6 +10,9 @@ interface Contributor {
9
10
  email?: string;
10
11
  }
11
12
 
13
+ const { config: siteConfig } = useSiteConfig();
14
+ const { t } = useI18n();
15
+
12
16
  const { config } = useSiteConfig();
13
17
  const contributors = config.value?.contributors as Contributor[] | undefined;
14
18
  </script>
@@ -16,7 +20,7 @@ const contributors = config.value?.contributors as Contributor[] | undefined;
16
20
  <template>
17
21
  <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
18
22
  <nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
19
- <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
23
+ <router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
20
24
  <span class="text-ink-200">/</span>
21
25
  <span class="text-ink-700">Contributors</span>
22
26
  </nav>