feat(app): sprach-switcher direkt im post (📖 DE | EN)
statt text-hinweis "auch verfügbar in: ..." zeigt der post jetzt einen
kompakten switcher (📖 aktiver-code | anderer-code). klick auf den
anderen code setzt die ui-sprache global und navigiert zur sprach-
variante — alles konsistent.
language names raus (unused): displayLanguage + tests entfernt, da die
darstellung nun nur noch sprachcodes (DE/EN) zeigt. auch i18n-keys
lang.de/lang.en und post.also_available_in aufgeräumt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
238b2a0938
commit
9040e5ac02
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NostrEvent, TranslationInfo } from '$lib/nostr/loaders';
|
import type { NostrEvent, TranslationInfo } from '$lib/nostr/loaders';
|
||||||
import { loadTranslations } from '$lib/nostr/loaders';
|
import { loadTranslations } from '$lib/nostr/loaders';
|
||||||
import { displayLanguage } from '$lib/nostr/languageNames';
|
import { activeLocale } from '$lib/i18n';
|
||||||
import { t } from '$lib/i18n';
|
import type { SupportedLocale } from '$lib/i18n/activeLocale';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
|
|
@ -25,28 +25,83 @@
|
||||||
if (event.id === currentId) loading = false;
|
if (event.id === currentId) loading = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function currentLang(): string {
|
||||||
|
return event.tags.find((tag) => tag[0] === 'l')?.[1] ?? 'de';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
code: string;
|
||||||
|
href: string | null; // null = aktueller post, kein klick-ziel
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = $derived.by<Option[]>(() => {
|
||||||
|
const self: Option = { code: currentLang(), href: null };
|
||||||
|
const others: Option[] = translations.map((t) => ({
|
||||||
|
code: t.lang,
|
||||||
|
href: `/${t.slug}/`
|
||||||
|
}));
|
||||||
|
// aktuelle sprache zuerst, dann rest sortiert nach code
|
||||||
|
return [self, ...others.sort((a, b) => a.code.localeCompare(b.code))];
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectOther(code: string, href: string) {
|
||||||
|
activeLocale.set(code as SupportedLocale);
|
||||||
|
// hartes location-setzen, damit svelte-kit-router den post-load triggert
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !loading && translations.length > 0}
|
{#if !loading && translations.length > 0}
|
||||||
<p class="availability">
|
<p class="lang-switch" role="group" aria-label="Article language">
|
||||||
{$t('post.also_available_in')}
|
<span class="icon" aria-hidden="true">📖</span>
|
||||||
{#each translations as t, i}
|
{#each options as opt, i}
|
||||||
<a href="/{t.slug}/" title={t.title}>{displayLanguage(t.lang)}</a>{#if i < translations.length - 1}, {/if}
|
{#if opt.href === null}
|
||||||
|
<span class="btn active" aria-current="true">{opt.code.toUpperCase()}</span>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
onclick={() => selectOther(opt.code, opt.href!)}
|
||||||
|
>{opt.code.toUpperCase()}</button>
|
||||||
|
{/if}
|
||||||
|
{#if i < options.length - 1}<span class="sep" aria-hidden="true">|</span>{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.availability {
|
.lang-switch {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin: 0.25rem 0 1rem;
|
margin: 0.25rem 0 1rem;
|
||||||
}
|
}
|
||||||
.availability a {
|
.icon {
|
||||||
color: var(--accent);
|
font-size: 1rem;
|
||||||
text-decoration: none;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.availability a:hover {
|
.btn {
|
||||||
text-decoration: underline;
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--muted);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 7px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover:not(.active) {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
.btn.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.sep {
|
||||||
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
"back_to_overview": "← Zurück zur Übersicht",
|
"back_to_overview": "← Zurück zur Übersicht",
|
||||||
"untitled": "(ohne Titel)",
|
"untitled": "(ohne Titel)",
|
||||||
"published_on": "Veröffentlicht am {date}",
|
"published_on": "Veröffentlicht am {date}",
|
||||||
"also_available_in": "Auch verfügbar in:",
|
|
||||||
"not_found": "Post \"{slug}\" nicht gefunden.",
|
"not_found": "Post \"{slug}\" nicht gefunden.",
|
||||||
"unknown_error": "Unbekannter Fehler"
|
"unknown_error": "Unbekannter Fehler"
|
||||||
},
|
},
|
||||||
|
|
@ -28,8 +27,6 @@
|
||||||
"doc_title": "Impressum – Jörg Lohrer"
|
"doc_title": "Impressum – Jörg Lohrer"
|
||||||
},
|
},
|
||||||
"lang": {
|
"lang": {
|
||||||
"de": "Deutsch",
|
|
||||||
"en": "English",
|
|
||||||
"switch_aria": "Sprache wechseln"
|
"switch_aria": "Sprache wechseln"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
"back_to_overview": "← Back to overview",
|
"back_to_overview": "← Back to overview",
|
||||||
"untitled": "(untitled)",
|
"untitled": "(untitled)",
|
||||||
"published_on": "Published on {date}",
|
"published_on": "Published on {date}",
|
||||||
"also_available_in": "Also available in:",
|
|
||||||
"not_found": "Post \"{slug}\" not found.",
|
"not_found": "Post \"{slug}\" not found.",
|
||||||
"unknown_error": "Unknown error"
|
"unknown_error": "Unknown error"
|
||||||
},
|
},
|
||||||
|
|
@ -28,8 +27,6 @@
|
||||||
"doc_title": "Imprint – Jörg Lohrer"
|
"doc_title": "Imprint – Jörg Lohrer"
|
||||||
},
|
},
|
||||||
"lang": {
|
"lang": {
|
||||||
"de": "German",
|
|
||||||
"en": "English",
|
|
||||||
"switch_aria": "Switch language"
|
"switch_aria": "Switch language"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { displayLanguage } from './languageNames';
|
|
||||||
|
|
||||||
describe('displayLanguage', () => {
|
|
||||||
it('kennt deutsch', () => {
|
|
||||||
expect(displayLanguage('de')).toBe('Deutsch');
|
|
||||||
});
|
|
||||||
it('kennt english', () => {
|
|
||||||
expect(displayLanguage('en')).toBe('English');
|
|
||||||
});
|
|
||||||
it('fällt bei unbekanntem code auf uppercase-code zurück', () => {
|
|
||||||
expect(displayLanguage('fr')).toBe('FR');
|
|
||||||
});
|
|
||||||
it('fällt bei leerer sprache auf ? zurück', () => {
|
|
||||||
expect(displayLanguage('')).toBe('?');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
const NAMES: Record<string, string> = {
|
|
||||||
de: 'Deutsch',
|
|
||||||
en: 'English'
|
|
||||||
};
|
|
||||||
|
|
||||||
export function displayLanguage(code: string): string {
|
|
||||||
if (!code) return '?';
|
|
||||||
return NAMES[code] ?? code.toUpperCase();
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue