feat(app): loadTranslations liefert sprach-varianten eines posts

This commit is contained in:
Jörg Lohrer 2026-04-21 12:43:52 +02:00
parent 8f4125fcc9
commit 7f48644dfc
2 changed files with 127 additions and 0 deletions

View File

@ -0,0 +1,74 @@
import { describe, it, expect } from 'vitest';
import { resolveTranslationsFromRefs } from './loaders';
import type { NostrEvent } from './loaders';
import type { TranslationRef } from './translations';
function ev(tags: string[][]): NostrEvent {
return {
id: 'x',
pubkey: 'p',
created_at: 0,
kind: 30023,
tags,
content: '',
sig: 's'
} as unknown as NostrEvent;
}
describe('resolveTranslationsFromRefs', () => {
it('liefert lang/slug/title für jeden aufgelösten ref', async () => {
const refs: TranslationRef[] = [
{ kind: 30023, pubkey: 'p1', dtag: 'hello' }
];
const fetcher = async () => [
ev([
['d', 'hello'],
['title', 'Hello World'],
['L', 'ISO-639-1'],
['l', 'en', 'ISO-639-1']
])
];
const result = await resolveTranslationsFromRefs(refs, fetcher);
expect(result).toEqual([
{ lang: 'en', slug: 'hello', title: 'Hello World' }
]);
});
it('ignoriert refs, zu denen kein event gefunden wird', async () => {
const refs: TranslationRef[] = [
{ kind: 30023, pubkey: 'p1', dtag: 'hello' },
{ kind: 30023, pubkey: 'p1', dtag: 'missing' }
];
const fetcher = async (r: TranslationRef) =>
r.dtag === 'hello'
? [ev([
['d', 'hello'],
['title', 'Hi'],
['l', 'en', 'ISO-639-1']
])]
: [];
const result = await resolveTranslationsFromRefs(refs, fetcher);
expect(result).toEqual([{ lang: 'en', slug: 'hello', title: 'Hi' }]);
});
it('ignoriert events ohne l-tag (sprache unklar)', async () => {
const refs: TranslationRef[] = [
{ kind: 30023, pubkey: 'p', dtag: 'x' }
];
const fetcher = async () => [
ev([
['d', 'x'],
['title', 'kein lang-tag']
])
];
const result = await resolveTranslationsFromRefs(refs, fetcher);
expect(result).toEqual([]);
});
it('leere ref-liste → leere ergebnis-liste', async () => {
const fetcher = async () => {
throw new Error('should not be called');
};
expect(await resolveTranslationsFromRefs([], fetcher)).toEqual([]);
});
});

View File

@ -6,6 +6,7 @@ import type { Filter as ApplesauceFilter } from 'applesauce-core/helpers/filter'
import { pool } from './pool'; import { pool } from './pool';
import { readRelays } from '$lib/stores/readRelays'; import { readRelays } from '$lib/stores/readRelays';
import { AUTHOR_PUBKEY_HEX, RELAY_HARD_TIMEOUT_MS } from './config'; import { AUTHOR_PUBKEY_HEX, RELAY_HARD_TIMEOUT_MS } from './config';
import type { TranslationRef } from './translations';
/** Re-export als sprechenden Alias */ /** Re-export als sprechenden Alias */
export type { NostrEvent }; export type { NostrEvent };
@ -189,3 +190,55 @@ export async function loadReactions(dtag: string): Promise<ReactionSummary[]> {
.map(([content, count]) => ({ content, count })) .map(([content, count]) => ({ content, count }))
.sort((a, b) => b.count - a.count); .sort((a, b) => b.count - a.count);
} }
export interface TranslationInfo {
lang: string;
slug: string;
title: string;
}
/**
* Pure Variante für Tests erhält die Events via Fetcher statt Relays.
*/
export async function resolveTranslationsFromRefs(
refs: TranslationRef[],
fetcher: (ref: TranslationRef) => Promise<NostrEvent[]>
): Promise<TranslationInfo[]> {
if (refs.length === 0) return [];
const results = await Promise.all(refs.map(fetcher));
const infos: TranslationInfo[] = [];
for (let i = 0; i < refs.length; i++) {
const evs = results[i];
if (evs.length === 0) continue;
const latest = evs.reduce((best, cur) =>
cur.created_at > best.created_at ? cur : best
);
const lang = latest.tags.find((t) => t[0] === 'l')?.[1];
if (!lang) continue;
const slug = latest.tags.find((t) => t[0] === 'd')?.[1] ?? refs[i].dtag;
const title = latest.tags.find((t) => t[0] === 'title')?.[1] ?? '';
infos.push({ lang, slug, title });
}
return infos;
}
/**
* Loader: findet die anderssprachigen Varianten eines Posts.
* Liefert leere Liste, wenn keine a-Tags mit marker "translation" vorhanden.
*/
export async function loadTranslations(
event: NostrEvent
): Promise<TranslationInfo[]> {
const { parseTranslationRefs } = await import('./translations');
const refs = parseTranslationRefs(event);
if (refs.length === 0) return [];
const relays = get(readRelays);
return resolveTranslationsFromRefs(refs, (ref) =>
collectEvents(relays, {
kinds: [ref.kind],
authors: [ref.pubkey],
'#d': [ref.dtag],
limit: 1
})
);
}