@rmdes/indiekit-endpoint-activitypub 3.6.5 → 3.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/mastodon/entities/account.js +15 -0
- package/lib/mastodon/helpers/account-cache.js +51 -0
- package/lib/mastodon/helpers/resolve-account.js +9 -0
- package/lib/mastodon/routes/accounts.js +70 -61
- package/locales/de.json +358 -0
- package/locales/es-419.json +358 -0
- package/locales/es.json +358 -0
- package/locales/fr.json +358 -0
- package/locales/hi.json +358 -0
- package/locales/id.json +358 -0
- package/locales/it.json +358 -0
- package/locales/nl.json +358 -0
- package/locales/pl.json +358 -0
- package/locales/pt-BR.json +358 -0
- package/locales/pt.json +358 -0
- package/locales/sr.json +358 -0
- package/locales/sv.json +358 -0
- package/locales/zh-Hans-CN.json +358 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { accountId } from "../helpers/id-mapping.js";
|
|
9
9
|
import { sanitizeHtml, stripHtml } from "./sanitize.js";
|
|
10
|
+
import { getCachedAccountStats } from "../helpers/account-cache.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Serialize an actor as a Mastodon Account entity.
|
|
@@ -111,6 +112,20 @@ export function serializeAccount(actor, { baseUrl, isLocal = false, handle = ""
|
|
|
111
112
|
statuses_count: actor.statusesCount || 0,
|
|
112
113
|
followers_count: actor.followersCount || 0,
|
|
113
114
|
following_count: actor.followingCount || 0,
|
|
115
|
+
// Enrich from cache if counts are 0 (embedded accounts in statuses lack counts)
|
|
116
|
+
...((!actor.statusesCount && !actor.followersCount && !isLocal)
|
|
117
|
+
? (() => {
|
|
118
|
+
const cached = getCachedAccountStats(url);
|
|
119
|
+
return cached
|
|
120
|
+
? {
|
|
121
|
+
statuses_count: cached.statusesCount || 0,
|
|
122
|
+
followers_count: cached.followersCount || 0,
|
|
123
|
+
following_count: cached.followingCount || 0,
|
|
124
|
+
created_at: cached.createdAt || actor.createdAt || new Date().toISOString(),
|
|
125
|
+
}
|
|
126
|
+
: {};
|
|
127
|
+
})()
|
|
128
|
+
: {}),
|
|
114
129
|
moved: actor.movedTo || null,
|
|
115
130
|
suspended: false,
|
|
116
131
|
limited: false,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache for remote account stats (followers, following, statuses).
|
|
3
|
+
*
|
|
4
|
+
* Populated by resolveRemoteAccount() when a profile is fetched.
|
|
5
|
+
* Read by serializeAccount() to enrich embedded account objects in statuses.
|
|
6
|
+
*
|
|
7
|
+
* LRU-style with TTL — entries expire after 1 hour.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
11
|
+
const MAX_ENTRIES = 500;
|
|
12
|
+
|
|
13
|
+
// Map<actorUrl, { followersCount, followingCount, statusesCount, createdAt, cachedAt }>
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Store account stats in cache.
|
|
18
|
+
* @param {string} actorUrl - The actor's URL (cache key)
|
|
19
|
+
* @param {object} stats - { followersCount, followingCount, statusesCount, createdAt }
|
|
20
|
+
*/
|
|
21
|
+
export function cacheAccountStats(actorUrl, stats) {
|
|
22
|
+
if (!actorUrl) return;
|
|
23
|
+
|
|
24
|
+
// Evict oldest if at capacity
|
|
25
|
+
if (cache.size >= MAX_ENTRIES) {
|
|
26
|
+
const oldest = cache.keys().next().value;
|
|
27
|
+
cache.delete(oldest);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
cache.set(actorUrl, { ...stats, cachedAt: Date.now() });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get cached account stats.
|
|
35
|
+
* @param {string} actorUrl - The actor's URL
|
|
36
|
+
* @returns {object|null} Stats or null if not cached/expired
|
|
37
|
+
*/
|
|
38
|
+
export function getCachedAccountStats(actorUrl) {
|
|
39
|
+
if (!actorUrl) return null;
|
|
40
|
+
|
|
41
|
+
const entry = cache.get(actorUrl);
|
|
42
|
+
if (!entry) return null;
|
|
43
|
+
|
|
44
|
+
// Check TTL
|
|
45
|
+
if (Date.now() - entry.cachedAt > CACHE_TTL_MS) {
|
|
46
|
+
cache.delete(actorUrl);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Shared by accounts.js (lookup) and search.js (resolve=true).
|
|
6
6
|
*/
|
|
7
7
|
import { serializeAccount } from "../entities/account.js";
|
|
8
|
+
import { cacheAccountStats } from "./account-cache.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @param {string} acct - Account identifier (user@domain or URL)
|
|
@@ -115,6 +116,14 @@ export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) {
|
|
|
115
116
|
account.following_count = followingCount;
|
|
116
117
|
account.statuses_count = statusesCount;
|
|
117
118
|
|
|
119
|
+
// Cache stats so embedded account objects in statuses can use them
|
|
120
|
+
cacheAccountStats(actorUrl, {
|
|
121
|
+
followersCount,
|
|
122
|
+
followingCount,
|
|
123
|
+
statusesCount,
|
|
124
|
+
createdAt: published || undefined,
|
|
125
|
+
});
|
|
126
|
+
|
|
118
127
|
return account;
|
|
119
128
|
} catch (error) {
|
|
120
129
|
console.warn(`[Mastodon API] Remote account resolution failed for ${acct}:`, error.message);
|
|
@@ -155,6 +155,65 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => {
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
// ─── GET /api/v1/accounts/relationships ──────────────────────────────────────
|
|
159
|
+
// MUST be before /accounts/:id to prevent Express matching "relationships" as :id
|
|
160
|
+
|
|
161
|
+
router.get("/api/v1/accounts/relationships", async (req, res, next) => {
|
|
162
|
+
try {
|
|
163
|
+
let ids = req.query["id[]"] || req.query.id || [];
|
|
164
|
+
if (!Array.isArray(ids)) ids = [ids];
|
|
165
|
+
|
|
166
|
+
if (ids.length === 0) {
|
|
167
|
+
return res.json([]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const collections = req.app.locals.mastodonCollections;
|
|
171
|
+
|
|
172
|
+
const [followers, following, blocked, muted] = await Promise.all([
|
|
173
|
+
collections.ap_followers.find({}).toArray(),
|
|
174
|
+
collections.ap_following.find({}).toArray(),
|
|
175
|
+
collections.ap_blocked.find({}).toArray(),
|
|
176
|
+
collections.ap_muted.find({}).toArray(),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const followerIds = new Set(followers.map((f) => remoteActorId(f.actorUrl)));
|
|
180
|
+
const followingIds = new Set(following.map((f) => remoteActorId(f.actorUrl)));
|
|
181
|
+
const blockedIds = new Set(blocked.map((b) => remoteActorId(b.url)));
|
|
182
|
+
const mutedIds = new Set(muted.filter((m) => m.url).map((m) => remoteActorId(m.url)));
|
|
183
|
+
|
|
184
|
+
const relationships = ids.map((id) => ({
|
|
185
|
+
id,
|
|
186
|
+
following: followingIds.has(id),
|
|
187
|
+
showing_reblogs: followingIds.has(id),
|
|
188
|
+
notifying: false,
|
|
189
|
+
languages: [],
|
|
190
|
+
followed_by: followerIds.has(id),
|
|
191
|
+
blocking: blockedIds.has(id),
|
|
192
|
+
blocked_by: false,
|
|
193
|
+
muting: mutedIds.has(id),
|
|
194
|
+
muting_notifications: mutedIds.has(id),
|
|
195
|
+
requested: false,
|
|
196
|
+
requested_by: false,
|
|
197
|
+
domain_blocking: false,
|
|
198
|
+
endorsed: false,
|
|
199
|
+
note: "",
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
res.json(relationships);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
next(error);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ─── GET /api/v1/accounts/familiar_followers ─────────────────────────────────
|
|
209
|
+
// MUST be before /accounts/:id
|
|
210
|
+
|
|
211
|
+
router.get("/api/v1/accounts/familiar_followers", (req, res) => {
|
|
212
|
+
let ids = req.query["id[]"] || req.query.id || [];
|
|
213
|
+
if (!Array.isArray(ids)) ids = [ids];
|
|
214
|
+
res.json(ids.map((id) => ({ id, accounts: [] })));
|
|
215
|
+
});
|
|
216
|
+
|
|
158
217
|
// ─── GET /api/v1/accounts/:id ────────────────────────────────────────────────
|
|
159
218
|
|
|
160
219
|
router.get("/api/v1/accounts/:id", async (req, res, next) => {
|
|
@@ -183,8 +242,18 @@ router.get("/api/v1/accounts/:id", async (req, res, next) => {
|
|
|
183
242
|
// Resolve remote actor from followers, following, or timeline
|
|
184
243
|
const { actor, actorUrl } = await resolveActorData(id, collections);
|
|
185
244
|
if (actor) {
|
|
245
|
+
// Try remote resolution to get real counts (followers, following, statuses)
|
|
246
|
+
const remoteAccount = await resolveRemoteAccount(
|
|
247
|
+
actorUrl,
|
|
248
|
+
pluginOptions,
|
|
249
|
+
baseUrl,
|
|
250
|
+
);
|
|
251
|
+
if (remoteAccount) {
|
|
252
|
+
return res.json(remoteAccount);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fallback to local data
|
|
186
256
|
const account = serializeAccount(actor, { baseUrl });
|
|
187
|
-
// Count this actor's posts in our timeline
|
|
188
257
|
account.statuses_count = await collections.ap_timeline.countDocuments({
|
|
189
258
|
"author.url": actorUrl,
|
|
190
259
|
});
|
|
@@ -354,66 +423,6 @@ router.get("/api/v1/accounts/:id/following", async (req, res, next) => {
|
|
|
354
423
|
}
|
|
355
424
|
});
|
|
356
425
|
|
|
357
|
-
// ─── GET /api/v1/accounts/relationships ──────────────────────────────────────
|
|
358
|
-
|
|
359
|
-
router.get("/api/v1/accounts/relationships", async (req, res, next) => {
|
|
360
|
-
try {
|
|
361
|
-
// id[] can come as single value or array
|
|
362
|
-
let ids = req.query["id[]"] || req.query.id || [];
|
|
363
|
-
if (!Array.isArray(ids)) ids = [ids];
|
|
364
|
-
|
|
365
|
-
if (ids.length === 0) {
|
|
366
|
-
return res.json([]);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const collections = req.app.locals.mastodonCollections;
|
|
370
|
-
|
|
371
|
-
// Load all followers/following for efficient lookup
|
|
372
|
-
const [followers, following, blocked, muted] = await Promise.all([
|
|
373
|
-
collections.ap_followers.find({}).toArray(),
|
|
374
|
-
collections.ap_following.find({}).toArray(),
|
|
375
|
-
collections.ap_blocked.find({}).toArray(),
|
|
376
|
-
collections.ap_muted.find({}).toArray(),
|
|
377
|
-
]);
|
|
378
|
-
|
|
379
|
-
const followerIds = new Set(followers.map((f) => remoteActorId(f.actorUrl)));
|
|
380
|
-
const followingIds = new Set(following.map((f) => remoteActorId(f.actorUrl)));
|
|
381
|
-
const blockedIds = new Set(blocked.map((b) => remoteActorId(b.url)));
|
|
382
|
-
const mutedIds = new Set(muted.filter((m) => m.url).map((m) => remoteActorId(m.url)));
|
|
383
|
-
|
|
384
|
-
const relationships = ids.map((id) => ({
|
|
385
|
-
id,
|
|
386
|
-
following: followingIds.has(id),
|
|
387
|
-
showing_reblogs: followingIds.has(id),
|
|
388
|
-
notifying: false,
|
|
389
|
-
languages: [],
|
|
390
|
-
followed_by: followerIds.has(id),
|
|
391
|
-
blocking: blockedIds.has(id),
|
|
392
|
-
blocked_by: false,
|
|
393
|
-
muting: mutedIds.has(id),
|
|
394
|
-
muting_notifications: mutedIds.has(id),
|
|
395
|
-
requested: false,
|
|
396
|
-
requested_by: false,
|
|
397
|
-
domain_blocking: false,
|
|
398
|
-
endorsed: false,
|
|
399
|
-
note: "",
|
|
400
|
-
}));
|
|
401
|
-
|
|
402
|
-
res.json(relationships);
|
|
403
|
-
} catch (error) {
|
|
404
|
-
next(error);
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// ─── GET /api/v1/accounts/familiar_followers ─────────────────────────────────
|
|
409
|
-
|
|
410
|
-
router.get("/api/v1/accounts/familiar_followers", (req, res) => {
|
|
411
|
-
// Stub — returns empty for each requested ID
|
|
412
|
-
let ids = req.query["id[]"] || req.query.id || [];
|
|
413
|
-
if (!Array.isArray(ids)) ids = [ids];
|
|
414
|
-
res.json(ids.map((id) => ({ id, accounts: [] })));
|
|
415
|
-
});
|
|
416
|
-
|
|
417
426
|
// ─── POST /api/v1/accounts/:id/follow ───────────────────────────────────────
|
|
418
427
|
|
|
419
428
|
router.post("/api/v1/accounts/:id/follow", async (req, res, next) => {
|
package/locales/de.json
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
{
|
|
2
|
+
"activitypub": {
|
|
3
|
+
"title": "ActivityPub",
|
|
4
|
+
"followers": "Follower",
|
|
5
|
+
"following": "Folge ich",
|
|
6
|
+
"activities": "Aktivitätsprotokoll",
|
|
7
|
+
"featured": "Angeheftete Beiträge",
|
|
8
|
+
"featuredTags": "Vorgestellte Tags",
|
|
9
|
+
"recentActivity": "Neueste Aktivität",
|
|
10
|
+
"noActivity": "Noch keine Aktivität. Sobald dein Akteur föderiert ist, werden Interaktionen hier angezeigt.",
|
|
11
|
+
"noFollowers": "Noch keine Follower.",
|
|
12
|
+
"noFollowing": "Folgt noch niemandem.",
|
|
13
|
+
"pendingFollows": "Ausstehend",
|
|
14
|
+
"noPendingFollows": "Keine ausstehenden Folgeanfragen.",
|
|
15
|
+
"approve": "Genehmigen",
|
|
16
|
+
"reject": "Ablehnen",
|
|
17
|
+
"followApproved": "Folgeanfrage genehmigt.",
|
|
18
|
+
"followRejected": "Folgeanfrage abgelehnt.",
|
|
19
|
+
"followRequest": "möchte dir folgen",
|
|
20
|
+
"followerCount": "%d Follower",
|
|
21
|
+
"followerCount_plural": "%d Follower",
|
|
22
|
+
"followingCount": "%d folge ich",
|
|
23
|
+
"followedAt": "Gefolgt seit",
|
|
24
|
+
"source": "Quelle",
|
|
25
|
+
"sourceImport": "Mastodon-Import",
|
|
26
|
+
"sourceManual": "Manuell",
|
|
27
|
+
"sourceFederation": "Föderation",
|
|
28
|
+
"sourceRefollowPending": "Erneutes Folgen ausstehend",
|
|
29
|
+
"sourceRefollowFailed": "Erneutes Folgen fehlgeschlagen",
|
|
30
|
+
"direction": "Richtung",
|
|
31
|
+
"directionInbound": "Empfangen",
|
|
32
|
+
"directionOutbound": "Gesendet",
|
|
33
|
+
"profile": {
|
|
34
|
+
"title": "Profil",
|
|
35
|
+
"intro": "Bearbeite, wie dein Akteur anderen Nutzern im Fediverse angezeigt wird. Änderungen werden sofort wirksam.",
|
|
36
|
+
"nameLabel": "Anzeigename",
|
|
37
|
+
"nameHint": "Dein Name, wie er auf deinem Fediverse-Profil angezeigt wird",
|
|
38
|
+
"summaryLabel": "Biografie",
|
|
39
|
+
"summaryHint": "Eine kurze Beschreibung über dich. HTML ist erlaubt.",
|
|
40
|
+
"urlLabel": "Website-URL",
|
|
41
|
+
"urlHint": "Deine Webadresse, als Link auf deinem Profil angezeigt",
|
|
42
|
+
"iconLabel": "Avatar-URL",
|
|
43
|
+
"iconHint": "URL zu deinem Profilbild (quadratisch, mindestens 400x400px empfohlen)",
|
|
44
|
+
"imageLabel": "Header-Bild-URL",
|
|
45
|
+
"imageHint": "URL zu einem Bannerbild, das oben auf deinem Profil angezeigt wird",
|
|
46
|
+
"manualApprovalLabel": "Follower manuell genehmigen",
|
|
47
|
+
"manualApprovalHint": "Wenn aktiviert, müssen Folgeanfragen erst genehmigt werden, bevor sie wirksam werden",
|
|
48
|
+
"actorTypeLabel": "Akteur-Typ",
|
|
49
|
+
"actorTypeHint": "Wie dein Konto im Fediverse erscheint. Person für Einzelpersonen, Service für Bots oder automatisierte Konten, Organization für Gruppen oder Unternehmen.",
|
|
50
|
+
"linksLabel": "Profil-Links",
|
|
51
|
+
"linksHint": "Links auf deinem Fediverse-Profil. Füge deine Website, soziale Konten oder andere URLs hinzu. Seiten, die mit rel=\"me\" zurückverlinken, werden auf Mastodon als verifiziert angezeigt.",
|
|
52
|
+
"linkNameLabel": "Bezeichnung",
|
|
53
|
+
"linkValueLabel": "URL",
|
|
54
|
+
"addLink": "Link hinzufügen",
|
|
55
|
+
"removeLink": "Entfernen",
|
|
56
|
+
"authorizedFetchLabel": "Autorisiertes Abrufen erfordern (sicherer Modus)",
|
|
57
|
+
"authorizedFetchHint": "Wenn aktiviert, können nur Server mit gültigen HTTP-Signaturen deinen Akteur und Sammlungen abrufen. Dies verbessert die Privatsphäre, kann aber die Kompatibilität mit einigen Clients einschränken.",
|
|
58
|
+
"save": "Profil speichern",
|
|
59
|
+
"saved": "Profil gespeichert. Änderungen sind jetzt im Fediverse sichtbar.",
|
|
60
|
+
"public": {
|
|
61
|
+
"followPrompt": "Folge mir im Fediverse",
|
|
62
|
+
"copyHandle": "Handle kopieren",
|
|
63
|
+
"copied": "Kopiert!",
|
|
64
|
+
"pinnedPosts": "Angeheftete Beiträge",
|
|
65
|
+
"recentPosts": "Neueste Beiträge",
|
|
66
|
+
"joinedDate": "Beigetreten",
|
|
67
|
+
"posts": "Beiträge",
|
|
68
|
+
"followers": "Follower",
|
|
69
|
+
"following": "Folge ich",
|
|
70
|
+
"viewOnSite": "Auf der Website anzeigen"
|
|
71
|
+
},
|
|
72
|
+
"remote": {
|
|
73
|
+
"follow": "Folgen",
|
|
74
|
+
"unfollow": "Entfolgen",
|
|
75
|
+
"viewOn": "Ansehen auf",
|
|
76
|
+
"postsTitle": "Beiträge",
|
|
77
|
+
"noPosts": "Noch keine Beiträge von diesem Konto.",
|
|
78
|
+
"followToSee": "Folge diesem Konto, um dessen Beiträge in deiner Zeitleiste zu sehen.",
|
|
79
|
+
"notFound": "Dieses Konto konnte nicht gefunden werden. Es wurde möglicherweise gelöscht oder der Server ist nicht erreichbar."
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"migrate": {
|
|
83
|
+
"title": "Mastodon-Migration",
|
|
84
|
+
"intro": "Diese Anleitung führt dich durch den Umzug deiner Mastodon-Identität auf deine IndieWeb-Website. Führe jeden Schritt der Reihe nach aus — deine bestehenden Follower werden benachrichtigt und können dir automatisch erneut folgen.",
|
|
85
|
+
"step1Title": "Schritt 1 — Altes Konto verknüpfen",
|
|
86
|
+
"step1Desc": "Teile dem Fediverse mit, dass dein altes Mastodon-Konto und diese Website derselben Person gehören. Dies setzt die <code>alsoKnownAs</code>-Eigenschaft auf deinem ActivityPub-Akteur, die Mastodon überprüft, bevor ein Umzug erlaubt wird.",
|
|
87
|
+
"aliasLabel": "URL des alten Mastodon-Kontos",
|
|
88
|
+
"aliasHint": "Die vollständige URL deines Mastodon-Profils, z.B. https://mstdn.social/users/rmdes",
|
|
89
|
+
"aliasSave": "Alias speichern",
|
|
90
|
+
"aliasCurrent": "Aktueller Alias",
|
|
91
|
+
"aliasNone": "Noch kein Alias konfiguriert.",
|
|
92
|
+
"step2Title": "Schritt 2 — Soziales Netzwerk importieren",
|
|
93
|
+
"step2Desc": "Lade die CSV-Dateien aus deinem Mastodon-Datenexport hoch, um deine Verbindungen zu übernehmen. Gehe zu deiner Mastodon-Instanz → Einstellungen → Import und Export → Datenexport, um sie herunterzuladen.",
|
|
94
|
+
"importLegend": "Was importieren",
|
|
95
|
+
"fileLabel": "CSV-Datei",
|
|
96
|
+
"fileHint": "Wähle eine CSV-Datei aus deinem Mastodon-Datenexport (z.B. following_accounts.csv oder followers.csv)",
|
|
97
|
+
"importButton": "Importieren",
|
|
98
|
+
"importFollowing": "Folge-Liste",
|
|
99
|
+
"importFollowingHint": "Konten, denen du folgst — sie erscheinen sofort in deiner Folge-Liste",
|
|
100
|
+
"importFollowers": "Follower-Liste",
|
|
101
|
+
"importFollowersHint": "Deine aktuellen Follower — sie werden als ausstehend erfasst, bis sie dir nach dem Umzug in Schritt 3 erneut folgen",
|
|
102
|
+
"step3Title": "Schritt 3 — Konto umziehen",
|
|
103
|
+
"step3Desc": "Sobald du deinen Alias gespeichert und deine Daten importiert hast, gehe zu deiner Mastodon-Instanz → Einstellungen → Konto → <strong>Zu einem anderen Konto umziehen</strong>. Gib dein neues Fediverse-Handle ein und bestätige. Mastodon wird alle deine Follower benachrichtigen, und die, deren Server es unterstützen, werden dir hier automatisch erneut folgen. Dieser Schritt ist unwiderruflich — dein altes Konto wird zu einer Weiterleitung.",
|
|
104
|
+
"errorNoFile": "Bitte wähle eine CSV-Datei vor dem Import aus.",
|
|
105
|
+
"success": "%d Folge-ich, %d Follower importiert (%d fehlgeschlagen).",
|
|
106
|
+
"failedList": "Konnte nicht aufgelöst werden: %s",
|
|
107
|
+
"failedListSummary": "Fehlgeschlagene Handles",
|
|
108
|
+
"aliasSuccess": "Alias gespeichert — dein Akteur-Dokument enthält jetzt dieses Konto als alsoKnownAs."
|
|
109
|
+
},
|
|
110
|
+
"refollow": {
|
|
111
|
+
"title": "Stapel-Neufolgen",
|
|
112
|
+
"progress": "Neufolgen-Fortschritt",
|
|
113
|
+
"remaining": "Verbleibend",
|
|
114
|
+
"awaitingAccept": "Warte auf Bestätigung",
|
|
115
|
+
"accepted": "Bestätigt",
|
|
116
|
+
"failed": "Fehlgeschlagen",
|
|
117
|
+
"pause": "Pausieren",
|
|
118
|
+
"resume": "Fortsetzen",
|
|
119
|
+
"status": {
|
|
120
|
+
"idle": "Inaktiv",
|
|
121
|
+
"running": "Läuft",
|
|
122
|
+
"paused": "Pausiert",
|
|
123
|
+
"completed": "Abgeschlossen"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"moderation": {
|
|
127
|
+
"title": "Moderation",
|
|
128
|
+
"blockedTitle": "Blockierte Konten",
|
|
129
|
+
"mutedActorsTitle": "Stummgeschaltete Konten",
|
|
130
|
+
"mutedKeywordsTitle": "Stummgeschaltete Schlüsselwörter",
|
|
131
|
+
"noBlocked": "Keine blockierten Konten.",
|
|
132
|
+
"noMutedActors": "Keine stummgeschalteten Konten.",
|
|
133
|
+
"noMutedKeywords": "Keine stummgeschalteten Schlüsselwörter.",
|
|
134
|
+
"unblock": "Entblocken",
|
|
135
|
+
"unmute": "Stummschaltung aufheben",
|
|
136
|
+
"addKeywordTitle": "Stummgeschaltetes Schlüsselwort hinzufügen",
|
|
137
|
+
"keywordPlaceholder": "Schlüsselwort oder Phrase eingeben…",
|
|
138
|
+
"addKeyword": "Hinzufügen",
|
|
139
|
+
"muteActor": "Stummschalten",
|
|
140
|
+
"blockActor": "Blockieren",
|
|
141
|
+
"filterModeTitle": "Filtermodus",
|
|
142
|
+
"filterModeHint": "Wähle, wie stummgeschaltete Inhalte in deiner Zeitleiste behandelt werden. Blockierte Konten werden immer ausgeblendet.",
|
|
143
|
+
"filterModeHide": "Ausblenden — aus der Zeitleiste entfernen",
|
|
144
|
+
"filterModeWarn": "Warnen — hinter Inhaltswarnung anzeigen",
|
|
145
|
+
"cwMutedAccount": "Stummgeschaltetes Konto",
|
|
146
|
+
"cwMutedKeyword": "Stummgeschaltetes Schlüsselwort:",
|
|
147
|
+
"cwFiltered": "Gefilterter Inhalt"
|
|
148
|
+
},
|
|
149
|
+
"compose": {
|
|
150
|
+
"title": "Antwort verfassen",
|
|
151
|
+
"placeholder": "Schreibe deine Antwort…",
|
|
152
|
+
"syndicateLabel": "Syndizieren an",
|
|
153
|
+
"submitMicropub": "Antwort veröffentlichen",
|
|
154
|
+
"cancel": "Abbrechen",
|
|
155
|
+
"errorEmpty": "Antwortinhalt darf nicht leer sein",
|
|
156
|
+
"visibilityLabel": "Sichtbarkeit",
|
|
157
|
+
"visibilityPublic": "Öffentlich",
|
|
158
|
+
"visibilityUnlisted": "Nicht gelistet",
|
|
159
|
+
"visibilityFollowers": "Nur Follower",
|
|
160
|
+
"cwLabel": "Inhaltswarnung",
|
|
161
|
+
"cwPlaceholder": "Schreibe deine Warnung hier…"
|
|
162
|
+
},
|
|
163
|
+
"notifications": {
|
|
164
|
+
"title": "Benachrichtigungen",
|
|
165
|
+
"empty": "Noch keine Benachrichtigungen. Interaktionen von anderen Fediverse-Nutzern werden hier angezeigt.",
|
|
166
|
+
"liked": "hat deinen Beitrag geliked",
|
|
167
|
+
"boostedPost": "hat deinen Beitrag geboostet",
|
|
168
|
+
"followedYou": "folgt dir",
|
|
169
|
+
"repliedTo": "hat auf deinen Beitrag geantwortet",
|
|
170
|
+
"mentionedYou": "hat dich erwähnt",
|
|
171
|
+
"markAllRead": "Alle als gelesen markieren",
|
|
172
|
+
"clearAll": "Alle löschen",
|
|
173
|
+
"clearConfirm": "Alle Benachrichtigungen löschen? Dies kann nicht rückgängig gemacht werden.",
|
|
174
|
+
"dismiss": "Verwerfen",
|
|
175
|
+
"viewThread": "Thread anzeigen",
|
|
176
|
+
"tabs": {
|
|
177
|
+
"all": "Alle",
|
|
178
|
+
"replies": "Antworten",
|
|
179
|
+
"likes": "Likes",
|
|
180
|
+
"boosts": "Boosts",
|
|
181
|
+
"follows": "Folgt",
|
|
182
|
+
"dms": "DMs",
|
|
183
|
+
"reports": "Meldungen"
|
|
184
|
+
},
|
|
185
|
+
"emptyTab": "Noch keine %s-Benachrichtigungen."
|
|
186
|
+
},
|
|
187
|
+
"messages": {
|
|
188
|
+
"title": "Nachrichten",
|
|
189
|
+
"empty": "Noch keine Nachrichten. Direktnachrichten von anderen Fediverse-Nutzern werden hier angezeigt.",
|
|
190
|
+
"allConversations": "Alle Unterhaltungen",
|
|
191
|
+
"compose": "Neue Nachricht",
|
|
192
|
+
"send": "Nachricht senden",
|
|
193
|
+
"delete": "Löschen",
|
|
194
|
+
"markAllRead": "Alle als gelesen markieren",
|
|
195
|
+
"clearAll": "Alle löschen",
|
|
196
|
+
"clearConfirm": "Alle Nachrichten löschen? Dies kann nicht rückgängig gemacht werden.",
|
|
197
|
+
"recipientLabel": "An",
|
|
198
|
+
"recipientPlaceholder": "@benutzer@instanz.social",
|
|
199
|
+
"placeholder": "Schreibe deine Nachricht...",
|
|
200
|
+
"sentTo": "An",
|
|
201
|
+
"replyingTo": "Antwort an",
|
|
202
|
+
"sentYouDM": "hat dir eine Direktnachricht gesendet",
|
|
203
|
+
"viewMessage": "Nachricht anzeigen",
|
|
204
|
+
"errorEmpty": "Nachrichteninhalt darf nicht leer sein.",
|
|
205
|
+
"errorNoRecipient": "Bitte gib einen Empfänger ein.",
|
|
206
|
+
"errorRecipientNotFound": "Dieser Benutzer konnte nicht gefunden werden. Versuche ein vollständiges @benutzer@domain-Handle."
|
|
207
|
+
},
|
|
208
|
+
"reader": {
|
|
209
|
+
"title": "Leser",
|
|
210
|
+
"tabs": {
|
|
211
|
+
"all": "Alle",
|
|
212
|
+
"notes": "Notizen",
|
|
213
|
+
"articles": "Artikel",
|
|
214
|
+
"replies": "Antworten",
|
|
215
|
+
"boosts": "Boosts",
|
|
216
|
+
"media": "Medien"
|
|
217
|
+
},
|
|
218
|
+
"empty": "Deine Zeitleiste ist leer. Folge einigen Konten, um deren Beiträge hier zu sehen.",
|
|
219
|
+
"boosted": "geboostet",
|
|
220
|
+
"replyingTo": "Antwort an",
|
|
221
|
+
"showContent": "Inhalt anzeigen",
|
|
222
|
+
"hideContent": "Inhalt ausblenden",
|
|
223
|
+
"sensitiveContent": "Sensibler Inhalt",
|
|
224
|
+
"videoNotSupported": "Dein Browser unterstützt das Video-Element nicht.",
|
|
225
|
+
"audioNotSupported": "Dein Browser unterstützt das Audio-Element nicht.",
|
|
226
|
+
"actions": {
|
|
227
|
+
"reply": "Antworten",
|
|
228
|
+
"boost": "Boosten",
|
|
229
|
+
"unboost": "Boost rückgängig",
|
|
230
|
+
"like": "Liken",
|
|
231
|
+
"unlike": "Like rückgängig",
|
|
232
|
+
"viewOriginal": "Original anzeigen",
|
|
233
|
+
"liked": "Geliked",
|
|
234
|
+
"boosted": "Geboostet",
|
|
235
|
+
"likeError": "Dieser Beitrag konnte nicht geliked werden",
|
|
236
|
+
"boostError": "Dieser Beitrag konnte nicht geboostet werden"
|
|
237
|
+
},
|
|
238
|
+
"post": {
|
|
239
|
+
"title": "Beitragsdetails",
|
|
240
|
+
"notFound": "Beitrag nicht gefunden oder nicht mehr verfügbar.",
|
|
241
|
+
"openExternal": "Auf der Original-Instanz öffnen",
|
|
242
|
+
"parentPosts": "Thread",
|
|
243
|
+
"replies": "Antworten",
|
|
244
|
+
"back": "Zurück zur Zeitleiste",
|
|
245
|
+
"loadingThread": "Thread wird geladen...",
|
|
246
|
+
"threadError": "Vollständiger Thread konnte nicht geladen werden"
|
|
247
|
+
},
|
|
248
|
+
"resolve": {
|
|
249
|
+
"placeholder": "Füge eine Fediverse-URL oder ein @benutzer@domain-Handle ein…",
|
|
250
|
+
"label": "Fediverse-Beitrag oder -Konto nachschlagen",
|
|
251
|
+
"button": "Nachschlagen",
|
|
252
|
+
"notFoundTitle": "Nicht gefunden",
|
|
253
|
+
"notFound": "Dieser Beitrag oder dieses Konto konnte nicht gefunden werden. Die URL ist möglicherweise ungültig, der Server nicht erreichbar oder der Inhalt wurde gelöscht.",
|
|
254
|
+
"followersLabel": "Follower"
|
|
255
|
+
},
|
|
256
|
+
"linkPreview": {
|
|
257
|
+
"label": "Linkvorschau"
|
|
258
|
+
},
|
|
259
|
+
"explore": {
|
|
260
|
+
"title": "Entdecken",
|
|
261
|
+
"description": "Durchsuche öffentliche Zeitleisten von entfernten Mastodon-kompatiblen Instanzen.",
|
|
262
|
+
"instancePlaceholder": "Gib einen Instanz-Hostnamen ein, z.B. mastodon.social",
|
|
263
|
+
"browse": "Durchsuchen",
|
|
264
|
+
"local": "Lokal",
|
|
265
|
+
"federated": "Föderiert",
|
|
266
|
+
"loadError": "Zeitleiste von dieser Instanz konnte nicht geladen werden. Sie ist möglicherweise nicht erreichbar oder unterstützt die Mastodon-API nicht.",
|
|
267
|
+
"timeout": "Zeitüberschreitung. Die Instanz ist möglicherweise langsam oder nicht erreichbar.",
|
|
268
|
+
"noResults": "Keine Beiträge auf der öffentlichen Zeitleiste dieser Instanz gefunden.",
|
|
269
|
+
"invalidInstance": "Ungültiger Instanz-Hostname. Bitte gib einen gültigen Domainnamen ein.",
|
|
270
|
+
"mauLabel": "MAU",
|
|
271
|
+
"timelineSupported": "Öffentliche Zeitleiste verfügbar",
|
|
272
|
+
"timelineUnsupported": "Öffentliche Zeitleiste nicht verfügbar",
|
|
273
|
+
"hashtagLabel": "Hashtag (optional)",
|
|
274
|
+
"hashtagPlaceholder": "z.B. indieweb",
|
|
275
|
+
"hashtagHint": "Ergebnisse nach einem bestimmten Hashtag filtern",
|
|
276
|
+
"tabs": {
|
|
277
|
+
"label": "Entdecken-Tabs",
|
|
278
|
+
"search": "Suchen",
|
|
279
|
+
"pinAsTab": "Als Tab anheften",
|
|
280
|
+
"pinned": "Angeheftet",
|
|
281
|
+
"remove": "Tab entfernen",
|
|
282
|
+
"moveUp": "Nach oben",
|
|
283
|
+
"moveDown": "Nach unten",
|
|
284
|
+
"addHashtag": "Hashtag-Tab hinzufügen",
|
|
285
|
+
"hashtagTabPlaceholder": "Hashtag eingeben",
|
|
286
|
+
"addTab": "Hinzufügen",
|
|
287
|
+
"retry": "Erneut versuchen",
|
|
288
|
+
"noInstances": "Hefte zuerst einige Instanzen an, um Hashtag-Tabs zu verwenden.",
|
|
289
|
+
"sources": "Suche #%s auf %d Instanz",
|
|
290
|
+
"sources_plural": "Suche #%s auf %d Instanzen",
|
|
291
|
+
"sourcesPartial": "%d von %d Instanzen haben geantwortet"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"tagTimeline": {
|
|
295
|
+
"postsTagged": "%d Beitrag",
|
|
296
|
+
"postsTagged_plural": "%d Beiträge",
|
|
297
|
+
"noPosts": "Keine Beiträge mit #%s in deiner Zeitleiste gefunden.",
|
|
298
|
+
"followTag": "Hashtag folgen",
|
|
299
|
+
"unfollowTag": "Hashtag entfolgen",
|
|
300
|
+
"following": "Folge ich"
|
|
301
|
+
},
|
|
302
|
+
"pagination": {
|
|
303
|
+
"newer": "← Neuere",
|
|
304
|
+
"older": "Ältere →",
|
|
305
|
+
"loadMore": "Mehr laden",
|
|
306
|
+
"loading": "Lädt…",
|
|
307
|
+
"noMore": "Du bist auf dem neuesten Stand."
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
"myProfile": {
|
|
311
|
+
"title": "Mein Profil",
|
|
312
|
+
"posts": "Beiträge",
|
|
313
|
+
"editProfile": "Profil bearbeiten",
|
|
314
|
+
"empty": "Hier ist noch nichts.",
|
|
315
|
+
"tabs": {
|
|
316
|
+
"posts": "Beiträge",
|
|
317
|
+
"replies": "Antworten",
|
|
318
|
+
"likes": "Likes",
|
|
319
|
+
"boosts": "Boosts"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
"poll": {
|
|
323
|
+
"voters": "Abstimmende",
|
|
324
|
+
"votes": "Stimmen",
|
|
325
|
+
"closed": "Abstimmung beendet",
|
|
326
|
+
"endsAt": "Endet"
|
|
327
|
+
},
|
|
328
|
+
"federation": {
|
|
329
|
+
"deleteSuccess": "Löschaktivität an Follower gesendet",
|
|
330
|
+
"deleteButton": "Aus dem Fediverse löschen"
|
|
331
|
+
},
|
|
332
|
+
"federationMgmt": {
|
|
333
|
+
"title": "Föderation",
|
|
334
|
+
"collections": "Sammlungszustand",
|
|
335
|
+
"quickActions": "Schnellaktionen",
|
|
336
|
+
"broadcastActor": "Akteur-Update senden",
|
|
337
|
+
"debugDashboard": "Debug-Dashboard",
|
|
338
|
+
"objectLookup": "Objekt-Nachschlag",
|
|
339
|
+
"lookupPlaceholder": "URL oder @benutzer@domain-Handle…",
|
|
340
|
+
"lookup": "Nachschlagen",
|
|
341
|
+
"lookupLoading": "Wird aufgelöst…",
|
|
342
|
+
"postActions": "Beitrags-Föderation",
|
|
343
|
+
"viewJson": "JSON",
|
|
344
|
+
"rebroadcast": "Create-Aktivität erneut senden",
|
|
345
|
+
"rebroadcastShort": "Erneut senden",
|
|
346
|
+
"broadcastDelete": "Delete-Aktivität senden",
|
|
347
|
+
"deleteShort": "Löschen",
|
|
348
|
+
"noPosts": "Keine Beiträge gefunden.",
|
|
349
|
+
"apJsonTitle": "ActivityStreams JSON-LD",
|
|
350
|
+
"recentActivity": "Neueste Aktivität",
|
|
351
|
+
"viewAllActivities": "Alle Aktivitäten anzeigen →"
|
|
352
|
+
},
|
|
353
|
+
"reports": {
|
|
354
|
+
"sentReport": "hat eine Meldung eingereicht",
|
|
355
|
+
"title": "Meldungen"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|