@rmdes/indiekit-endpoint-blogroll 1.0.19 → 1.0.21
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/index.js +3 -0
- package/lib/controllers/sources.js +58 -0
- package/lib/storage/sources.js +6 -2
- package/lib/sync/feedland.js +139 -0
- package/lib/sync/scheduler.js +4 -1
- package/locales/de.json +20 -11
- package/locales/en.json +20 -11
- package/locales/es-419.json +20 -11
- package/locales/es.json +20 -11
- package/locales/fr.json +20 -11
- package/locales/hi.json +20 -11
- package/locales/id.json +20 -11
- package/locales/it.json +20 -11
- package/locales/nl.json +20 -11
- package/locales/pl.json +20 -11
- package/locales/pt-BR.json +20 -11
- package/locales/pt.json +20 -11
- package/locales/sr.json +20 -11
- package/locales/sv.json +20 -11
- package/locales/zh-Hans-CN.json +20 -11
- package/package.json +1 -1
- package/views/blogroll-source-edit.njk +92 -0
package/index.js
CHANGED
|
@@ -93,6 +93,9 @@ export default class BlogrollEndpoint {
|
|
|
93
93
|
protectedRouter.post("/api/microsub-webhook", apiController.microsubWebhook);
|
|
94
94
|
protectedRouter.get("/api/microsub-status", apiController.microsubStatus);
|
|
95
95
|
|
|
96
|
+
// FeedLand integration (protected - category discovery)
|
|
97
|
+
protectedRouter.get("/api/feedland-categories", sourcesController.feedlandCategories);
|
|
98
|
+
|
|
96
99
|
return protectedRouter;
|
|
97
100
|
}
|
|
98
101
|
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
getMicrosubChannels,
|
|
17
17
|
isMicrosubAvailable,
|
|
18
18
|
} from "../sync/microsub.js";
|
|
19
|
+
import {
|
|
20
|
+
syncFeedlandSource,
|
|
21
|
+
fetchFeedlandCategories,
|
|
22
|
+
} from "../sync/feedland.js";
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* List sources
|
|
@@ -95,6 +99,9 @@ async function create(request, response) {
|
|
|
95
99
|
enabled,
|
|
96
100
|
channelFilter,
|
|
97
101
|
categoryPrefix,
|
|
102
|
+
feedlandInstance,
|
|
103
|
+
feedlandUsername,
|
|
104
|
+
feedlandCategory,
|
|
98
105
|
} = request.body;
|
|
99
106
|
|
|
100
107
|
try {
|
|
@@ -120,6 +127,13 @@ async function create(request, response) {
|
|
|
120
127
|
return response.redirect(`${request.baseUrl}/sources/new`);
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
if (type === "feedland" && (!feedlandInstance || !feedlandUsername)) {
|
|
131
|
+
request.session.messages = [
|
|
132
|
+
{ type: "error", content: request.__("blogroll.sources.form.feedlandRequired") },
|
|
133
|
+
];
|
|
134
|
+
return response.redirect(`${request.baseUrl}/sources/new`);
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
const sourceData = {
|
|
124
138
|
name,
|
|
125
139
|
type,
|
|
@@ -135,12 +149,21 @@ async function create(request, response) {
|
|
|
135
149
|
sourceData.categoryPrefix = categoryPrefix || "";
|
|
136
150
|
}
|
|
137
151
|
|
|
152
|
+
// Add feedland-specific fields
|
|
153
|
+
if (type === "feedland") {
|
|
154
|
+
sourceData.feedlandInstance = feedlandInstance.replace(/\/+$/, "");
|
|
155
|
+
sourceData.feedlandUsername = feedlandUsername;
|
|
156
|
+
sourceData.feedlandCategory = feedlandCategory || null;
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
const source = await createSource(application, sourceData);
|
|
139
160
|
|
|
140
161
|
// Trigger initial sync based on source type
|
|
141
162
|
try {
|
|
142
163
|
if (type === "microsub") {
|
|
143
164
|
await syncMicrosubSource(application, source);
|
|
165
|
+
} else if (type === "feedland") {
|
|
166
|
+
await syncFeedlandSource(application, source);
|
|
144
167
|
} else {
|
|
145
168
|
await syncOpmlSource(application, source);
|
|
146
169
|
}
|
|
@@ -223,6 +246,9 @@ async function update(request, response) {
|
|
|
223
246
|
enabled,
|
|
224
247
|
channelFilter,
|
|
225
248
|
categoryPrefix,
|
|
249
|
+
feedlandInstance,
|
|
250
|
+
feedlandUsername,
|
|
251
|
+
feedlandCategory,
|
|
226
252
|
} = request.body;
|
|
227
253
|
|
|
228
254
|
try {
|
|
@@ -247,6 +273,15 @@ async function update(request, response) {
|
|
|
247
273
|
updateData.categoryPrefix = categoryPrefix || "";
|
|
248
274
|
}
|
|
249
275
|
|
|
276
|
+
// Add feedland-specific fields
|
|
277
|
+
if (type === "feedland") {
|
|
278
|
+
updateData.feedlandInstance = feedlandInstance
|
|
279
|
+
? feedlandInstance.replace(/\/+$/, "")
|
|
280
|
+
: null;
|
|
281
|
+
updateData.feedlandUsername = feedlandUsername || null;
|
|
282
|
+
updateData.feedlandCategory = feedlandCategory || null;
|
|
283
|
+
}
|
|
284
|
+
|
|
250
285
|
await updateSource(application, id, updateData);
|
|
251
286
|
|
|
252
287
|
request.session.messages = [
|
|
@@ -313,6 +348,8 @@ async function sync(request, response) {
|
|
|
313
348
|
let result;
|
|
314
349
|
if (source.type === "microsub") {
|
|
315
350
|
result = await syncMicrosubSource(application, source);
|
|
351
|
+
} else if (source.type === "feedland") {
|
|
352
|
+
result = await syncFeedlandSource(application, source);
|
|
316
353
|
} else {
|
|
317
354
|
result = await syncOpmlSource(application, source);
|
|
318
355
|
}
|
|
@@ -358,6 +395,26 @@ function consumeFlashMessage(request) {
|
|
|
358
395
|
return result;
|
|
359
396
|
}
|
|
360
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Fetch FeedLand categories (AJAX endpoint)
|
|
400
|
+
* GET /api/feedland-categories?instance=...&username=...
|
|
401
|
+
*/
|
|
402
|
+
async function feedlandCategories(request, response) {
|
|
403
|
+
const { instance, username } = request.query;
|
|
404
|
+
|
|
405
|
+
if (!instance || !username) {
|
|
406
|
+
return response.status(400).json({ error: "instance and username are required" });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const data = await fetchFeedlandCategories(instance, username);
|
|
411
|
+
response.json(data);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error("[Blogroll] FeedLand categories fetch error:", error.message);
|
|
414
|
+
response.status(502).json({ error: error.message });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
361
418
|
export const sourcesController = {
|
|
362
419
|
list,
|
|
363
420
|
newForm,
|
|
@@ -366,4 +423,5 @@ export const sourcesController = {
|
|
|
366
423
|
update,
|
|
367
424
|
remove,
|
|
368
425
|
sync,
|
|
426
|
+
feedlandCategories,
|
|
369
427
|
};
|
package/lib/storage/sources.js
CHANGED
|
@@ -48,13 +48,17 @@ export async function createSource(application, data) {
|
|
|
48
48
|
const now = new Date().toISOString();
|
|
49
49
|
|
|
50
50
|
const source = {
|
|
51
|
-
type: data.type, // "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub"
|
|
51
|
+
type: data.type, // "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub" | "feedland"
|
|
52
52
|
name: data.name,
|
|
53
53
|
url: data.url || null,
|
|
54
54
|
opmlContent: data.opmlContent || null,
|
|
55
55
|
// Microsub-specific fields
|
|
56
56
|
channelFilter: data.channelFilter || null,
|
|
57
57
|
categoryPrefix: data.categoryPrefix || "",
|
|
58
|
+
// FeedLand-specific fields
|
|
59
|
+
feedlandInstance: data.feedlandInstance || null,
|
|
60
|
+
feedlandUsername: data.feedlandUsername || null,
|
|
61
|
+
feedlandCategory: data.feedlandCategory || null,
|
|
58
62
|
enabled: data.enabled !== false,
|
|
59
63
|
syncInterval: data.syncInterval || 60, // minutes
|
|
60
64
|
lastSyncAt: null,
|
|
@@ -160,7 +164,7 @@ export async function getSourcesDueForSync(application) {
|
|
|
160
164
|
return collection
|
|
161
165
|
.find({
|
|
162
166
|
enabled: true,
|
|
163
|
-
type: { $in: ["opml_url", "json_feed", "microsub"] },
|
|
167
|
+
type: { $in: ["opml_url", "json_feed", "microsub", "feedland"] },
|
|
164
168
|
$or: [
|
|
165
169
|
{ lastSyncAt: null },
|
|
166
170
|
{
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeedLand synchronization
|
|
3
|
+
* @module sync/feedland
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fetchAndParseOpml } from "./opml.js";
|
|
7
|
+
import { upsertBlog } from "../storage/blogs.js";
|
|
8
|
+
import { updateSourceSyncStatus } from "../storage/sources.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fetch user categories from a FeedLand instance
|
|
12
|
+
* @param {string} instanceUrl - FeedLand instance URL (e.g., https://feedland.com)
|
|
13
|
+
* @param {string} username - FeedLand username
|
|
14
|
+
* @param {number} timeout - Fetch timeout in ms
|
|
15
|
+
* @returns {Promise<object>} Category data { categories: string[], homePageCategories: string[] }
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchFeedlandCategories(instanceUrl, username, timeout = 10000) {
|
|
18
|
+
const baseUrl = instanceUrl.replace(/\/+$/, "");
|
|
19
|
+
const url = `${baseUrl}/getusercategories?screenname=${encodeURIComponent(username)}`;
|
|
20
|
+
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
headers: {
|
|
28
|
+
"User-Agent": "Indiekit-Blogroll/1.0",
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
|
|
41
|
+
// FeedLand returns comma-separated strings
|
|
42
|
+
const categories = data.categories
|
|
43
|
+
? data.categories.split(",").map((c) => c.trim()).filter(Boolean)
|
|
44
|
+
: [];
|
|
45
|
+
const homePageCategories = data.homePageCategories
|
|
46
|
+
? data.homePageCategories.split(",").map((c) => c.trim()).filter(Boolean)
|
|
47
|
+
: [];
|
|
48
|
+
|
|
49
|
+
return { categories, homePageCategories, screenname: data.screenname };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
clearTimeout(timeoutId);
|
|
52
|
+
if (error.name === "AbortError") {
|
|
53
|
+
throw new Error("Request timed out");
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the OPML URL for a FeedLand source
|
|
61
|
+
* @param {object} source - Source document with feedland fields
|
|
62
|
+
* @returns {string} OPML URL
|
|
63
|
+
*/
|
|
64
|
+
export function buildFeedlandOpmlUrl(source) {
|
|
65
|
+
const baseUrl = source.feedlandInstance.replace(/\/+$/, "");
|
|
66
|
+
let url = `${baseUrl}/opml?screenname=${encodeURIComponent(source.feedlandUsername)}`;
|
|
67
|
+
|
|
68
|
+
if (source.feedlandCategory) {
|
|
69
|
+
url += `&catname=${encodeURIComponent(source.feedlandCategory)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return url;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build the FeedLand river URL for linking back
|
|
77
|
+
* @param {object} source - Source document with feedland fields
|
|
78
|
+
* @returns {string} River URL
|
|
79
|
+
*/
|
|
80
|
+
export function buildFeedlandRiverUrl(source) {
|
|
81
|
+
const baseUrl = source.feedlandInstance.replace(/\/+$/, "");
|
|
82
|
+
return `${baseUrl}/?river=true&screenname=${encodeURIComponent(source.feedlandUsername)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sync blogs from a FeedLand source
|
|
87
|
+
* @param {object} application - Application instance
|
|
88
|
+
* @param {object} source - Source document
|
|
89
|
+
* @returns {Promise<object>} Sync result
|
|
90
|
+
*/
|
|
91
|
+
export async function syncFeedlandSource(application, source) {
|
|
92
|
+
try {
|
|
93
|
+
const opmlUrl = buildFeedlandOpmlUrl(source);
|
|
94
|
+
const blogs = await fetchAndParseOpml(opmlUrl);
|
|
95
|
+
|
|
96
|
+
let added = 0;
|
|
97
|
+
let updated = 0;
|
|
98
|
+
|
|
99
|
+
for (const blog of blogs) {
|
|
100
|
+
// FeedLand OPML includes a category attribute with comma-separated categories.
|
|
101
|
+
// Use the first category, or fall back to the source's feedlandCategory filter,
|
|
102
|
+
// or use the FeedLand username as a category grouping.
|
|
103
|
+
const category = blog.category
|
|
104
|
+
|| source.feedlandCategory
|
|
105
|
+
|| source.feedlandUsername
|
|
106
|
+
|| "";
|
|
107
|
+
|
|
108
|
+
const result = await upsertBlog(application, {
|
|
109
|
+
...blog,
|
|
110
|
+
category,
|
|
111
|
+
sourceId: source._id,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (result.upserted) added++;
|
|
115
|
+
else if (result.modified) updated++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Update source sync status
|
|
119
|
+
await updateSourceSyncStatus(application, source._id, { success: true });
|
|
120
|
+
|
|
121
|
+
console.log(
|
|
122
|
+
`[Blogroll] Synced FeedLand source "${source.name}" (${source.feedlandUsername}@${source.feedlandInstance}): ${added} added, ${updated} updated, ${blogs.length} total`
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return { success: true, added, updated, total: blogs.length };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Update source with error status
|
|
128
|
+
await updateSourceSyncStatus(application, source._id, {
|
|
129
|
+
success: false,
|
|
130
|
+
error: error.message,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.error(
|
|
134
|
+
`[Blogroll] FeedLand sync failed for "${source.name}":`,
|
|
135
|
+
error.message
|
|
136
|
+
);
|
|
137
|
+
return { success: false, error: error.message };
|
|
138
|
+
}
|
|
139
|
+
}
|
package/lib/sync/scheduler.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getBlogs, countBlogs } from "../storage/blogs.js";
|
|
|
8
8
|
import { countItems, deleteOldItems } from "../storage/items.js";
|
|
9
9
|
import { syncOpmlSource } from "./opml.js";
|
|
10
10
|
import { syncMicrosubSource } from "./microsub.js";
|
|
11
|
+
import { syncFeedlandSource } from "./feedland.js";
|
|
11
12
|
import { syncBlogItems } from "./feed.js";
|
|
12
13
|
|
|
13
14
|
let syncInterval = null;
|
|
@@ -42,7 +43,7 @@ export async function runFullSync(application, options = {}) {
|
|
|
42
43
|
// Sync all enabled sources (OPML, JSON, Microsub)
|
|
43
44
|
const sources = await getSources(application);
|
|
44
45
|
const enabledSources = sources.filter(
|
|
45
|
-
(s) => s.enabled && ["opml_url", "opml_file", "json_feed", "microsub"].includes(s.type)
|
|
46
|
+
(s) => s.enabled && ["opml_url", "opml_file", "json_feed", "microsub", "feedland"].includes(s.type)
|
|
46
47
|
);
|
|
47
48
|
|
|
48
49
|
let sourcesSuccess = 0;
|
|
@@ -53,6 +54,8 @@ export async function runFullSync(application, options = {}) {
|
|
|
53
54
|
let result;
|
|
54
55
|
if (source.type === "microsub") {
|
|
55
56
|
result = await syncMicrosubSource(application, source);
|
|
57
|
+
} else if (source.type === "feedland") {
|
|
58
|
+
result = await syncFeedlandSource(application, source);
|
|
56
59
|
} else {
|
|
57
60
|
result = await syncOpmlSource(application, source);
|
|
58
61
|
}
|
package/locales/de.json
CHANGED
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
"sync": {
|
|
30
|
-
"success": "Synced
|
|
31
|
-
"error": "Sync failed:
|
|
30
|
+
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
|
+
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
33
|
-
"cleared_success": "Cleared and re-synced
|
|
33
|
+
"cleared_success": "Cleared and re-synced {{blogs}} blogs, added {{items}} items."
|
|
34
34
|
},
|
|
35
35
|
|
|
36
36
|
"errors": {
|
|
37
37
|
"title": "Blogs mit Fehlern",
|
|
38
|
-
"seeAll": "Alle
|
|
38
|
+
"seeAll": "Alle {{count}} Blogs mit Fehlern anzeigen"
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
"sources": {
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"save": "Speichern",
|
|
49
49
|
"empty": "Keine OPML-Quellen konfiguriert. Verwenden Sie diese Funktion, um Blogs aus FreshRSS oder anderen Feed-Readern zu importieren.",
|
|
50
50
|
"recent": "OPML-Quellen",
|
|
51
|
-
"interval": "Alle
|
|
51
|
+
"interval": "Alle {{minutes}} Min.",
|
|
52
52
|
"lastSync": "Zuletzt synchronisiert",
|
|
53
53
|
"deleteConfirm": "Diese OPML-Quelle löschen? Importierte Blogs bleiben erhalten.",
|
|
54
54
|
"created": "OPML-Quelle erfolgreich erstellt.",
|
|
55
55
|
"created_synced": "OPML-Quelle erfolgreich erstellt und synchronisiert.",
|
|
56
|
-
"created_sync_failed": "OPML-Quelle erstellt, aber Synchronisation fehlgeschlagen:
|
|
56
|
+
"created_sync_failed": "OPML-Quelle erstellt, aber Synchronisation fehlgeschlagen: {{error}}",
|
|
57
57
|
"updated": "OPML-Quelle erfolgreich aktualisiert.",
|
|
58
58
|
"deleted": "OPML-Quelle erfolgreich gelöscht.",
|
|
59
|
-
"synced": "Erfolgreich synchronisiert. Hinzugefügt:
|
|
59
|
+
"synced": "Erfolgreich synchronisiert. Hinzugefügt: {{added}}, Aktualisiert: {{updated}}",
|
|
60
60
|
"form": {
|
|
61
61
|
"name": "Name",
|
|
62
62
|
"type": "Import-Typ",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
|
@@ -96,11 +105,11 @@
|
|
|
96
105
|
"clearFilters": "Filter löschen",
|
|
97
106
|
"deleteConfirm": "Diesen Blog und alle zwischengespeicherten Einträge löschen?",
|
|
98
107
|
"created": "Blog erfolgreich hinzugefügt.",
|
|
99
|
-
"created_synced": "Blog hinzugefügt und synchronisiert.
|
|
100
|
-
"created_sync_failed": "Blog hinzugefügt, aber erster Abruf fehlgeschlagen:
|
|
108
|
+
"created_synced": "Blog hinzugefügt und synchronisiert. {{items}} Einträge abgerufen.",
|
|
109
|
+
"created_sync_failed": "Blog hinzugefügt, aber erster Abruf fehlgeschlagen: {{error}}",
|
|
101
110
|
"updated": "Blog erfolgreich aktualisiert.",
|
|
102
111
|
"deleted": "Blog erfolgreich gelöscht.",
|
|
103
|
-
"refreshed": "Blog aktualisiert.
|
|
112
|
+
"refreshed": "Blog aktualisiert. {{items}} neue Einträge hinzugefügt.",
|
|
104
113
|
"form": {
|
|
105
114
|
"discoverUrl": "Website-URL",
|
|
106
115
|
"discover": "Feed entdecken",
|
package/locales/en.json
CHANGED
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
"sync": {
|
|
30
|
-
"success": "Synced
|
|
31
|
-
"error": "Sync failed:
|
|
30
|
+
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
|
+
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
33
|
-
"cleared_success": "Cleared and re-synced
|
|
33
|
+
"cleared_success": "Cleared and re-synced {{blogs}} blogs, added {{items}} items."
|
|
34
34
|
},
|
|
35
35
|
|
|
36
36
|
"errors": {
|
|
37
37
|
"title": "Blogs with Errors",
|
|
38
|
-
"seeAll": "See all
|
|
38
|
+
"seeAll": "See all {{count}} blogs with errors"
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
"sources": {
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"save": "Save",
|
|
49
49
|
"empty": "No OPML sources configured. Use this to bulk-import blogs from FreshRSS or other feed readers.",
|
|
50
50
|
"recent": "OPML Sources",
|
|
51
|
-
"interval": "Every
|
|
51
|
+
"interval": "Every {{minutes}} min",
|
|
52
52
|
"lastSync": "Last synced",
|
|
53
53
|
"deleteConfirm": "Delete this OPML source? Blogs imported from it will remain.",
|
|
54
54
|
"created": "OPML source created successfully.",
|
|
55
55
|
"created_synced": "OPML source created and synced successfully.",
|
|
56
|
-
"created_sync_failed": "OPML source created, but sync failed:
|
|
56
|
+
"created_sync_failed": "OPML source created, but sync failed: {{error}}",
|
|
57
57
|
"updated": "OPML source updated successfully.",
|
|
58
58
|
"deleted": "OPML source deleted successfully.",
|
|
59
|
-
"synced": "Synced successfully. Added:
|
|
59
|
+
"synced": "Synced successfully. Added: {{added}}, Updated: {{updated}}",
|
|
60
60
|
"form": {
|
|
61
61
|
"name": "Name",
|
|
62
62
|
"type": "Import Type",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
|
@@ -96,11 +105,11 @@
|
|
|
96
105
|
"clearFilters": "Clear filters",
|
|
97
106
|
"deleteConfirm": "Delete this blog and all its cached items?",
|
|
98
107
|
"created": "Blog added successfully.",
|
|
99
|
-
"created_synced": "Blog added and synced. Fetched
|
|
100
|
-
"created_sync_failed": "Blog added, but initial fetch failed:
|
|
108
|
+
"created_synced": "Blog added and synced. Fetched {{items}} items.",
|
|
109
|
+
"created_sync_failed": "Blog added, but initial fetch failed: {{error}}",
|
|
101
110
|
"updated": "Blog updated successfully.",
|
|
102
111
|
"deleted": "Blog deleted successfully.",
|
|
103
|
-
"refreshed": "Blog refreshed. Added
|
|
112
|
+
"refreshed": "Blog refreshed. Added {{items}} new items.",
|
|
104
113
|
"form": {
|
|
105
114
|
"discoverUrl": "Website URL",
|
|
106
115
|
"discover": "Discover Feed",
|
package/locales/es-419.json
CHANGED
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
"sync": {
|
|
30
|
-
"success": "Synced
|
|
31
|
-
"error": "Sync failed:
|
|
30
|
+
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
|
+
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
33
|
-
"cleared_success": "Cleared and re-synced
|
|
33
|
+
"cleared_success": "Cleared and re-synced {{blogs}} blogs, added {{items}} items."
|
|
34
34
|
},
|
|
35
35
|
|
|
36
36
|
"errors": {
|
|
37
37
|
"title": "Blogs con errores",
|
|
38
|
-
"seeAll": "Ver los
|
|
38
|
+
"seeAll": "Ver los {{count}} blogs con errores"
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
"sources": {
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"save": "Guardar",
|
|
49
49
|
"empty": "No hay fuentes OPML configuradas. Usa esto para importar blogs de forma masiva desde FreshRSS u otros lectores de feeds.",
|
|
50
50
|
"recent": "Fuentes OPML",
|
|
51
|
-
"interval": "Cada
|
|
51
|
+
"interval": "Cada {{minutes}} min",
|
|
52
52
|
"lastSync": "Última sincronización",
|
|
53
53
|
"deleteConfirm": "¿Eliminar esta fuente OPML? Los blogs importados se conservarán.",
|
|
54
54
|
"created": "Fuente OPML creada exitosamente.",
|
|
55
55
|
"created_synced": "Fuente OPML creada y sincronizada exitosamente.",
|
|
56
|
-
"created_sync_failed": "Fuente OPML creada, pero la sincronización falló:
|
|
56
|
+
"created_sync_failed": "Fuente OPML creada, pero la sincronización falló: {{error}}",
|
|
57
57
|
"updated": "Fuente OPML actualizada exitosamente.",
|
|
58
58
|
"deleted": "Fuente OPML eliminada exitosamente.",
|
|
59
|
-
"synced": "Sincronización exitosa. Agregados:
|
|
59
|
+
"synced": "Sincronización exitosa. Agregados: {{added}}, Actualizados: {{updated}}",
|
|
60
60
|
"form": {
|
|
61
61
|
"name": "Nombre",
|
|
62
62
|
"type": "Tipo de importación",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
|
@@ -96,11 +105,11 @@
|
|
|
96
105
|
"clearFilters": "Limpiar filtros",
|
|
97
106
|
"deleteConfirm": "¿Eliminar este blog y todas sus entradas almacenadas?",
|
|
98
107
|
"created": "Blog agregado exitosamente.",
|
|
99
|
-
"created_synced": "Blog agregado y sincronizado. Se descargaron
|
|
100
|
-
"created_sync_failed": "Blog agregado, pero la descarga inicial falló:
|
|
108
|
+
"created_synced": "Blog agregado y sincronizado. Se descargaron {{items}} entradas.",
|
|
109
|
+
"created_sync_failed": "Blog agregado, pero la descarga inicial falló: {{error}}",
|
|
101
110
|
"updated": "Blog actualizado exitosamente.",
|
|
102
111
|
"deleted": "Blog eliminado exitosamente.",
|
|
103
|
-
"refreshed": "Blog actualizado. Se agregaron
|
|
112
|
+
"refreshed": "Blog actualizado. Se agregaron {{items}} entradas nuevas.",
|
|
104
113
|
"form": {
|
|
105
114
|
"discoverUrl": "URL del sitio web",
|
|
106
115
|
"discover": "Descubrir feed",
|
package/locales/es.json
CHANGED
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
"sync": {
|
|
30
|
-
"success": "Synced
|
|
31
|
-
"error": "Sync failed:
|
|
30
|
+
"success": "Synced {{blogs}} blogs, added {{items}} items.",
|
|
31
|
+
"error": "Sync failed: {{error}}",
|
|
32
32
|
"already_running": "A sync is already in progress.",
|
|
33
|
-
"cleared_success": "Cleared and re-synced
|
|
33
|
+
"cleared_success": "Cleared and re-synced {{blogs}} blogs, added {{items}} items."
|
|
34
34
|
},
|
|
35
35
|
|
|
36
36
|
"errors": {
|
|
37
37
|
"title": "Blogs con errores",
|
|
38
|
-
"seeAll": "Ver todos los
|
|
38
|
+
"seeAll": "Ver todos los {{count}} blogs con errores"
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
"sources": {
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"save": "Guardar",
|
|
49
49
|
"empty": "No hay fuentes OPML configuradas. Utiliza esto para importar blogs en bloque desde FreshRSS u otros lectores de feeds.",
|
|
50
50
|
"recent": "Fuentes OPML",
|
|
51
|
-
"interval": "Cada
|
|
51
|
+
"interval": "Cada {{minutes}} min",
|
|
52
52
|
"lastSync": "Última sincronización",
|
|
53
53
|
"deleteConfirm": "¿Eliminar esta fuente OPML? Los blogs importados se conservarán.",
|
|
54
54
|
"created": "Fuente OPML creada correctamente.",
|
|
55
55
|
"created_synced": "Fuente OPML creada y sincronizada correctamente.",
|
|
56
|
-
"created_sync_failed": "Fuente OPML creada, pero la sincronización falló:
|
|
56
|
+
"created_sync_failed": "Fuente OPML creada, pero la sincronización falló: {{error}}",
|
|
57
57
|
"updated": "Fuente OPML actualizada correctamente.",
|
|
58
58
|
"deleted": "Fuente OPML eliminada correctamente.",
|
|
59
|
-
"synced": "Sincronización exitosa. Añadidos:
|
|
59
|
+
"synced": "Sincronización exitosa. Añadidos: {{added}}, Actualizados: {{updated}}",
|
|
60
60
|
"form": {
|
|
61
61
|
"name": "Nombre",
|
|
62
62
|
"type": "Tipo de importación",
|
|
@@ -70,7 +70,16 @@
|
|
|
70
70
|
"microsubChannel": "Microsub Channel",
|
|
71
71
|
"microsubChannelHint": "Sync feeds from a specific channel, or all channels",
|
|
72
72
|
"categoryPrefix": "Category Prefix",
|
|
73
|
-
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')"
|
|
73
|
+
"categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')",
|
|
74
|
+
"feedlandInstance": "FeedLand Instance URL",
|
|
75
|
+
"feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)",
|
|
76
|
+
"feedlandUsername": "FeedLand Username",
|
|
77
|
+
"feedlandUsernameHint": "Your FeedLand screen name",
|
|
78
|
+
"feedlandCategory": "FeedLand Category",
|
|
79
|
+
"feedlandCategoryAll": "All subscriptions",
|
|
80
|
+
"feedlandCategoryHint": "Optional: sync only feeds from a specific category",
|
|
81
|
+
"feedlandLoadCategories": "Load",
|
|
82
|
+
"feedlandRequired": "FeedLand instance URL and username are required"
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
85
|
|
|
@@ -96,11 +105,11 @@
|
|
|
96
105
|
"clearFilters": "Limpiar filtros",
|
|
97
106
|
"deleteConfirm": "¿Eliminar este blog y todas sus entradas almacenadas?",
|
|
98
107
|
"created": "Blog añadido correctamente.",
|
|
99
|
-
"created_synced": "Blog añadido y sincronizado. Se obtuvieron
|
|
100
|
-
"created_sync_failed": "Blog añadido, pero la obtención inicial falló:
|
|
108
|
+
"created_synced": "Blog añadido y sincronizado. Se obtuvieron {{items}} entradas.",
|
|
109
|
+
"created_sync_failed": "Blog añadido, pero la obtención inicial falló: {{error}}",
|
|
101
110
|
"updated": "Blog actualizado correctamente.",
|
|
102
111
|
"deleted": "Blog eliminado correctamente.",
|
|
103
|
-
"refreshed": "Blog actualizado. Se añadieron
|
|
112
|
+
"refreshed": "Blog actualizado. Se añadieron {{items}} entradas nuevas.",
|
|
104
113
|
"form": {
|
|
105
114
|
"discoverUrl": "URL del sitio web",
|
|
106
115
|
"discover": "Descubrir feed",
|