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'
import { submitInquiry, type ContactMethod } from '@/features/nostr/submitInquiry'
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
const METHODS: ContactMethod[] = ['email', 'whatsapp', 'signal', 'telegram', 'nostr']

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import ProjectImage from './ProjectImage.vue'
import type { Project } from '@/data/projects'
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 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'
const props = defineProps<{ image: ProjectImage }>()
const { t } = useI18n()
const { t } = useI18n({ useScope: 'global' })
const open = ref(false)
</script>

View file

@ -104,7 +104,7 @@
"messageTooLong": "Keep it under 2000 characters.",
"badEmail": "That does not look like an email address.",
"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."
},
"toast": {
@ -125,10 +125,10 @@
"nostr": "Nostr"
},
"methodPlaceholders": {
"email": "you@example.com",
"email": "you{'@'}example.com",
"whatsapp": "+1 555 123 4567",
"signal": "+1 555 123 4567 or @username",
"telegram": "@yourhandle",
"signal": "+1 555 123 4567 or {'@'}username",
"telegram": "{'@'}yourhandle",
"nostr": "npub1…"
}
}

View file

@ -104,7 +104,7 @@
"messageTooLong": "Manténgalo bajo 2000 caracteres.",
"badEmail": "Eso no parece una dirección de correo.",
"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."
},
"toast": {
@ -125,10 +125,10 @@
"nostr": "Nostr"
},
"methodPlaceholders": {
"email": "tu@ejemplo.com",
"email": "tu{'@'}ejemplo.com",
"whatsapp": "+34 600 123 456",
"signal": "+34 600 123 456 o @usuario",
"telegram": "@suusuario",
"signal": "+34 600 123 456 o {'@'}usuario",
"telegram": "{'@'}suusuario",
"nostr": "npub1…"
}
}

View file

@ -104,7 +104,7 @@
"messageTooLong": "Moins de 2000 caractères, s'il vous plaît.",
"badEmail": "Cela ne ressemble pas à une adresse email.",
"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."
},
"toast": {
@ -125,10 +125,10 @@
"nostr": "Nostr"
},
"methodPlaceholders": {
"email": "vous@exemple.com",
"email": "vous{'@'}exemple.com",
"whatsapp": "+33 6 12 34 56 78",
"signal": "+33 6 12 34 56 78 ou @pseudo",
"telegram": "@votrepseudo",
"signal": "+33 6 12 34 56 78 ou {'@'}pseudo",
"telegram": "{'@'}votrepseudo",
"nostr": "npub1…"
}
}

View file

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

View file

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

View file

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