feat(i18n): fix remaining hardcoded strings + login page language switcher #16
@@ -3,35 +3,38 @@ import { test, expect } from '@playwright/test';
|
|||||||
test.describe('Language selector', () => {
|
test.describe('Language selector', () => {
|
||||||
test('shows DE, EN, ES buttons in the header', async ({ page }) => {
|
test('shows DE, EN, ES buttons in the header', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page.getByRole('banner').getByRole('button', { name: 'DE' })).toBeVisible();
|
await expect(page.getByRole('banner').getByRole('button', { name: 'DE', exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole('banner').getByRole('button', { name: 'EN' })).toBeVisible();
|
await expect(page.getByRole('banner').getByRole('button', { name: 'EN', exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole('banner').getByRole('button', { name: 'ES' })).toBeVisible();
|
await expect(page.getByRole('banner').getByRole('button', { name: 'ES', exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('switching to EN translates the navigation', async ({ page }) => {
|
test('switching to EN translates the navigation', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
await page.getByRole('banner').getByRole('button', { name: 'EN', exact: true }).click();
|
||||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Documents' })).toBeVisible();
|
await expect(page.getByRole('navigation').getByRole('link', { name: 'Documents' })).toBeVisible();
|
||||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Persons' })).toBeVisible();
|
await expect(page.getByRole('navigation').getByRole('link', { name: 'Persons' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('language choice persists after navigation', async ({ page }) => {
|
test('language choice persists after navigation', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
await page.getByRole('banner').getByRole('button', { name: 'EN', exact: true }).click();
|
||||||
await page.goto('/persons');
|
await page.goto('/persons');
|
||||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Documents' })).toBeVisible();
|
await expect(page.getByRole('navigation').getByRole('link', { name: 'Documents' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('switching back to DE restores German', async ({ page }) => {
|
test('switching back to DE restores German', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
|
await page.waitForSelector('[data-hydrated]');
|
||||||
await page.getByRole('banner').getByRole('button', { name: 'DE' }).click();
|
await page.getByRole('banner').getByRole('button', { name: 'EN', exact: true }).click();
|
||||||
|
await page.getByRole('banner').getByRole('button', { name: 'DE', exact: true }).click();
|
||||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Dokumente' })).toBeVisible();
|
await expect(page.getByRole('navigation').getByRole('link', { name: 'Dokumente' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('active language button is visually highlighted', async ({ page }) => {
|
test('active language button is visually highlighted', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
const deBtn = page.getByRole('banner').getByRole('button', { name: 'DE' });
|
const deBtn = page.getByRole('banner').getByRole('button', { name: 'DE', exact: true });
|
||||||
await expect(deBtn).toHaveClass(/font-bold/);
|
await expect(deBtn).toHaveClass(/font-bold/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ test.describe('Conversations', () => {
|
|||||||
|
|
||||||
test('sort toggle changes the button label', async ({ page }) => {
|
test('sort toggle changes the button label', async ({ page }) => {
|
||||||
await page.goto('/conversations');
|
await page.goto('/conversations');
|
||||||
|
await page.waitForSelector('[data-hydrated]');
|
||||||
const btn = page.getByRole('button', { name: /Sortierung/i });
|
const btn = page.getByRole('button', { name: /Sortierung/i });
|
||||||
await expect(btn).toContainText('Neueste zuerst');
|
await expect(btn).toContainText('Neueste zuerst');
|
||||||
await btn.click();
|
await btn.click();
|
||||||
|
|||||||
@@ -155,6 +155,17 @@
|
|||||||
"admin_group_delete_confirm": "Gruppe wirklich löschen?",
|
"admin_group_delete_confirm": "Gruppe wirklich löschen?",
|
||||||
"admin_section_new_group": "Neue Gruppe anlegen",
|
"admin_section_new_group": "Neue Gruppe anlegen",
|
||||||
"admin_group_name_placeholder": "Gruppenname (z.B. Editoren)",
|
"admin_group_name_placeholder": "Gruppenname (z.B. Editoren)",
|
||||||
|
"admin_user_delete_confirm": "Benutzer {username} wirklich löschen?",
|
||||||
|
|
||||||
|
"doc_file_error_preview": "Vorschau konnte nicht geladen werden.",
|
||||||
|
"doc_download_title": "Herunterladen",
|
||||||
|
"doc_tag_filter_title": "Nach {name} filtern",
|
||||||
|
"doc_conversation_title": "Konversation anzeigen",
|
||||||
|
"doc_preview_iframe_title": "Dokumentvorschau",
|
||||||
|
"doc_image_alt": "Original-Scan",
|
||||||
|
"doc_no_date": "Kein Datum",
|
||||||
|
|
||||||
|
"person_merge_will_be_deleted": "wird gelöscht.",
|
||||||
|
|
||||||
"comp_typeahead_placeholder": "Namen tippen...",
|
"comp_typeahead_placeholder": "Namen tippen...",
|
||||||
"comp_typeahead_loading": "Suche...",
|
"comp_typeahead_loading": "Suche...",
|
||||||
|
|||||||
@@ -155,6 +155,17 @@
|
|||||||
"admin_group_delete_confirm": "Really delete group?",
|
"admin_group_delete_confirm": "Really delete group?",
|
||||||
"admin_section_new_group": "Create new group",
|
"admin_section_new_group": "Create new group",
|
||||||
"admin_group_name_placeholder": "Group name (e.g. Editors)",
|
"admin_group_name_placeholder": "Group name (e.g. Editors)",
|
||||||
|
"admin_user_delete_confirm": "Really delete user {username}?",
|
||||||
|
|
||||||
|
"doc_file_error_preview": "Could not load preview.",
|
||||||
|
"doc_download_title": "Download",
|
||||||
|
"doc_tag_filter_title": "Filter by {name}",
|
||||||
|
"doc_conversation_title": "Show conversation",
|
||||||
|
"doc_preview_iframe_title": "Document Preview",
|
||||||
|
"doc_image_alt": "Original scan",
|
||||||
|
"doc_no_date": "No date",
|
||||||
|
|
||||||
|
"person_merge_will_be_deleted": "will be deleted.",
|
||||||
|
|
||||||
"comp_typeahead_placeholder": "Type a name...",
|
"comp_typeahead_placeholder": "Type a name...",
|
||||||
"comp_typeahead_loading": "Searching...",
|
"comp_typeahead_loading": "Searching...",
|
||||||
|
|||||||
@@ -155,6 +155,17 @@
|
|||||||
"admin_group_delete_confirm": "¿Realmente eliminar el grupo?",
|
"admin_group_delete_confirm": "¿Realmente eliminar el grupo?",
|
||||||
"admin_section_new_group": "Crear nuevo grupo",
|
"admin_section_new_group": "Crear nuevo grupo",
|
||||||
"admin_group_name_placeholder": "Nombre del grupo (p.ej. Editores)",
|
"admin_group_name_placeholder": "Nombre del grupo (p.ej. Editores)",
|
||||||
|
"admin_user_delete_confirm": "¿Realmente eliminar al usuario {username}?",
|
||||||
|
|
||||||
|
"doc_file_error_preview": "No se pudo cargar la vista previa.",
|
||||||
|
"doc_download_title": "Descargar",
|
||||||
|
"doc_tag_filter_title": "Filtrar por {name}",
|
||||||
|
"doc_conversation_title": "Ver conversación",
|
||||||
|
"doc_preview_iframe_title": "Vista previa del documento",
|
||||||
|
"doc_image_alt": "Escaneado original",
|
||||||
|
"doc_no_date": "Sin fecha",
|
||||||
|
|
||||||
|
"person_merge_will_be_deleted": "será eliminado.",
|
||||||
|
|
||||||
"comp_typeahead_placeholder": "Escriba un nombre...",
|
"comp_typeahead_placeholder": "Escriba un nombre...",
|
||||||
"comp_typeahead_loading": "Buscando...",
|
"comp_typeahead_loading": "Buscando...",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
use: {
|
use: {
|
||||||
baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:3000',
|
baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:3000',
|
||||||
|
locale: 'de-DE', // ensures Accept-Language: de is sent so locale detection defaults to German
|
||||||
screenshot: 'on', // always capture screenshots
|
screenshot: 'on', // always capture screenshots
|
||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
trace: 'retain-on-failure'
|
trace: 'retain-on-failure'
|
||||||
|
|||||||
@@ -2,9 +2,21 @@ import { redirect, type Handle, type HandleFetch } from '@sveltejs/kit';
|
|||||||
import { paraglideMiddleware } from '$lib/paraglide/server';
|
import { paraglideMiddleware } from '$lib/paraglide/server';
|
||||||
import { sequence } from '@sveltejs/kit/hooks';
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
import { env } from 'process';
|
import { env } from 'process';
|
||||||
|
import { cookieName, cookieMaxAge } from '$lib/paraglide/runtime';
|
||||||
|
import { detectLocale } from '$lib/server/locale';
|
||||||
|
|
||||||
const PUBLIC_PATHS = ['/login', '/logout'];
|
const PUBLIC_PATHS = ['/login', '/logout'];
|
||||||
|
|
||||||
|
const handleLocaleDetection: Handle = ({ event, resolve }) => {
|
||||||
|
if (!event.cookies.get(cookieName)) {
|
||||||
|
const locale = detectLocale(event.request.headers.get('accept-language') ?? '');
|
||||||
|
if (locale) {
|
||||||
|
event.cookies.set(cookieName, locale, { path: '/', sameSite: 'lax', maxAge: cookieMaxAge, httpOnly: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAuth: Handle = async ({ event, resolve }) => {
|
const handleAuth: Handle = async ({ event, resolve }) => {
|
||||||
const isPublic = PUBLIC_PATHS.some((p) => event.url.pathname.startsWith(p));
|
const isPublic = PUBLIC_PATHS.some((p) => event.url.pathname.startsWith(p));
|
||||||
if (!isPublic && !event.locals.user) {
|
if (!isPublic && !event.locals.user) {
|
||||||
@@ -74,4 +86,4 @@ export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
|
|||||||
return fetch(request);
|
return fetch(request);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handle = sequence(userGroup, handleAuth, handleParaglide);
|
export const handle = sequence(userGroup, handleAuth, handleLocaleDetection, handleParaglide);
|
||||||
|
|||||||
32
frontend/src/lib/server/locale.spec.ts
Normal file
32
frontend/src/lib/server/locale.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { detectLocale } from './locale';
|
||||||
|
|
||||||
|
describe('detectLocale', () => {
|
||||||
|
it('returns de for a German browser', () => {
|
||||||
|
expect(detectLocale('de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7')).toBe('de');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns en for an English browser', () => {
|
||||||
|
expect(detectLocale('en-US,en;q=0.9')).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns es for a Spanish browser', () => {
|
||||||
|
expect(detectLocale('es-MX,es;q=0.9,en-US;q=0.8')).toBe('es');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to a supported language when the primary is unsupported', () => {
|
||||||
|
expect(detectLocale('fr-FR,fr;q=0.9,en;q=0.8')).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects quality values — picks the highest-priority supported locale', () => {
|
||||||
|
expect(detectLocale('en-US;q=0.7,de-DE;q=0.9')).toBe('de');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for a completely unsupported language', () => {
|
||||||
|
expect(detectLocale('ja-JP,ja;q=0.9,zh-CN;q=0.8')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for an empty header', () => {
|
||||||
|
expect(detectLocale('')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
20
frontend/src/lib/server/locale.ts
Normal file
20
frontend/src/lib/server/locale.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { locales } from '$lib/paraglide/runtime';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks the best supported locale from an Accept-Language header value.
|
||||||
|
* Returns null when no supported locale is found.
|
||||||
|
*/
|
||||||
|
export function detectLocale(acceptLanguage: string): string | null {
|
||||||
|
const preferred = acceptLanguage
|
||||||
|
.split(',')
|
||||||
|
.map((part) => {
|
||||||
|
const [lang, q] = part.trim().split(';q=');
|
||||||
|
return { lang: lang.trim().split('-')[0].toLowerCase(), q: q ? parseFloat(q) : 1 };
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.q - a.q);
|
||||||
|
|
||||||
|
for (const { lang } of preferred) {
|
||||||
|
if ((locales as readonly string[]).includes(lang)) return lang;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
method="POST"
|
method="POST"
|
||||||
action="?/deleteUser"
|
action="?/deleteUser"
|
||||||
use:enhance={({ cancel }) => {
|
use:enhance={({ cancel }) => {
|
||||||
if (!confirm(`Benutzer '${user.username}' wirklich löschen?`)) {
|
if (!confirm(m.admin_user_delete_confirm({ username: user.username }))) {
|
||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
return async ({ update }) => {
|
return async ({ update }) => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
if (fromDate) params.set('from', fromDate);
|
if (fromDate) params.set('from', fromDate);
|
||||||
if (toDate) params.set('to', toDate);
|
if (toDate) params.set('to', toDate);
|
||||||
params.set('dir', sortDir);
|
params.set('dir', sortDir);
|
||||||
goto(`?${params.toString()}`, { keepFocus: true });
|
goto(`/conversations?${params.toString()}`, { keepFocus: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSort() {
|
function toggleSort() {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
error = 'Vorschau konnte nicht geladen werden.';
|
error = m.doc_file_error_preview();
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
href={fileUrl}
|
href={fileUrl}
|
||||||
download={doc.originalFilename}
|
download={doc.originalFilename}
|
||||||
class="text-brand-navy bg-brand-sand/50 hover:bg-brand-mint border border-transparent p-2 rounded transition"
|
class="text-brand-navy bg-brand-sand/50 hover:bg-brand-mint border border-transparent p-2 rounded transition"
|
||||||
title="Download"
|
title={m.doc_download_title()}
|
||||||
>
|
>
|
||||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Download-MD.svg" alt="" aria-hidden="true" class="w-5 h-5" />
|
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Download-MD.svg" alt="" aria-hidden="true" class="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
<a
|
<a
|
||||||
href="/?tag={encodeURIComponent(tag.name)}"
|
href="/?tag={encodeURIComponent(tag.name)}"
|
||||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold uppercase tracking-wide bg-brand-sand/50 text-brand-navy hover:bg-brand-navy hover:text-white transition-colors"
|
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold uppercase tracking-wide bg-brand-sand/50 text-brand-navy hover:bg-brand-navy hover:text-white transition-colors"
|
||||||
title="Nach '{tag.name}' filtern"
|
title={m.doc_tag_filter_title({ name: tag.name })}
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</a>
|
</a>
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
<a
|
<a
|
||||||
href="/conversations?senderId={doc.sender.id}&receiverId={receiver.id}"
|
href="/conversations?senderId={doc.sender.id}&receiverId={receiver.id}"
|
||||||
class="text-gray-300 hover:text-brand-mint transition"
|
class="text-gray-300 hover:text-brand-mint transition"
|
||||||
title="Konversation anzeigen"
|
title={m.doc_conversation_title()}
|
||||||
>
|
>
|
||||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Chat-MD.svg" alt="" aria-hidden="true" class="w-5 h-5" />
|
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Chat-MD.svg" alt="" aria-hidden="true" class="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
@@ -341,14 +341,14 @@
|
|||||||
{:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')}
|
{:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')}
|
||||||
<iframe
|
<iframe
|
||||||
src={fileUrl}
|
src={fileUrl}
|
||||||
title="Document Preview"
|
title={m.doc_preview_iframe_title()}
|
||||||
class="w-full h-full border-none bg-white"
|
class="w-full h-full border-none bg-white"
|
||||||
></iframe>
|
></iframe>
|
||||||
{:else if fileUrl}
|
{:else if fileUrl}
|
||||||
<div class="w-full h-full flex items-center justify-center overflow-auto p-8">
|
<div class="w-full h-full flex items-center justify-center overflow-auto p-8">
|
||||||
<img
|
<img
|
||||||
src={fileUrl}
|
src={fileUrl}
|
||||||
alt="Original Scan"
|
alt={m.doc_image_alt()}
|
||||||
class="max-w-full max-h-full object-contain shadow-2xl"
|
class="max-w-full max-h-full object-contain shadow-2xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||||
|
|
||||||
let { form }: { form?: { error?: string; success?: boolean } } = $props();
|
let { form }: { form?: { error?: string; success?: boolean } } = $props();
|
||||||
|
|
||||||
|
const locales = ['DE', 'EN', 'ES'] as const;
|
||||||
|
const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
|
||||||
|
const activeLocale = $derived(getLocale().toUpperCase());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-screen bg-white flex flex-col">
|
<div class="relative min-h-screen bg-white flex flex-col">
|
||||||
<!-- DGB purple accent strip -->
|
<!-- DGB purple accent strip -->
|
||||||
<div class="h-1 bg-brand-purple"></div>
|
<div class="h-1 bg-brand-purple"></div>
|
||||||
|
|
||||||
|
<!-- Language switcher -->
|
||||||
|
<div class="absolute top-4 right-4 flex items-center gap-1">
|
||||||
|
{#each locales as locale (locale)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => setLocale(localeMap[locale])}
|
||||||
|
class="text-xs font-sans tracking-widest px-1.5 py-1 transition-colors
|
||||||
|
{activeLocale === locale
|
||||||
|
? 'font-bold text-brand-navy'
|
||||||
|
: 'font-normal text-gray-400 hover:text-brand-navy'}"
|
||||||
|
>
|
||||||
|
{locale}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 flex items-center justify-center px-4">
|
<div class="flex-1 flex items-center justify-center px-4">
|
||||||
<div class="w-full max-w-sm">
|
<div class="w-full max-w-sm">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
|
|||||||
@@ -189,7 +189,7 @@
|
|||||||
|
|
||||||
{#if showMergeConfirm}
|
{#if showMergeConfirm}
|
||||||
<p class="mt-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2">
|
<p class="mt-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2">
|
||||||
{m.person_merge_warning()} <strong>{person.firstName} {person.lastName}</strong> wird gelöscht.
|
{m.person_merge_warning()} <strong>{person.firstName} {person.lastName}</strong> {m.person_merge_will_be_deleted()}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
{doc.title || doc.originalFilename}
|
{doc.title || doc.originalFilename}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center text-xs font-sans text-gray-500 mt-0.5 space-x-2">
|
<div class="flex items-center text-xs font-sans text-gray-500 mt-0.5 space-x-2">
|
||||||
<span>{doc.documentDate ? new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(doc.documentDate + 'T12:00:00')) : 'Kein Datum'}</span>
|
<span>{doc.documentDate ? new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(doc.documentDate + 'T12:00:00')) : m.doc_no_date()}</span>
|
||||||
{#if doc.location}
|
{#if doc.location}
|
||||||
<span class="text-brand-mint">•</span>
|
<span class="text-brand-mint">•</span>
|
||||||
<span>{doc.location}</span>
|
<span>{doc.location}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user