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">
|
||||
import type { NostrEvent, TranslationInfo } from '$lib/nostr/loaders';
|
||||
import { loadTranslations } from '$lib/nostr/loaders';
|
||||
import { displayLanguage } from '$lib/nostr/languageNames';
|
||||
import { t } from '$lib/i18n';
|
||||
import { activeLocale } from '$lib/i18n';
|
||||
import type { SupportedLocale } from '$lib/i18n/activeLocale';
|
||||
|
||||
interface Props {
|
||||
event: NostrEvent;
|
||||
|
|
@ -25,28 +25,83 @@
|
|||
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>
|
||||
|
||||
{#if !loading && translations.length > 0}
|
||||
<p class="availability">
|
||||
{$t('post.also_available_in')}
|
||||
{#each translations as t, i}
|
||||
<a href="/{t.slug}/" title={t.title}>{displayLanguage(t.lang)}</a>{#if i < translations.length - 1}, {/if}
|
||||
<p class="lang-switch" role="group" aria-label="Article language">
|
||||
<span class="icon" aria-hidden="true">📖</span>
|
||||
{#each options as opt, i}
|
||||
{#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}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.availability {
|
||||
.lang-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.88rem;
|
||||
color: var(--muted);
|
||||
margin: 0.25rem 0 1rem;
|
||||
}
|
||||
.availability a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
.icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.availability a:hover {
|
||||
text-decoration: underline;
|
||||
.btn {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
"back_to_overview": "← Zurück zur Übersicht",
|
||||
"untitled": "(ohne Titel)",
|
||||
"published_on": "Veröffentlicht am {date}",
|
||||
"also_available_in": "Auch verfügbar in:",
|
||||
"not_found": "Post \"{slug}\" nicht gefunden.",
|
||||
"unknown_error": "Unbekannter Fehler"
|
||||
},
|
||||
|
|
@ -28,8 +27,6 @@
|
|||
"doc_title": "Impressum – Jörg Lohrer"
|
||||
},
|
||||
"lang": {
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"switch_aria": "Sprache wechseln"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
"back_to_overview": "← Back to overview",
|
||||
"untitled": "(untitled)",
|
||||
"published_on": "Published on {date}",
|
||||
"also_available_in": "Also available in:",
|
||||
"not_found": "Post \"{slug}\" not found.",
|
||||
"unknown_error": "Unknown error"
|
||||
},
|
||||
|
|
@ -28,8 +27,6 @@
|
|||
"doc_title": "Imprint – Jörg Lohrer"
|
||||
},
|
||||
"lang": {
|
||||
"de": "German",
|
||||
"en": "English",
|
||||
"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