feat(app): loadTranslations liefert sprach-varianten eines posts
This commit is contained in:
parent
8f4125fcc9
commit
7f48644dfc
|
|
@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue