@cat-factory/app 0.46.0 → 0.46.1
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/app/components/layout/LanguageSwitcher.vue +54 -0
- package/app/components/layout/SideBar.vue +5 -1
- package/app/components/layout/TranslationWarningBanner.vue +69 -0
- package/app/pages/index.vue +2 -0
- package/app/plugins/locale.client.ts +20 -0
- package/app/stores/locale.ts +20 -0
- package/i18n/locales/en.json +9 -0
- package/i18n/locales/es.json +9 -0
- package/i18n/locales/fr.json +9 -0
- package/i18n/locales/pl.json +9 -0
- package/i18n/locales/uk.json +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { useLocaleStore } from '~/stores/locale'
|
|
5
|
+
|
|
6
|
+
// Language picker for the SPA's supported locales, shown at the sidebar bottom next to
|
|
7
|
+
// the user menu. The list is data-driven from the i18n config (`useI18n().locales`), so
|
|
8
|
+
// adding a locale in nuxt.config.ts surfaces it here automatically. Selecting one switches
|
|
9
|
+
// the live locale AND persists the choice (the locale store) so it survives a reload.
|
|
10
|
+
const { t, locale, locales, setLocale } = useI18n()
|
|
11
|
+
const localeStore = useLocaleStore()
|
|
12
|
+
|
|
13
|
+
const current = computed(
|
|
14
|
+
() => locales.value.find((l) => l.code === locale.value)?.name ?? locale.value,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// The typed-messages guard narrows the locale to the configured union, so take the same
|
|
18
|
+
// type setLocale expects rather than a bare string.
|
|
19
|
+
async function choose(code: typeof locale.value) {
|
|
20
|
+
if (code === locale.value) return
|
|
21
|
+
await setLocale(code)
|
|
22
|
+
localeStore.set(code)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const items = computed<DropdownMenuItem[][]>(() => [
|
|
26
|
+
locales.value.map((l) => ({
|
|
27
|
+
label: l.name ?? l.code,
|
|
28
|
+
icon: l.code === locale.value ? 'i-lucide-check' : undefined,
|
|
29
|
+
onSelect: () => {
|
|
30
|
+
void choose(l.code)
|
|
31
|
+
},
|
|
32
|
+
})),
|
|
33
|
+
])
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<UDropdownMenu :items="items" :content="{ side: 'top', align: 'start' }">
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
data-testid="language-switcher"
|
|
41
|
+
:aria-label="t('language.switcher')"
|
|
42
|
+
class="flex w-full items-center gap-2 rounded-lg border border-slate-800 bg-slate-900/60 p-2 text-left transition hover:bg-slate-800/60"
|
|
43
|
+
>
|
|
44
|
+
<UIcon name="i-lucide-languages" class="h-4 w-4 shrink-0 text-slate-400" />
|
|
45
|
+
<div class="min-w-0 flex-1">
|
|
46
|
+
<div class="truncate text-[10px] uppercase tracking-wide text-slate-500">
|
|
47
|
+
{{ t('language.switcher') }}
|
|
48
|
+
</div>
|
|
49
|
+
<div class="truncate text-xs font-medium text-white">{{ current }}</div>
|
|
50
|
+
</div>
|
|
51
|
+
<UIcon name="i-lucide-chevron-up" class="h-4 w-4 shrink-0 text-slate-500" />
|
|
52
|
+
</button>
|
|
53
|
+
</UDropdownMenu>
|
|
54
|
+
</template>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// default models).
|
|
8
8
|
import { useEventListener, useScrollLock } from '@vueuse/core'
|
|
9
9
|
import BoardSwitcher from '~/components/layout/BoardSwitcher.vue'
|
|
10
|
+
import LanguageSwitcher from '~/components/layout/LanguageSwitcher.vue'
|
|
10
11
|
import UserMenu from '~/components/auth/UserMenu.vue'
|
|
11
12
|
import { useViewport } from '~/composables/useViewport'
|
|
12
13
|
|
|
@@ -306,6 +307,9 @@ watch(
|
|
|
306
307
|
</section>
|
|
307
308
|
</div>
|
|
308
309
|
|
|
309
|
-
<
|
|
310
|
+
<div class="mt-auto space-y-2">
|
|
311
|
+
<LanguageSwitcher />
|
|
312
|
+
<UserMenu />
|
|
313
|
+
</div>
|
|
310
314
|
</aside>
|
|
311
315
|
</template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
// Shown whenever the active locale is NOT English: the non-English catalogs are
|
|
5
|
+
// community/AI-provided and may be inaccurate, so warn the user and point them at the
|
|
6
|
+
// repository to report mistakes or open a fix PR. Rendered as a slim full-width strip at
|
|
7
|
+
// the very top (distinct from the centered config-warning cards below it, so they don't
|
|
8
|
+
// overlap). Dismissible per session; re-appears the next time the user switches locale.
|
|
9
|
+
const REPO_URL = 'https://github.com/kibertoad/cat-factory'
|
|
10
|
+
|
|
11
|
+
const { t, locale } = useI18n()
|
|
12
|
+
|
|
13
|
+
const dismissed = ref(false)
|
|
14
|
+
const show = computed(() => locale.value !== 'en' && !dismissed.value)
|
|
15
|
+
|
|
16
|
+
// A fresh switch is a new context for the warning, so un-dismiss on every locale change.
|
|
17
|
+
watch(locale, () => {
|
|
18
|
+
dismissed.value = false
|
|
19
|
+
})
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<Transition name="fade">
|
|
24
|
+
<div
|
|
25
|
+
v-if="show"
|
|
26
|
+
data-testid="translation-warning"
|
|
27
|
+
role="alert"
|
|
28
|
+
class="fixed inset-x-0 top-0 z-50 flex items-center gap-3 border-b border-amber-500/40 bg-amber-950/95 px-4 py-2 text-[13px] text-amber-100 shadow-lg backdrop-blur"
|
|
29
|
+
>
|
|
30
|
+
<UIcon name="i-lucide-languages" class="h-4 w-4 shrink-0 text-amber-400" />
|
|
31
|
+
<p class="min-w-0 flex-1">
|
|
32
|
+
<span class="font-semibold">{{ t('language.warning.title') }}</span>
|
|
33
|
+
<span class="mx-1.5 text-amber-400/60">·</span>
|
|
34
|
+
<i18n-t keypath="language.warning.body" tag="span" scope="global">
|
|
35
|
+
<template #repoLink>
|
|
36
|
+
<a
|
|
37
|
+
:href="REPO_URL"
|
|
38
|
+
target="_blank"
|
|
39
|
+
rel="noopener noreferrer"
|
|
40
|
+
class="inline-flex items-center gap-1 font-medium text-sky-300 hover:underline"
|
|
41
|
+
>
|
|
42
|
+
{{ t('language.warning.repoLinkLabel') }}
|
|
43
|
+
<UIcon name="i-lucide-external-link" class="h-3 w-3" />
|
|
44
|
+
</a>
|
|
45
|
+
</template>
|
|
46
|
+
</i18n-t>
|
|
47
|
+
</p>
|
|
48
|
+
<UButton
|
|
49
|
+
color="neutral"
|
|
50
|
+
variant="ghost"
|
|
51
|
+
size="xs"
|
|
52
|
+
icon="i-lucide-x"
|
|
53
|
+
:aria-label="t('language.warning.dismiss')"
|
|
54
|
+
@click="dismissed = true"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</Transition>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<style scoped>
|
|
61
|
+
.fade-enter-active,
|
|
62
|
+
.fade-leave-active {
|
|
63
|
+
transition: opacity 0.2s ease;
|
|
64
|
+
}
|
|
65
|
+
.fade-enter-from,
|
|
66
|
+
.fade-leave-to {
|
|
67
|
+
opacity: 0;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
package/app/pages/index.vue
CHANGED
|
@@ -208,6 +208,8 @@ watch(
|
|
|
208
208
|
|
|
209
209
|
<template>
|
|
210
210
|
<div class="flex h-screen w-screen overflow-hidden bg-slate-950 text-slate-100">
|
|
211
|
+
<!-- Non-English locale warning (unofficial translation); slim strip above everything. -->
|
|
212
|
+
<TranslationWarningBanner />
|
|
211
213
|
<!-- Local-mode setup prompt (missing GitHub PAT); floats over whatever is shown below. -->
|
|
212
214
|
<GitHubPatBanner />
|
|
213
215
|
<!-- AI-readiness prompt (no usable model source, or default preset uses unavailable models). -->
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useLocaleStore } from '~/stores/locale'
|
|
2
|
+
|
|
3
|
+
// Restore the user's persisted language choice on boot. The app defaults to English
|
|
4
|
+
// (the locale store's default); this only re-applies an EXPLICIT prior pick. Kept
|
|
5
|
+
// client-only (the SPA renders client-side) and guarded against an unknown code so a
|
|
6
|
+
// stale/removed locale falls back to the i18n default instead of throwing.
|
|
7
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
8
|
+
const i18n = nuxtApp.$i18n as {
|
|
9
|
+
locale: { value: string }
|
|
10
|
+
locales: { value: Array<{ code: string }> }
|
|
11
|
+
setLocale: (code: string) => Promise<void>
|
|
12
|
+
}
|
|
13
|
+
if (!i18n?.setLocale) return
|
|
14
|
+
|
|
15
|
+
const stored = useLocaleStore().current
|
|
16
|
+
const supported = i18n.locales.value.some((l) => l.code === stored)
|
|
17
|
+
if (supported && stored !== i18n.locale.value) {
|
|
18
|
+
await i18n.setLocale(stored)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
// Persists the user's EXPLICIT language choice so it survives reloads. The app always
|
|
5
|
+
// boots in English (the `current` default) — there is no browser auto-detect — and only
|
|
6
|
+
// an explicit pick via the switcher changes this. A client plugin applies `current` to
|
|
7
|
+
// the active i18n locale on startup; the switcher writes here AND calls i18n's setLocale.
|
|
8
|
+
export const useLocaleStore = defineStore(
|
|
9
|
+
'locale',
|
|
10
|
+
() => {
|
|
11
|
+
const current = ref('en')
|
|
12
|
+
|
|
13
|
+
function set(code: string) {
|
|
14
|
+
current.value = code
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { current, set }
|
|
18
|
+
},
|
|
19
|
+
{ persist: { pick: ['current'] } },
|
|
20
|
+
)
|
package/i18n/locales/en.json
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
"language": {
|
|
3
|
+
"switcher": "Language",
|
|
4
|
+
"warning": {
|
|
5
|
+
"title": "Unofficial translation",
|
|
6
|
+
"body": "This translation is community-provided and may be inaccurate. Spotted a mistake? Please report it or open a fix on {repoLink}.",
|
|
7
|
+
"repoLinkLabel": "the cat-factory repository",
|
|
8
|
+
"dismiss": "Dismiss"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
2
11
|
"common": {
|
|
3
12
|
"save": "Save",
|
|
4
13
|
"cancel": "Cancel",
|
package/i18n/locales/es.json
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
"language": {
|
|
3
|
+
"switcher": "Idioma",
|
|
4
|
+
"warning": {
|
|
5
|
+
"title": "Traducción no oficial",
|
|
6
|
+
"body": "Esta traducción es proporcionada por la comunidad y puede contener errores. ¿Has encontrado un error? Repórtalo o envía una corrección en {repoLink}.",
|
|
7
|
+
"repoLinkLabel": "el repositorio de cat-factory",
|
|
8
|
+
"dismiss": "Descartar"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
2
11
|
"common": {
|
|
3
12
|
"save": "Guardar",
|
|
4
13
|
"cancel": "Cancelar",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
"language": {
|
|
3
|
+
"switcher": "Langue",
|
|
4
|
+
"warning": {
|
|
5
|
+
"title": "Traduction non officielle",
|
|
6
|
+
"body": "Cette traduction est fournie par la communauté et peut être inexacte. Vous avez repéré une erreur ? Signalez-la ou proposez une correction sur {repoLink}.",
|
|
7
|
+
"repoLinkLabel": "le dépôt cat-factory",
|
|
8
|
+
"dismiss": "Fermer"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
2
11
|
"common": {
|
|
3
12
|
"save": "Enregistrer",
|
|
4
13
|
"cancel": "Annuler",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
"language": {
|
|
3
|
+
"switcher": "Język",
|
|
4
|
+
"warning": {
|
|
5
|
+
"title": "Nieoficjalne tłumaczenie",
|
|
6
|
+
"body": "To tłumaczenie pochodzi od społeczności i może być niedokładne. Zauważyłeś błąd? Zgłoś go lub prześlij poprawkę w {repoLink}.",
|
|
7
|
+
"repoLinkLabel": "repozytorium cat-factory",
|
|
8
|
+
"dismiss": "Odrzuć"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
2
11
|
"common": {
|
|
3
12
|
"save": "Zapisz",
|
|
4
13
|
"cancel": "Anuluj",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
"language": {
|
|
3
|
+
"switcher": "Мова",
|
|
4
|
+
"warning": {
|
|
5
|
+
"title": "Неофіційний переклад",
|
|
6
|
+
"body": "Цей переклад надано спільнотою та може бути неточним. Помітили помилку? Повідомте про неї або надішліть виправлення у {repoLink}.",
|
|
7
|
+
"repoLinkLabel": "репозиторії cat-factory",
|
|
8
|
+
"dismiss": "Закрити"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
2
11
|
"common": {
|
|
3
12
|
"save": "Зберегти",
|
|
4
13
|
"cancel": "Скасувати",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|