fix: escape @ in i18n messages, set useScope:'global' everywhere

Vue-i18n treats a bare '@' as the start of a linked-message reference,
which made every placeholder containing one — you{'@'}example.com,
the Telegram '{'@'}yourname' hint, the bad-Telegram error — crash the
compiler with "Invalid linked format". Escaping each '@' as the
literal {'@'} in en/es/fr fixes the compile and renders as a plain
'@' to the visitor.

Separately, every useI18n() call now passes { useScope: 'global' }.
Without it, components mounted inside <Form> / <Field> contexts
couldn't find a parent i18n scope and vue-i18n logged "Not found
parent scope. use the global scope." on every render. Explicit
global scope silences the warning and matches what the app
actually intends — there are no per-component message bundles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-27 11:39:19 +02:00
commit c65ee029dd
14 changed files with 23 additions and 23 deletions

View file

@ -25,7 +25,7 @@ import {
} from '@/components/ui/select' } from '@/components/ui/select'
import { submitInquiry, type ContactMethod } from '@/features/nostr/submitInquiry' import { submitInquiry, type ContactMethod } from '@/features/nostr/submitInquiry'
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
const METHODS: ContactMethod[] = ['email', 'whatsapp', 'signal', 'telegram', 'nostr'] const METHODS: ContactMethod[] = ['email', 'whatsapp', 'signal', 'telegram', 'nostr']

View file

@ -2,7 +2,7 @@
import { Lock } from '@lucide/vue' import { Lock } from '@lucide/vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
</script> </script>
<template> <template>

View file

@ -8,7 +8,7 @@ import {
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { SUPPORTED_LOCALES, persistLocale, type LocaleCode } from '@/i18n' import { SUPPORTED_LOCALES, persistLocale, type LocaleCode } from '@/i18n'
const { locale } = useI18n() const { locale } = useI18n({ useScope: 'global' })
function set(code: LocaleCode) { function set(code: LocaleCode) {
locale.value = code locale.value = code

View file

@ -2,7 +2,7 @@
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
const year = new Date().getFullYear() const year = new Date().getFullYear()
</script> </script>

View file

@ -13,7 +13,7 @@ import {
import ThemeToggle from './ThemeToggle.vue' import ThemeToggle from './ThemeToggle.vue'
import LocaleSwitcher from './LocaleSwitcher.vue' import LocaleSwitcher from './LocaleSwitcher.vue'
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
const navItems = computed(() => [ const navItems = computed(() => [
{ to: '/portfolio', label: t('nav.portfolio') }, { to: '/portfolio', label: t('nav.portfolio') },

View file

@ -3,7 +3,7 @@ import { useI18n } from 'vue-i18n'
import { Moon, Sun } from '@lucide/vue' import { Moon, Sun } from '@lucide/vue'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
const { theme, toggle } = useTheme() const { theme, toggle } = useTheme()
</script> </script>

View file

@ -7,7 +7,7 @@ import ProjectImage from './ProjectImage.vue'
import type { Project } from '@/data/projects' import type { Project } from '@/data/projects'
const props = defineProps<{ project: Project }>() const props = defineProps<{ project: Project }>()
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
const hero = computed(() => props.project.images.find((img) => img.feature === 'hero')) const hero = computed(() => props.project.images.find((img) => img.feature === 'hero'))
const rest = computed(() => props.project.images.filter((img) => img.feature !== 'hero')) const rest = computed(() => props.project.images.filter((img) => img.feature !== 'hero'))

View file

@ -5,7 +5,7 @@ import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
import type { ProjectImage } from '@/data/projects' import type { ProjectImage } from '@/data/projects'
const props = defineProps<{ image: ProjectImage }>() const props = defineProps<{ image: ProjectImage }>()
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
const open = ref(false) const open = ref(false)
</script> </script>

View file

@ -104,7 +104,7 @@
"messageTooLong": "Keep it under 2000 characters.", "messageTooLong": "Keep it under 2000 characters.",
"badEmail": "That does not look like an email address.", "badEmail": "That does not look like an email address.",
"badPhoneOrHandle": "Use a phone number or a handle.", "badPhoneOrHandle": "Use a phone number or a handle.",
"badTelegram": "Use a Telegram handle, like @yourname.", "badTelegram": "Use a Telegram handle, like {'@'}yourname.",
"badNpub": "That does not look like an npub." "badNpub": "That does not look like an npub."
}, },
"toast": { "toast": {
@ -125,10 +125,10 @@
"nostr": "Nostr" "nostr": "Nostr"
}, },
"methodPlaceholders": { "methodPlaceholders": {
"email": "you@example.com", "email": "you{'@'}example.com",
"whatsapp": "+1 555 123 4567", "whatsapp": "+1 555 123 4567",
"signal": "+1 555 123 4567 or @username", "signal": "+1 555 123 4567 or {'@'}username",
"telegram": "@yourhandle", "telegram": "{'@'}yourhandle",
"nostr": "npub1…" "nostr": "npub1…"
} }
} }

View file

@ -104,7 +104,7 @@
"messageTooLong": "Manténgalo bajo 2000 caracteres.", "messageTooLong": "Manténgalo bajo 2000 caracteres.",
"badEmail": "Eso no parece una dirección de correo.", "badEmail": "Eso no parece una dirección de correo.",
"badPhoneOrHandle": "Use un número de teléfono o un usuario.", "badPhoneOrHandle": "Use un número de teléfono o un usuario.",
"badTelegram": "Use un usuario de Telegram, como @sunombre.", "badTelegram": "Use un usuario de Telegram, como {'@'}sunombre.",
"badNpub": "Eso no parece un npub." "badNpub": "Eso no parece un npub."
}, },
"toast": { "toast": {
@ -125,10 +125,10 @@
"nostr": "Nostr" "nostr": "Nostr"
}, },
"methodPlaceholders": { "methodPlaceholders": {
"email": "tu@ejemplo.com", "email": "tu{'@'}ejemplo.com",
"whatsapp": "+34 600 123 456", "whatsapp": "+34 600 123 456",
"signal": "+34 600 123 456 o @usuario", "signal": "+34 600 123 456 o {'@'}usuario",
"telegram": "@suusuario", "telegram": "{'@'}suusuario",
"nostr": "npub1…" "nostr": "npub1…"
} }
} }

