@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.
- package/cli/index.mjs +17 -0
- package/package.json +1 -1
- package/scripts/generate-data.mjs +49 -0
- package/src/App.vue +3 -0
- package/src/__tests__/markdown-lite.test.ts +26 -0
- package/src/components/AppFooter.vue +3 -1
- package/src/components/AppHeader.vue +48 -6
- package/src/components/AppSidebar.vue +17 -8
- package/src/components/ConceptCard.vue +5 -2
- package/src/components/ConceptDetail.vue +14 -11
- package/src/components/GraphPanel.vue +18 -16
- package/src/components/LanguageDetail.vue +11 -8
- package/src/components/SearchBar.vue +12 -11
- package/src/config/types.ts +2 -0
- package/src/config/use-site-config.ts +25 -1
- package/src/i18n/index.ts +51 -0
- package/src/i18n/locales/eng.yml +128 -0
- package/src/i18n/locales/fra.yml +128 -0
- package/src/shims/glossarist-tags.ts +10 -0
- package/src/style.css +12 -0
- package/src/utils/markdown-lite.ts +15 -0
- package/src/views/AboutView.vue +3 -1
- package/src/views/ContributorsView.vue +5 -1
- package/src/views/DatasetView.vue +22 -20
- package/src/views/GraphView.vue +6 -4
- package/src/views/HomeView.vue +23 -19
- package/src/views/NewsView.vue +3 -1
- package/src/views/PageView.vue +26 -11
- package/src/views/SearchView.vue +4 -2
- package/src/views/StatsView.vue +11 -9
- package/vite.config.ts +34 -1
|
@@ -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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
179
|
+
<button type="submit" class="btn-primary" :disabled="loading">{{ t('search.button') }}</button>
|
|
179
180
|
</div>
|
|
180
181
|
</form>
|
|
181
182
|
|
|
@@ -184,23 +185,23 @@ onMounted(() => {
|
|
|
184
185
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
185
186
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
186
187
|
</svg>
|
|
187
|
-
<p class="text-sm text-ink-400">
|
|
188
|
+
<p class="text-sm text-ink-400">{{ t('search.searching') }}</p>
|
|
188
189
|
</div>
|
|
189
190
|
|
|
190
191
|
<div v-else-if="searchError" class="text-center py-16">
|
|
191
192
|
<div class="card p-8 border-red-200 bg-red-50/50 max-w-md mx-auto">
|
|
192
|
-
<p class="text-red-700 font-medium mb-1">
|
|
193
|
+
<p class="text-red-700 font-medium mb-1">{{ t('search.failed') }}</p>
|
|
193
194
|
<p class="text-sm text-red-600/80 mb-4">{{ searchError }}</p>
|
|
194
|
-
<button @click="doSearch" class="btn-primary">
|
|
195
|
+
<button @click="doSearch" class="btn-primary">{{ t('search.retry') }}</button>
|
|
195
196
|
</div>
|
|
196
197
|
</div>
|
|
197
198
|
|
|
198
199
|
<div v-else-if="searched">
|
|
199
|
-
<p class="text-sm text-ink-400 mb-4">{{ results.length
|
|
200
|
+
<p class="text-sm text-ink-400 mb-4">{{ results.length === 1 ? t('search.oneResultFor', { query: ui.searchQuery }) : t('search.manyResultsFor', { count: String(results.length), query: ui.searchQuery }) }}</p>
|
|
200
201
|
<div v-if="results.length === 0" class="text-center py-16">
|
|
201
202
|
<div class="text-ink-200 text-5xl mb-4 font-serif">∅</div>
|
|
202
|
-
<p class="text-ink-500 font-medium">
|
|
203
|
-
<p class="text-sm text-ink-300 mt-1">
|
|
203
|
+
<p class="text-ink-500 font-medium">{{ t('search.noResults') }}</p>
|
|
204
|
+
<p class="text-sm text-ink-300 mt-1">{{ t('search.tryDifferent') }}</p>
|
|
204
205
|
</div>
|
|
205
206
|
|
|
206
207
|
<!-- Grouped results -->
|
|
@@ -210,7 +211,7 @@ onMounted(() => {
|
|
|
210
211
|
<div class="flex items-center gap-2 mb-2">
|
|
211
212
|
<span class="w-2 h-2 rounded-full flex-shrink-0" :style="{ backgroundColor: group.style.color }"></span>
|
|
212
213
|
<span class="text-xs font-semibold text-ink-500 uppercase tracking-wide">{{ group.title }}</span>
|
|
213
|
-
<span class="text-xs text-ink-300">{{ group.hits.length }}
|
|
214
|
+
<span class="text-xs text-ink-300">{{ group.hits.length }} {{ group.hits.length === 1 ? t('search.result') : t('search.results') }}</span>
|
|
214
215
|
</div>
|
|
215
216
|
<!-- Hits -->
|
|
216
217
|
<div class="space-y-1.5">
|
|
@@ -227,7 +228,7 @@ onMounted(() => {
|
|
|
227
228
|
<span v-if="hit.snippet" class="block text-xs text-ink-300 mt-0.5 truncate">{{ hit.snippet }}</span>
|
|
228
229
|
</div>
|
|
229
230
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
230
|
-
<span v-if="hit.matchField === 'id'" class="badge badge-gray text-[10px]">
|
|
231
|
+
<span v-if="hit.matchField === 'id'" class="badge badge-gray text-[10px]">{{ t('search.idMatch') }}</span>
|
|
231
232
|
<span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langName(hit.language) }}</span>
|
|
232
233
|
</div>
|
|
233
234
|
</button>
|
|
@@ -236,7 +237,7 @@ onMounted(() => {
|
|
|
236
237
|
</div>
|
|
237
238
|
|
|
238
239
|
<div v-if="results.length > 100" class="text-center text-sm text-ink-300 mt-6 pt-4 border-t border-ink-100/60">
|
|
239
|
-
|
|
240
|
+
{{ t('search.showingFirst', { max: '100', total: String(results.length) }) }}
|
|
240
241
|
</div>
|
|
241
242
|
</div>
|
|
242
243
|
</div>
|
package/src/config/types.ts
CHANGED
|
@@ -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++;
|
package/src/views/AboutView.vue
CHANGED
|
@@ -4,10 +4,12 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
4
4
|
import { useDsStyle } from '../utils/dataset-style';
|
|
5
5
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
6
6
|
import { langName, langLabel } from '../utils/lang';
|
|
7
|
+
import { useI18n } from '../i18n';
|
|
7
8
|
|
|
8
9
|
const props = defineProps<{ registerId?: string }>();
|
|
9
10
|
|
|
10
11
|
const store = useVocabularyStore();
|
|
12
|
+
const { t } = useI18n();
|
|
11
13
|
const { getColor } = useDsStyle();
|
|
12
14
|
const { loading, localError, ensureLoaded, resolvedId } = useDatasetLoader(() => props.registerId);
|
|
13
15
|
|
|
@@ -18,7 +20,7 @@ const manifest = computed(() => store.manifests.get(resolvedId.value));
|
|
|
18
20
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
19
21
|
<!-- Breadcrumb -->
|
|
20
22
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
21
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
23
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
22
24
|
<span class="text-ink-200">/</span>
|
|
23
25
|
<router-link :to="{ name: 'dataset', params: { registerId: resolvedId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title || resolvedId }}</router-link>
|
|
24
26
|
<span class="text-ink-200">/</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useSiteConfig } from '../config/use-site-config';
|
|
3
|
+
import { useI18n } from '../i18n';
|
|
3
4
|
|
|
4
5
|
interface Contributor {
|
|
5
6
|
name: string;
|
|
@@ -9,6 +10,9 @@ interface Contributor {
|
|
|
9
10
|
email?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
const { config: siteConfig } = useSiteConfig();
|
|
14
|
+
const { t } = useI18n();
|
|
15
|
+
|
|
12
16
|
const { config } = useSiteConfig();
|
|
13
17
|
const contributors = config.value?.contributors as Contributor[] | undefined;
|
|
14
18
|
</script>
|
|
@@ -16,7 +20,7 @@ const contributors = config.value?.contributors as Contributor[] | undefined;
|
|
|
16
20
|
<template>
|
|
17
21
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
18
22
|
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
19
|
-
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">
|
|
23
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">{{ t('nav.home') }}</router-link>
|
|
20
24
|
<span class="text-ink-200">/</span>
|
|
21
25
|
<span class="text-ink-700">Contributors</span>
|
|
22
26
|
</nav>
|