@rmdes/indiekit-endpoint-posts 1.0.0-beta.25

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.
Files changed (50) hide show
  1. package/README.md +43 -0
  2. package/assets/icon.svg +4 -0
  3. package/includes/@indiekit-endpoint-posts-syndicate.njk +21 -0
  4. package/includes/@indiekit-endpoint-posts-widget.njk +14 -0
  5. package/includes/post-types/category-field.njk +11 -0
  6. package/includes/post-types/category.njk +4 -0
  7. package/includes/post-types/content-field.njk +17 -0
  8. package/includes/post-types/content.njk +3 -0
  9. package/includes/post-types/featured-field.njk +30 -0
  10. package/includes/post-types/featured.njk +8 -0
  11. package/includes/post-types/geo-field.njk +8 -0
  12. package/includes/post-types/location-field.njk +42 -0
  13. package/includes/post-types/location.njk +12 -0
  14. package/includes/post-types/mp-channel.njk +4 -0
  15. package/includes/post-types/name-field.njk +7 -0
  16. package/includes/post-types/published.njk +4 -0
  17. package/includes/post-types/summary-field.njk +17 -0
  18. package/includes/post-types/summary.njk +4 -0
  19. package/index.js +108 -0
  20. package/lib/controllers/delete.js +52 -0
  21. package/lib/controllers/form.js +121 -0
  22. package/lib/controllers/new.js +87 -0
  23. package/lib/controllers/post.js +51 -0
  24. package/lib/controllers/posts.js +96 -0
  25. package/lib/endpoint.js +53 -0
  26. package/lib/middleware/post-data.js +98 -0
  27. package/lib/middleware/validation.js +23 -0
  28. package/lib/status-types.js +32 -0
  29. package/lib/utils.js +201 -0
  30. package/locales/de.json +127 -0
  31. package/locales/en.json +127 -0
  32. package/locales/es-419.json +127 -0
  33. package/locales/es.json +127 -0
  34. package/locales/fr.json +127 -0
  35. package/locales/hi.json +127 -0
  36. package/locales/id.json +127 -0
  37. package/locales/it.json +127 -0
  38. package/locales/nl.json +127 -0
  39. package/locales/pl.json +127 -0
  40. package/locales/pt-BR.json +127 -0
  41. package/locales/pt.json +127 -0
  42. package/locales/sr.json +127 -0
  43. package/locales/sv.json +127 -0
  44. package/locales/zh-Hans-CN.json +127 -0
  45. package/package.json +55 -0
  46. package/views/new.njk +21 -0
  47. package/views/post-delete.njk +17 -0
  48. package/views/post-form.njk +114 -0
  49. package/views/post.njk +39 -0
  50. package/views/posts.njk +13 -0
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+
3
+ import { checkScope } from "@indiekit/endpoint-micropub/lib/scope.js";
4
+
5
+ /**
6
+ * View published post
7
+ * @type {import("express").RequestHandler}
8
+ */
9
+ export const postController = async (request, response) => {
10
+ const { draftMode, postName, postsPath, postStatus, properties, scope } =
11
+ response.locals;
12
+
13
+ const postEditable = draftMode ? postStatus === "draft" : true;
14
+
15
+ response.render("post", {
16
+ title: postName,
17
+ parent: {
18
+ href: postsPath,
19
+ text: response.locals.__("posts.posts.title"),
20
+ },
21
+ actions: [
22
+ scope &&
23
+ checkScope(scope, "update") &&
24
+ !properties.deleted &&
25
+ postEditable
26
+ ? {
27
+ href: path.join(request.baseUrl + request.path, "/update"),
28
+ icon: "updatePost",
29
+ text: response.locals.__("posts.update.action"),
30
+ }
31
+ : {},
32
+ scope && checkScope(scope, "delete") && !properties.deleted
33
+ ? {
34
+ classes: "actions__link--warning",
35
+ href: path.join(request.baseUrl + request.path, "/delete"),
36
+ icon: "delete",
37
+ text: response.locals.__("posts.delete.action"),
38
+ }
39
+ : {},
40
+ scope && checkScope(scope, "undelete") && properties.deleted
41
+ ? {
42
+ href: path.join(request.baseUrl + request.path, "/undelete"),
43
+ icon: "undelete",
44
+ text: response.locals.__("posts.undelete.action"),
45
+ }
46
+ : {},
47
+ ],
48
+ redirectUri: path.join(request.baseUrl, request.params.uid),
49
+ success: request.query.success,
50
+ });
51
+ };
@@ -0,0 +1,96 @@
1
+ import path from "node:path";
2
+
3
+ import { checkScope } from "@indiekit/endpoint-micropub/lib/scope.js";
4
+ import { excerpt } from "@indiekit/util";
5
+ import { mf2tojf2 } from "@paulrobertlloyd/mf2tojf2";
6
+
7
+ import { endpoint } from "../endpoint.js";
8
+ import { statusTypes } from "../status-types.js";
9
+ import { getPostStatusBadges, getPostName, getPhotoUrl } from "../utils.js";
10
+
11
+ /**
12
+ * List published posts
13
+ * @type {import("express").RequestHandler}
14
+ */
15
+ export const postsController = async (request, response, next) => {
16
+ try {
17
+ const { application, publication } = request.app.locals;
18
+ const { access_token, scope } = request.session;
19
+ const { after, before, success } = request.query;
20
+ const limit = Number(request.query.limit) || 12;
21
+
22
+ const micropubUrl = new URL(application.micropubEndpoint);
23
+ micropubUrl.searchParams.append("q", "source");
24
+ micropubUrl.searchParams.append("limit", String(limit));
25
+
26
+ if (after) {
27
+ micropubUrl.searchParams.append("after", String(after));
28
+ }
29
+
30
+ if (before) {
31
+ micropubUrl.searchParams.append("before", String(before));
32
+ }
33
+
34
+ const micropubResponse = await endpoint.get(micropubUrl.href, access_token);
35
+
36
+ let posts;
37
+ if (micropubResponse?.items?.length > 0) {
38
+ const jf2 = mf2tojf2(micropubResponse);
39
+ const items = jf2.children || [jf2];
40
+
41
+ posts = items.map((item) => {
42
+ item.id = item.uid;
43
+ item.icon = item["post-type"];
44
+ item.locale = application.locale;
45
+ item.photo = getPhotoUrl(publication, item);
46
+ item.description = {
47
+ text:
48
+ item.summary ||
49
+ (item.content?.text &&
50
+ excerpt(item.content.text, 30, publication.locale)),
51
+ };
52
+ item.title = getPostName(publication, item);
53
+ item.url = path.join(request.baseUrl, request.path, item.uid);
54
+ item.badges = getPostStatusBadges(item, response);
55
+
56
+ return item;
57
+ });
58
+ }
59
+
60
+ const cursor = {};
61
+
62
+ if (micropubResponse?.paging?.after) {
63
+ cursor.next = {
64
+ href: `?after=${micropubResponse.paging.after}`,
65
+ };
66
+ }
67
+
68
+ if (micropubResponse?.paging?.before) {
69
+ cursor.previous = {
70
+ href: `?before=${micropubResponse.paging.before}`,
71
+ };
72
+ }
73
+
74
+ response.render("posts", {
75
+ title: response.locals.__("posts.posts.title"),
76
+ actions: [
77
+ scope && checkScope(scope, "create")
78
+ ? {
79
+ href: path.join(request.baseUrl + request.path, "/new"),
80
+ icon: "createPost",
81
+ text: response.locals.__("posts.create.action"),
82
+ }
83
+ : {},
84
+ ],
85
+ cursor,
86
+ posts,
87
+ limit,
88
+ count: micropubResponse._count,
89
+ parentUrl: request.baseUrl + request.path,
90
+ statusTypes,
91
+ success,
92
+ });
93
+ } catch (error) {
94
+ next(error);
95
+ }
96
+ };
@@ -0,0 +1,53 @@
1
+ import { IndiekitError } from "@indiekit/error";
2
+
3
+ export const endpoint = {
4
+ /**
5
+ * Micropub query
6
+ * @param {string} url - URL
7
+ * @param {string} accessToken - Access token
8
+ * @returns {Promise<object>} Response data
9
+ */
10
+ async get(url, accessToken) {
11
+ const endpointResponse = await fetch(url, {
12
+ headers: {
13
+ accept: "application/json",
14
+ authorization: `Bearer ${accessToken}`,
15
+ },
16
+ });
17
+
18
+ if (!endpointResponse.ok) {
19
+ throw await IndiekitError.fromFetch(endpointResponse);
20
+ }
21
+
22
+ const body = await endpointResponse.json();
23
+
24
+ return body;
25
+ },
26
+
27
+ /**
28
+ * Micropub action
29
+ * @param {string} url - URL
30
+ * @param {string} accessToken - Access token
31
+ * @param {object} [jsonBody] - JSON body
32
+ * @returns {Promise<object>} Response data
33
+ */
34
+ async post(url, accessToken, jsonBody = false) {
35
+ const endpointResponse = await fetch(url, {
36
+ method: "POST",
37
+ headers: {
38
+ accept: "application/json",
39
+ authorization: `Bearer ${accessToken}`,
40
+ ...(jsonBody && { "content-type": "application/json" }),
41
+ },
42
+ ...(jsonBody && { body: JSON.stringify(jsonBody) }),
43
+ });
44
+
45
+ if (!endpointResponse.ok) {
46
+ throw await IndiekitError.fromFetch(endpointResponse);
47
+ }
48
+
49
+ return endpointResponse.status === 204
50
+ ? { success_description: endpointResponse.headers.get("location") }
51
+ : await endpointResponse.json();
52
+ },
53
+ };
@@ -0,0 +1,98 @@
1
+ import path from "node:path";
2
+
3
+ import { IndiekitError } from "@indiekit/error";
4
+
5
+ import { statusTypes } from "../status-types.js";
6
+ import {
7
+ getChannelItems,
8
+ getGeoValue,
9
+ getPostName,
10
+ getPostProperties,
11
+ getSyndicateToItems,
12
+ } from "../utils.js";
13
+
14
+ export const postData = {
15
+ create(request, response, next) {
16
+ const { publication } = request.app.locals;
17
+ const { access_token, scope } = request.session;
18
+
19
+ // Create new post object with default values
20
+ const postType = request.query.type || "note";
21
+ const properties = request.body || {};
22
+
23
+ // Get post type config
24
+ const { name, fields, h } = publication.postTypes[postType];
25
+
26
+ // Only select ‘checked’ syndication targets on first view
27
+ const checkTargets = Object.entries(properties).length === 0;
28
+
29
+ response.locals = {
30
+ accessToken: access_token,
31
+ action: "create",
32
+ channelItems: getChannelItems(publication),
33
+ fields,
34
+ name,
35
+ postsPath: path.dirname(request.baseUrl + request.path),
36
+ postType,
37
+ properties,
38
+ scope,
39
+ showAdvancedOptions: false,
40
+ syndicationTargetItems: getSyndicateToItems(publication, checkTargets),
41
+ type: h,
42
+ ...response.locals,
43
+ };
44
+
45
+ next();
46
+ },
47
+
48
+ async read(request, response, next) {
49
+ try {
50
+ const { application, publication } = request.app.locals;
51
+ const { action, uid } = request.params;
52
+ const { access_token, scope } = request.session;
53
+
54
+ const properties = await getPostProperties(
55
+ uid,
56
+ application.micropubEndpoint,
57
+ access_token,
58
+ );
59
+
60
+ if (!properties) {
61
+ throw IndiekitError.notFound(response.locals.__("NotFoundError.page"));
62
+ }
63
+
64
+ const allDay = properties?.start && !properties.start.includes("T");
65
+ const geo = properties?.location && getGeoValue(properties.location);
66
+ const postType = properties["post-type"];
67
+
68
+ // Get post type config
69
+ const { name, fields, h } = publication.postTypes[postType];
70
+
71
+ response.locals = {
72
+ accessToken: access_token,
73
+ action: action || "create",
74
+ allDay,
75
+ channelItems: getChannelItems(publication),
76
+ draftMode: scope?.includes("draft"),
77
+ fields,
78
+ geo,
79
+ h,
80
+ name,
81
+ postName: getPostName(publication, properties),
82
+ postsPath: path.dirname(request.baseUrl + request.path),
83
+ postStatus: properties["post-status"],
84
+ postType,
85
+ properties,
86
+ scope,
87
+ showAdvancedOptions: true,
88
+ syndicationTargetItems: getSyndicateToItems(publication),
89
+ statusTypes,
90
+ ...response.locals,
91
+ };
92
+
93
+ next();
94
+ } catch (error) {
95
+ next(error);
96
+ }
97
+ },
98
+ };
@@ -0,0 +1,23 @@
1
+ import { check, checkSchema } from "express-validator";
2
+
3
+ export const validate = {
4
+ async form(request, response, next) {
5
+ const { validationSchemas } = request.app.locals;
6
+ const validations = [];
7
+
8
+ for (const schema of [Object.fromEntries(validationSchemas)]) {
9
+ validations.push(checkSchema(schema));
10
+ }
11
+
12
+ for (let validation of validations) {
13
+ await validation.run(request);
14
+ }
15
+
16
+ next();
17
+ },
18
+ new: [
19
+ check("type")
20
+ .exists()
21
+ .withMessage((value, { req }) => req.__("posts.error.type.empty")),
22
+ ],
23
+ };
@@ -0,0 +1,32 @@
1
+ export const statusTypes = {
2
+ deleted: {
3
+ color: "red",
4
+ text: "posts.status.deleted",
5
+ },
6
+ draft: {
7
+ color: "offset",
8
+ text: "posts.status.draft",
9
+ },
10
+ private: {
11
+ color: "offset-purple",
12
+ icon: "publicOff",
13
+ text: "posts.status.private",
14
+ },
15
+ public: {
16
+ color: "offset-purple",
17
+ icon: "public",
18
+ text: "posts.status.public",
19
+ },
20
+ published: {
21
+ color: "purple",
22
+ text: "posts.status.published",
23
+ },
24
+ syndicated: {
25
+ text: "posts.status.syndicated",
26
+ },
27
+ unlisted: {
28
+ color: "offset-purple",
29
+ icon: "unlisted",
30
+ text: "posts.status.unlisted",
31
+ },
32
+ };
package/lib/utils.js ADDED
@@ -0,0 +1,201 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ import { sanitise, ISO_6709_RE } from "@indiekit/util";
4
+ import { mf2tojf2 } from "@paulrobertlloyd/mf2tojf2";
5
+ import formatcoords from "formatcoords";
6
+
7
+ import { endpoint } from "./endpoint.js";
8
+ import { statusTypes } from "./status-types.js";
9
+
10
+ /**
11
+ * Get channel `items` for checkboxes component
12
+ * @param {object} publication - Publication configuration
13
+ * @returns {object} Items for checkboxes component
14
+ */
15
+ export const getChannelItems = (publication) => {
16
+ return Object.entries(publication.channels).map(([uid, channel]) => ({
17
+ label: channel.name,
18
+ value: uid,
19
+ }));
20
+ };
21
+
22
+ /**
23
+ * Get geographic coordinates property
24
+ * @param {string} geo - Latitude and longitude, comma separated
25
+ * @returns {object} JF2 geo location property
26
+ */
27
+ export const getGeoProperty = (geo) => {
28
+ const { latitude, longitude } = geo.match(ISO_6709_RE).groups;
29
+
30
+ return {
31
+ type: "geo",
32
+ name: formatcoords(geo).format({
33
+ decimalPlaces: 2,
34
+ }),
35
+ latitude: Number(latitude),
36
+ longitude: Number(longitude),
37
+ };
38
+ };
39
+
40
+ /**
41
+ * Get comma separated geographic coordinates
42
+ * @param {object} location - JF2 location property
43
+ * @returns {string|undefined} Latitude and longitude, comma separated
44
+ */
45
+ export const getGeoValue = (location) => {
46
+ if (location && location.geo) {
47
+ return [location.geo.latitude, location.geo.longitude].toString();
48
+ } else if (location && location.type === "geo") {
49
+ return [location.latitude, location.longitude].toString();
50
+ }
51
+ };
52
+
53
+ /**
54
+ * Get location property
55
+ * @param {object} values - Latitude and longitude, comma separated
56
+ * @returns {object} JF2 location property
57
+ */
58
+ export const getLocationProperty = (values) => {
59
+ const { geo, location } = values;
60
+
61
+ const hasGeo = geo && geo.length > 0;
62
+ const hasLocation = location && Object.entries(sanitise(location)).length > 0;
63
+
64
+ // Determine Microformat type
65
+ if (hasLocation && location.name) {
66
+ location.type = "card";
67
+ } else if (hasLocation && !hasGeo) {
68
+ location.type = "adr";
69
+ }
70
+
71
+ // Add (or use) any provided geo location properties
72
+ if (hasLocation && hasGeo) {
73
+ location.geo = getGeoProperty(geo);
74
+ } else if (hasGeo) {
75
+ return getGeoProperty(geo);
76
+ }
77
+
78
+ return sanitise(location);
79
+ };
80
+
81
+ /**
82
+ * Get photo URL
83
+ * @param {object} publication - Publication configuration
84
+ * @param {object} properties - JF2 properties
85
+ * @returns {object|boolean} Photo object, with URL
86
+ */
87
+ export const getPhotoUrl = (publication, properties) => {
88
+ const photo = Array.isArray(properties.photo)
89
+ ? properties.photo[0]
90
+ : properties.photo;
91
+
92
+ if (!photo) {
93
+ return false;
94
+ } else if (URL.canParse(photo.url)) {
95
+ return photo;
96
+ } else {
97
+ return {
98
+ url: new URL(photo.url, publication.me).href,
99
+ };
100
+ }
101
+ };
102
+
103
+ /**
104
+ * Get post status badges
105
+ * @param {object} post - Post
106
+ * @param {import("express").Response} response - Response
107
+ * @returns {Array} Badges
108
+ */
109
+ export const getPostStatusBadges = (post, response) => {
110
+ const badges = [];
111
+
112
+ if (post["post-status"]) {
113
+ const statusType = post["post-status"];
114
+ badges.push({
115
+ color: statusTypes[statusType].color,
116
+ size: "small",
117
+ text: response.locals.__(statusTypes[statusType].text),
118
+ });
119
+ }
120
+
121
+ if (post.deleted) {
122
+ badges.push({
123
+ color: statusTypes.deleted.color,
124
+ size: "small",
125
+ text: response.locals.__(statusTypes.deleted.text),
126
+ });
127
+ }
128
+
129
+ return badges;
130
+ };
131
+
132
+ /**
133
+ * Get post name, falling back to post type name
134
+ * @param {object} publication - Publication configuration
135
+ * @param {object} properties - JF2 properties
136
+ * @returns {string} Post name or post type name
137
+ */
138
+ export const getPostName = (publication, properties) => {
139
+ if (properties.name) {
140
+ return properties.name;
141
+ }
142
+
143
+ const type = properties["post-type"];
144
+ const { name } = publication.postTypes[type];
145
+
146
+ return name;
147
+ };
148
+
149
+ /**
150
+ * Query Micropub endpoint for post data
151
+ * @param {string} uid - Item UID
152
+ * @param {string} micropubEndpoint - Micropub endpoint
153
+ * @param {string} accessToken - Access token
154
+ * @returns {Promise<object>} JF2 properties
155
+ */
156
+ export const getPostProperties = async (uid, micropubEndpoint, accessToken) => {
157
+ const micropubUrl = new URL(micropubEndpoint);
158
+ micropubUrl.searchParams.append("q", "source");
159
+
160
+ const micropubResponse = await endpoint.get(micropubUrl.href, accessToken);
161
+
162
+ if (micropubResponse?.items?.length > 0) {
163
+ const jf2 = mf2tojf2(micropubResponse);
164
+ const items = jf2.children || [jf2];
165
+ return items.find((item) => item.uid === uid);
166
+ }
167
+
168
+ return false;
169
+ };
170
+
171
+ /**
172
+ * Get post URL from ID
173
+ * @param {string} id - ID
174
+ * @returns {string} Post URL
175
+ */
176
+ export const getPostUrl = (id) => {
177
+ const url = Buffer.from(id, "base64url").toString("utf8");
178
+ return new URL(url).href;
179
+ };
180
+
181
+ /**
182
+ * Get syndication target `items` for checkboxes component
183
+ * @param {object} publication - Publication configuration
184
+ * @param {boolean} [checkTargets] - Select ’checked’ targets
185
+ * @returns {object} Items for checkboxes component
186
+ */
187
+ export const getSyndicateToItems = (publication, checkTargets = false) => {
188
+ return publication.syndicationTargets.map((target) => ({
189
+ label: target.info.service.name,
190
+ ...(target?.info?.error
191
+ ? {
192
+ disabled: true,
193
+ hint: target?.info?.error || false,
194
+ }
195
+ : {
196
+ hint: target?.info.uid,
197
+ value: target?.info.uid,
198
+ ...(checkTargets && { checked: target.options.checked }),
199
+ }),
200
+ }));
201
+ };
@@ -0,0 +1,127 @@
1
+ {
2
+ "posts": {
3
+ "create": {
4
+ "action": "Neuer Beitrag",
5
+ "title": "Einen neuen %s-Beitrag erstellen"
6
+ },
7
+ "delete": {
8
+ "action": "Beitrag löschen",
9
+ "cancel": "Nein — zurück zur Post",
10
+ "submit": "Ich bin mir sicher — lösche diesen Beitrag",
11
+ "title": "Möchtest du diesen Beitrag wirklich löschen?"
12
+ },
13
+ "error": {
14
+ "content": {
15
+ "empty": "Geben Sie einige Inhalte ein"
16
+ },
17
+ "featured-alt": {
18
+ "empty": "Geben Sie eine Beschreibung dieses Bildes ein"
19
+ },
20
+ "geo": {
21
+ "invalid": "Geben Sie gültige Koordinaten ein"
22
+ },
23
+ "media": {
24
+ "empty": "Geben Sie einen Dateipfad oder eine Webadresse wie %s ein"
25
+ },
26
+ "name": {
27
+ "empty": "Titel eingeben"
28
+ },
29
+ "type": {
30
+ "empty": "Wählen Sie einen Beitragstyp"
31
+ },
32
+ "url": {
33
+ "empty": "Geben Sie eine Webadresse wie %s"
34
+ }
35
+ },
36
+ "form": {
37
+ "advancedOptions": "Erweiterte Optionen",
38
+ "back": "Beitragstyp ändern",
39
+ "cancel": "Abbrechen",
40
+ "category": {
41
+ "hint": "Trennen Sie jede Kategorie durch ein Komma",
42
+ "label": "Kategorien",
43
+ "tag": "Kategorie"
44
+ },
45
+ "content": {
46
+ "label": "Inhalt"
47
+ },
48
+ "continue": "Fortsetzen",
49
+ "featured": {
50
+ "alt": "Barrierefreie Beschreibung",
51
+ "label": "Ausgewähltes Bild"
52
+ },
53
+ "geo": {
54
+ "hint": "Breitengrad und Längengrad, zum Beispiel %s",
55
+ "label": "Koordinaten des Standorts"
56
+ },
57
+ "location": {
58
+ "country-name": "Land",
59
+ "label": "Standort",
60
+ "locality": "Stadt oder Ort",
61
+ "name": "Veranstaltungsort",
62
+ "postal-code": "Postleitzahl",
63
+ "street-address": "Straße"
64
+ },
65
+ "media": {
66
+ "label": "Dateipfad oder URL"
67
+ },
68
+ "mp-channel": {
69
+ "label": "Kanal"
70
+ },
71
+ "mp-slug": {
72
+ "label": "Slug"
73
+ },
74
+ "mp-syndicate-to": {
75
+ "label": "Syndikat zu"
76
+ },
77
+ "name": {
78
+ "label": "Titel"
79
+ },
80
+ "publish": "Beitrag veröffentlichen",
81
+ "publishDraft": "Entwurf speichern",
82
+ "published": {
83
+ "label": "Veröffentlichungsdatum",
84
+ "now": "Jetzt",
85
+ "scheduled": "Bestimmtes Datum und Uhrzeit"
86
+ },
87
+ "summary": {
88
+ "label": "Übersicht"
89
+ },
90
+ "update": "Beitrag aktualisieren",
91
+ "updateDraft": "Entwurf aktualisieren",
92
+ "visibility": {
93
+ "label": "Sichtbarkeit"
94
+ }
95
+ },
96
+ "new": {
97
+ "title": "Welche Art von Beitrag möchtest du erstellen?"
98
+ },
99
+ "post": {
100
+ "properties": "Eigenschaften",
101
+ "syndicate": "Syndikatsposten"
102
+ },
103
+ "posts": {
104
+ "none": "Keine Beiträge",
105
+ "title": "Veröffentlichte Beiträge"
106
+ },
107
+ "status": {
108
+ "deleted": "Gelöscht",
109
+ "draft": "Entwurf",
110
+ "private": "Privat",
111
+ "public": "Öffentlich",
112
+ "published": "Veröffentlicht",
113
+ "syndicated": "Syndiziert",
114
+ "unlisted": "Nicht gelistete"
115
+ },
116
+ "title": "Beiträge",
117
+ "undelete": {
118
+ "action": "Beitrag wiederherstellen",
119
+ "submit": "Ich bin sicher – diesen Beitrag wiederherstellen",
120
+ "title": "Möchten Sie diesen Beitrag wirklich wiederherstellen?"
121
+ },
122
+ "update": {
123
+ "action": "Beitrag bearbeiten",
124
+ "title": "%s-Beitrag aktualisieren"
125
+ }
126
+ }
127
+ }