Skip to content

Commit 3094d28

Browse files
authored
Merge pull request #3 from babu-ch/feat/i18n-add-languages
feat: add 6 major language translations and locale dropdown
2 parents ce2fb26 + c5b8124 commit 3094d28

10 files changed

Lines changed: 900 additions & 21 deletions

File tree

playground/src/App.vue

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
<script setup lang="ts">
22
import { ref } from "vue";
33
import { useI18n } from "vue-i18n";
4+
import { LANGUAGE_NAMES } from "./i18n";
45
56
const { locale } = useI18n();
67
78
const logs = ref<string[]>([]);
89
9-
function toggleLocale() {
10-
locale.value = locale.value === "ja" ? "en" : "ja";
11-
}
12-
1310
const originalLog = console.log;
1411
console.log = (...args: unknown[]) => {
1512
originalLog(...args);
@@ -26,9 +23,11 @@ console.log = (...args: unknown[]) => {
2623
<header>
2724
<div class="header-top">
2825
<h1>link-interceptor</h1>
29-
<button class="locale-btn" @click="toggleLocale">
30-
{{ $t("locale.switch") }}
31-
</button>
26+
<select v-model="locale" class="locale-select">
27+
<option v-for="(name, code) in LANGUAGE_NAMES" :key="code" :value="code">
28+
{{ name }}
29+
</option>
30+
</select>
3231
</div>
3332
<nav>
3433
<router-link to="/">{{ $t("nav.home") }}</router-link>
@@ -92,20 +91,25 @@ header h1 {
9291
font-size: 1.5rem;
9392
}
9493
95-
.locale-btn {
96-
padding: 0.3rem 0.75rem;
94+
.locale-select {
95+
padding: 0.3rem 0.5rem;
9796
border: 1px solid #ddd;
9897
border-radius: 4px;
9998
background: #fff;
10099
color: #333;
101100
font-size: 0.8rem;
102101
cursor: pointer;
102+
outline: none;
103103
}
104104
105-
.locale-btn:hover {
105+
.locale-select:hover {
106106
background: #f0f0f0;
107107
}
108108
109+
.locale-select:focus {
110+
border-color: #4361ee;
111+
}
112+
109113
nav {
110114
display: flex;
111115
flex-wrap: wrap;

playground/src/i18n/de.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
export default {
2+
nav: {
3+
home: "Startseite",
4+
internal: "Intern",
5+
external: "Extern",
6+
prevent: "Blockieren",
7+
analytics: "Analytik",
8+
confirm: "Bestätigen",
9+
formGuard: "Form Guard",
10+
security: "Sicherheit",
11+
},
12+
home: {
13+
title: "link-interceptor",
14+
description:
15+
"Fängt alle Klicks auf {tag}-Tags in Ihrer SPA ab. Framework-unabhängiger Kern mit Vue-, React- und Svelte-Wrappern. Erfasst in der Capture-Phase und bietet Callbacks für interne/externe Links.",
16+
install: "Installation",
17+
basic: "Interaktive Demos",
18+
useCases: "Anwendungsfälle",
19+
console: "Konsole",
20+
consoleDescription:
21+
"Interceptor-Logs erscheinen im Konsolenpanel unten. Klicken Sie auf einen Link, um sie zu sehen.",
22+
internalDesc: "Interne Links in router.push() umwandeln",
23+
externalDesc: "URLs externer Links umschreiben",
24+
preventDesc: "Link-Navigation blockieren",
25+
analyticsDesc: "Link-Klicks verfolgen",
26+
confirmDesc: "Bestätigungsdialog für externe Navigation",
27+
formGuardDesc: "Navigation bei ungespeicherten Formularänderungen verhindern",
28+
securityDesc: "Domain-Erlaubnisliste + automatisches rel-Attribut",
29+
},
30+
internal: {
31+
title: "Interne Links",
32+
description:
33+
"Erfasst Klicks auf gleichherkunfts-{tag}-Tags mit onInternalLink und wandelt sie über router.push() in SPA-Routing um.",
34+
normalLinks: "Normale HTML-Links (vom Plugin abgefangen)",
35+
toHome: "Zur Startseite",
36+
toExternal: "Zu Externen Links",
37+
toPrevent: "Zu Blockieren",
38+
vhtml: "Links in v-html (dynamisch generiertes HTML wird ebenfalls abgefangen)",
39+
vhtmlContent:
40+
'Dies ist Inhalt, der mit v-html gerendert wurde: <a href="/">Zurück zur Startseite</a> | <a href="/prevent">Blockieren ansehen</a>',
41+
nested: "Verschachtelte Elemente",
42+
nestedDesc: "Klicks auf Kindelemente innerhalb von {tag} werden ebenfalls erkannt",
43+
nestedLink: "Dekorierter Link",
44+
routerLink: "Koexistenz mit Router Link",
45+
routerLinkDesc:
46+
"<router-link> und einfache <a>-Tags funktionieren nebeneinander. Der Interceptor erfasst beide in der Capture-Phase. RouterLink prüft event.defaultPrevented und überspringt seine eigene Navigation, wenn der Interceptor sie bereits behandelt hat.",
47+
routerLinkToHome: "router-link zur Startseite",
48+
plainLinkToExternal: "einfaches <a> zu Externen Links",
49+
routerLinkNote:
50+
"Beide Links erscheinen in der Konsole — der Interceptor behandelt alle <a>-Klicks, unabhängig davon, ob sie von <router-link> oder einfachem HTML stammen.",
51+
routerLinkGotcha: "Fallstrick: router-link replace",
52+
routerLinkGotchaDesc:
53+
"Der Interceptor erfasst auch Klicks auf <router-link replace>. Wenn der Callback ctx.preventDefault() und router.push() aufruft, wird die replace-Prop stillschweigend ignoriert — ein Verlaufseintrag wird hinzugefügt statt ersetzt.",
54+
routerLinkReplaceBroken: "ohne Workaround — replace wird ignoriert (klicken, dann Zurück drücken zum Prüfen)",
55+
routerLinkReplaceFixed: "mit data-no-intercept — replace funktioniert (klicken, dann Zurück drücken zum Vergleichen)",
56+
routerLinkGotchaNote:
57+
"Der erste Link hat keinen Workaround: Der Interceptor ruft preventDefault() + router.push() auf, daher geht replace verloren und ein Verlaufseintrag wird hinzugefügt. Der zweite Link hat data-no-intercept: Der Callback überspringt preventDefault(), sodass RouterLink die Navigation mit replace durchführt.",
58+
routerLinkWorkaround:
59+
"Workaround: Fügen Sie ein data-no-intercept-Attribut zu <router-link>-Elementen hinzu, die Props wie replace beibehalten müssen. Im Callback ctx.anchor.hasAttribute('data-no-intercept') prüfen und ctx.preventDefault() überspringen, damit RouterLink die Navigation selbst durchführt. Siehe main.ts für die Implementierung.",
60+
},
61+
external: {
62+
title: "Externe Links",
63+
description:
64+
"Erfasst Klicks auf externe Links (andere Herkunft) mit onExternalLink. Diese Demo fügt automatisch den Parameter ?from=playground hinzu.",
65+
externalLinks: "Externe Links (URL wird beim Klick umgeschrieben)",
66+
note: 'Prüfen Sie die umgeschriebene URL in der Konsole. Links mit target="_blank" werden ebenfalls erfasst.',
67+
modifierTest: "Modifikatortasten-Test",
68+
modifierDesc:
69+
"Versuchen Sie Strg/Cmd + Klick. Klicks mit Modifikatortasten werden übersprungen, das Verhalten des Browsers für neue Tabs wird respektiert.",
70+
thisLink: "diesen Link",
71+
},
72+
prevent: {
73+
title: "Navigation blockieren",
74+
description:
75+
"Rufen Sie ctx.preventDefault() im Callback auf, um die Link-Navigation abzubrechen.",
76+
normalLink: "Normaler interner Link (navigiert)",
77+
toHome: "Zur Startseite navigieren",
78+
blockedLinks: "Blockierte Links (keine Navigation)",
79+
blockedDesc:
80+
"Die folgenden Links haben ein data-block-Attribut. Die Demo blockiert die Navigation in main.ts.",
81+
blockedLink: "blocked.example.com (Klick navigiert nicht)",
82+
blockedToast: "Navigation zu {url} blockiert",
83+
},
84+
analytics: {
85+
title: "Analytik / Tracking",
86+
description:
87+
"Beispiel für das Auslösen von Analyse-Events bei Link-Klicks. Stellen Sie sich das Senden an GA4 oder Mixpanel vor.",
88+
tryClick: "Versuchen Sie, auf diese Links zu klicken",
89+
internalLink: "Interner Link (Seitennavigation)",
90+
anotherDemo: "Zu einer anderen Demo-Seite",
91+
collectedEvents: "Gesammelte Events",
92+
time: "Zeit",
93+
type: "Typ",
94+
url: "URL",
95+
noEvents: "Noch keine Events",
96+
},
97+
confirm: {
98+
title: "Bestätigungsdialog",
99+
description:
100+
'Zeigt einen Bestätigungsdialog beim Klick auf einen externen Link. "Abbrechen" blockiert die Navigation, "OK" erlaubt sie.',
101+
withConfirm: "Links mit Bestätigungsdialog",
102+
withConfirmDesc: "Die folgenden Links haben ein data-confirm-Attribut.",
103+
confirmSuffix: " (mit Bestätigung)",
104+
withoutConfirm: "Links ohne Bestätigung (normales Verhalten)",
105+
withoutConfirmSuffix: " (ohne Bestätigung)",
106+
internalLink: "Interner Link (ohne Bestätigung)",
107+
implementation: "Implementierungsbeispiel",
108+
confirmPrompt: "Zu {hostname} navigieren?",
109+
},
110+
formGuard: {
111+
title: "Form Guard",
112+
description:
113+
'Zeigt eine Warnung „Änderungen gehen verloren" beim Klick auf einen Link während der Formularbearbeitung. Der Dialog erscheint nur bei ungespeicherten Eingaben.',
114+
formSection: "Formular (geben Sie etwas ein und klicken Sie dann auf einen Link)",
115+
name: "Name",
116+
namePlaceholder: "Max Mustermann",
117+
email: "E-Mail",
118+
emailPlaceholder: "max{'@'}example.com",
119+
dirty: "Ungespeicherte Änderungen",
120+
clean: "Keine Änderungen",
121+
navLinks: "Navigationslinks",
122+
navDesc: "Ein Bestätigungsdialog erscheint beim Klick mit ungespeicherten Formulareingaben.",
123+
implementation: "Implementierungsbeispiel",
124+
confirmLeave: "Änderungen gehen verloren. Trotzdem navigieren?",
125+
},
126+
security: {
127+
title: "Sicherheit",
128+
description:
129+
"Sicherheitskontrollen für externe Links. Kombiniert eine Domain-Erlaubnisliste mit einem automatischen rel-Attribut.",
130+
allowlist: "Erlaubnisliste",
131+
allowlistDesc: "Erlaubte Domains: {domains}. Alle anderen sind blockiert.",
132+
allowed: "Erlaubt",
133+
blocked: "Blockiert",
134+
relSection: "Automatisches rel-Attribut",
135+
relDesc:
136+
'Alle externen Links erhalten automatisch rel="noopener noreferrer". Prüfen Sie im Elements-Panel der DevTools.',
137+
implementation: "Implementierungsbeispiel",
138+
blockedAlert: "{hostname} ist blockiert",
139+
},
140+
console: {
141+
title: "Konsole",
142+
empty: "Klicken Sie auf einen Link, um Interceptor-Logs hier zu sehen",
143+
},
144+
};

playground/src/i18n/en.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ export default {
99
formGuard: "Form Guard",
1010
security: "Security",
1111
},
12-
locale: {
13-
switch: "日本語",
14-
},
15-
home: {
12+
home: {
1613
title: "link-interceptor",
1714
description:
1815
"Intercept all {tag} tag clicks in your SPA. Framework-agnostic core with Vue, React, and Svelte wrappers. Captures at the capture phase and provides callbacks for internal/external links.",

playground/src/i18n/es.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
export default {
2+
nav: {
3+
home: "Inicio",
4+
internal: "Internos",
5+
external: "Externos",
6+
prevent: "Prevenir",
7+
analytics: "Analítica",
8+
confirm: "Confirmar",
9+
formGuard: "Form Guard",
10+
security: "Seguridad",
11+
},
12+
home: {
13+
title: "link-interceptor",
14+
description:
15+
"Intercepta todos los clics en etiquetas {tag} en tu SPA. Núcleo independiente del framework con wrappers para Vue, React y Svelte. Captura en la fase de captura y proporciona callbacks para enlaces internos/externos.",
16+
install: "Instalación",
17+
basic: "Demos interactivas",
18+
useCases: "Casos de uso",
19+
console: "Consola",
20+
consoleDescription:
21+
"Los registros del interceptor aparecen en el panel de consola en la parte inferior. Haz clic en un enlace para verlos.",
22+
internalDesc: "Convertir enlaces internos a router.push()",
23+
externalDesc: "Reescribir URLs de enlaces externos",
24+
preventDesc: "Bloquear la navegación de enlaces",
25+
analyticsDesc: "Rastrear clics en enlaces",
26+
confirmDesc: "Diálogo de confirmación para navegación externa",
27+
formGuardDesc: "Prevenir la navegación con cambios no guardados",
28+
securityDesc: "Lista de dominios permitidos + atributo rel automático",
29+
},
30+
internal: {
31+
title: "Enlaces internos",
32+
description:
33+
"Captura clics en etiquetas {tag} del mismo origen con onInternalLink y los convierte en enrutamiento SPA mediante router.push().",
34+
normalLinks: "Enlaces HTML normales (interceptados por el plugin)",
35+
toHome: "Ir a Inicio",
36+
toExternal: "Ir a Enlaces externos",
37+
toPrevent: "Ir a Prevenir",
38+
vhtml: "Enlaces en v-html (el HTML generado dinámicamente también es interceptado)",
39+
vhtmlContent:
40+
'Este es contenido renderizado con v-html: <a href="/">Volver a Inicio</a> | <a href="/prevent">Ver Prevenir</a>',
41+
nested: "Elementos anidados",
42+
nestedDesc: "Los clics en elementos hijos dentro de {tag} también son detectados",
43+
nestedLink: "Enlace decorado",
44+
routerLink: "Coexistencia con Router Link",
45+
routerLinkDesc:
46+
"<router-link> y las etiquetas <a> simples funcionan lado a lado. El interceptor captura ambos en la fase de captura. RouterLink verifica event.defaultPrevented y omite su propia navegación cuando el interceptor ya lo ha gestionado.",
47+
routerLinkToHome: "router-link a Inicio",
48+
plainLinkToExternal: "<a> simple a Enlaces externos",
49+
routerLinkNote:
50+
"Ambos enlaces aparecen en la consola — el interceptor maneja todos los clics en <a> independientemente de si provienen de <router-link> o HTML simple.",
51+
routerLinkGotcha: "Trampa: router-link replace",
52+
routerLinkGotchaDesc:
53+
"El interceptor también captura clics de <router-link replace>. Si el callback llama a ctx.preventDefault() y router.push(), la prop replace se ignora silenciosamente — se añade una entrada al historial en lugar de reemplazar.",
54+
routerLinkReplaceBroken: "sin solución — replace se ignora (haz clic, luego presiona Atrás para ver)",
55+
routerLinkReplaceFixed: "con data-no-intercept — replace funciona (haz clic, luego presiona Atrás para comparar)",
56+
routerLinkGotchaNote:
57+
"El primer enlace no tiene solución: el interceptor llama a preventDefault() + router.push(), así que replace se pierde y se añade una entrada al historial. El segundo enlace tiene data-no-intercept: el callback omite preventDefault(), dejando que RouterLink maneje la navegación con replace intacto.",
58+
routerLinkWorkaround:
59+
"Solución: añadir un atributo data-no-intercept a los elementos <router-link> que necesiten preservar props como replace. En el callback, verificar ctx.anchor.hasAttribute('data-no-intercept') y omitir ctx.preventDefault() para que RouterLink maneje la navegación. Ver main.ts para la implementación.",
60+
},
61+
external: {
62+
title: "Enlaces externos",
63+
description:
64+
"Captura clics de enlaces externos (diferente origen) con onExternalLink. Esta demo añade automáticamente el parámetro ?from=playground.",
65+
externalLinks: "Enlaces externos (la URL se reescribe al hacer clic)",
66+
note: 'Verifica la URL reescrita en la consola. Los enlaces con target="_blank" también son interceptados.',
67+
modifierTest: "Prueba de teclas modificadoras",
68+
modifierDesc:
69+
"Prueba Ctrl/Cmd + Clic. Los clics con teclas modificadoras se omiten, respetando el comportamiento de nueva pestaña del navegador.",
70+
thisLink: "este enlace",
71+
},
72+
prevent: {
73+
title: "Prevenir navegación",
74+
description:
75+
"Llama a ctx.preventDefault() en el callback para cancelar la navegación del enlace.",
76+
normalLink: "Enlace interno normal (navega)",
77+
toHome: "Navegar a Inicio",
78+
blockedLinks: "Enlaces bloqueados (sin navegación)",
79+
blockedDesc:
80+
"Los siguientes enlaces tienen un atributo data-block. La demo bloquea la navegación en main.ts.",
81+
blockedLink: "blocked.example.com (el clic no navega)",
82+
blockedToast: "Navegación bloqueada a {url}",
83+
},
84+
analytics: {
85+
title: "Analítica / Seguimiento",
86+
description:
87+
"Ejemplo de disparo de eventos de analítica al hacer clic en enlaces. Imagina enviando a GA4 o Mixpanel.",
88+
tryClick: "Prueba hacer clic en estos enlaces",
89+
internalLink: "Enlace interno (navegación de página)",
90+
anotherDemo: "Ir a otra página de demo",
91+
collectedEvents: "Eventos recopilados",
92+
time: "Hora",
93+
type: "Tipo",
94+
url: "URL",
95+
noEvents: "Aún no hay eventos",
96+
},
97+
confirm: {
98+
title: "Diálogo de confirmación",
99+
description:
100+
'Muestra un diálogo de confirmación al hacer clic en un enlace externo. "Cancelar" bloquea la navegación, "Aceptar" la permite.',
101+
withConfirm: "Enlaces con diálogo de confirmación",
102+
withConfirmDesc: "Los siguientes enlaces tienen un atributo data-confirm.",
103+
confirmSuffix: " (con confirmación)",
104+
withoutConfirm: "Enlaces sin confirmación (comportamiento normal)",
105+
withoutConfirmSuffix: " (sin confirmación)",
106+
internalLink: "Enlace interno (sin confirmación)",
107+
implementation: "Ejemplo de implementación",
108+
confirmPrompt: "¿Navegar a {hostname}?",
109+
},
110+
formGuard: {
111+
title: "Form Guard",
112+
description:
113+
'Muestra una advertencia de "los cambios se perderán" al hacer clic en un enlace mientras se edita un formulario. El diálogo solo aparece cuando hay entradas no guardadas.',
114+
formSection: "Formulario (escribe algo y luego haz clic en un enlace)",
115+
name: "Nombre",
116+
namePlaceholder: "Juan García",
117+
email: "Correo electrónico",
118+
emailPlaceholder: "juan{'@'}example.com",
119+
dirty: "Cambios no guardados",
120+
clean: "Sin cambios",
121+
navLinks: "Enlaces de navegación",
122+
navDesc: "Aparece un diálogo de confirmación al hacer clic con entradas de formulario no guardadas.",
123+
implementation: "Ejemplo de implementación",
124+
confirmLeave: "Los cambios se perderán. ¿Navegar de todos modos?",
125+
},
126+
security: {
127+
title: "Seguridad",
128+
description:
129+
"Controles de seguridad para enlaces externos. Combina lista de dominios permitidos con atributo rel automático.",
130+
allowlist: "Lista de permitidos",
131+
allowlistDesc: "Dominios permitidos: {domains}. Todos los demás están bloqueados.",
132+
allowed: "Permitido",
133+
blocked: "Bloqueado",
134+
relSection: "Atributo rel automático",
135+
relDesc:
136+
'Todos los enlaces externos obtienen automáticamente rel="noopener noreferrer". Verifica en el panel Elements de DevTools.',
137+
implementation: "Ejemplo de implementación",
138+
blockedAlert: "{hostname} está bloqueado",
139+
},
140+
console: {
141+
title: "Consola",
142+
empty: "Haz clic en un enlace para ver los registros del interceptor aquí",
143+
},
144+
};

0 commit comments

Comments
 (0)