View file

@ -104,7 +104,7 @@
"messageTooLong": "Moins de 2000 caractères, s'il vous plaît.", "messageTooLong": "Moins de 2000 caractères, s'il vous plaît.",
"badEmail": "Cela ne ressemble pas à une adresse email.", "badEmail": "Cela ne ressemble pas à une adresse email.",
"badPhoneOrHandle": "Utilisez un numéro de téléphone ou un pseudo.", "badPhoneOrHandle": "Utilisez un numéro de téléphone ou un pseudo.",
"badTelegram": "Utilisez un pseudo Telegram, comme @votrenom.", "badTelegram": "Utilisez un pseudo Telegram, comme {'@'}votrenom.",
"badNpub": "Cela ne ressemble pas à un npub." "badNpub": "Cela ne ressemble pas à un npub."
}, },
"toast": { "toast": {
@ -125,10 +125,10 @@
"nostr": "Nostr" "nostr": "Nostr"
}, },
"methodPlaceholders": { "methodPlaceholders": {
"email": "vous@exemple.com", "email": "vous{'@'}exemple.com",
"whatsapp": "+33 6 12 34 56 78", "whatsapp": "+33 6 12 34 56 78",
"signal": "+33 6 12 34 56 78 ou @pseudo", "signal": "+33 6 12 34 56 78 ou {'@'}pseudo",
"telegram": "@votrepseudo", "telegram": "{'@'}votrepseudo",
"nostr": "npub1…" "nostr": "npub1…"
} }
} }

View file

@ -3,7 +3,7 @@ import { useI18n } from 'vue-i18n'
import ContactForm from '@/components/contact/ContactForm.vue' import ContactForm from '@/components/contact/ContactForm.vue'
import PrivacyBlurb from '@/components/contact/PrivacyBlurb.vue' import PrivacyBlurb from '@/components/contact/PrivacyBlurb.vue'
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
</script> </script>
<template> <template>

View file

@ -6,7 +6,7 @@ import { AspectRatio } from '@/components/ui/aspect-ratio'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { projects } from '@/data/projects' import { projects } from '@/data/projects'
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
const projectCards = computed(() => const projectCards = computed(() =>
projects.map((p) => ({ projects.map((p) => ({

View file

@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'
import { AspectRatio } from '@/components/ui/aspect-ratio' import { AspectRatio } from '@/components/ui/aspect-ratio'
import { projects } from '@/data/projects' import { projects } from '@/data/projects'
const { t } = useI18n() const { t } = useI18n({ useScope: 'global' })
const projectCards = computed(() => const projectCards = computed(() =>
projects.map((p) => ({ projects.map((p) => ({