@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.
@@ -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
- <UserMenu class="mt-auto" />
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>
@@ -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
+ )
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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.0",
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",