@jhits/plugin-blog 0.0.16 → 0.0.18

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 (67) hide show
  1. package/dist/api/categories.d.ts.map +1 -1
  2. package/dist/api/categories.js +43 -12
  3. package/dist/api/handler.d.ts +1 -0
  4. package/dist/api/handler.d.ts.map +1 -1
  5. package/dist/api/handler.js +259 -32
  6. package/dist/hooks/useBlogs.d.ts +2 -0
  7. package/dist/hooks/useBlogs.d.ts.map +1 -1
  8. package/dist/hooks/useBlogs.js +10 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -3
  12. package/dist/lib/i18n.d.ts +14 -0
  13. package/dist/lib/i18n.d.ts.map +1 -0
  14. package/dist/lib/i18n.js +58 -0
  15. package/dist/lib/mappers/apiMapper.d.ts +18 -0
  16. package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
  17. package/dist/lib/mappers/apiMapper.js +1 -0
  18. package/dist/state/reducer.d.ts.map +1 -1
  19. package/dist/state/reducer.js +11 -6
  20. package/dist/state/types.d.ts +5 -0
  21. package/dist/state/types.d.ts.map +1 -1
  22. package/dist/state/types.js +1 -0
  23. package/dist/types/post.d.ts +25 -0
  24. package/dist/types/post.d.ts.map +1 -1
  25. package/dist/utils/client.d.ts +2 -0
  26. package/dist/utils/client.d.ts.map +1 -1
  27. package/dist/utils/client.js +3 -1
  28. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  29. package/dist/views/CanvasEditor/CanvasEditorView.js +130 -4
  30. package/dist/views/CanvasEditor/EditorHeader.d.ts +5 -1
  31. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
  32. package/dist/views/CanvasEditor/EditorHeader.js +23 -5
  33. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +1 -1
  34. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/hooks/usePostLoader.js +14 -4
  36. package/dist/views/PostManager/LanguageFlags.d.ts +11 -0
  37. package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -0
  38. package/dist/views/PostManager/LanguageFlags.js +60 -0
  39. package/dist/views/PostManager/PostCards.d.ts.map +1 -1
  40. package/dist/views/PostManager/PostCards.js +4 -1
  41. package/dist/views/PostManager/PostFilters.d.ts +4 -1
  42. package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
  43. package/dist/views/PostManager/PostFilters.js +13 -3
  44. package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
  45. package/dist/views/PostManager/PostManagerView.js +24 -3
  46. package/dist/views/PostManager/PostTable.d.ts.map +1 -1
  47. package/dist/views/PostManager/PostTable.js +4 -1
  48. package/package.json +4 -4
  49. package/src/api/categories.ts +58 -11
  50. package/src/api/handler.ts +286 -31
  51. package/src/hooks/useBlogs.ts +12 -1
  52. package/src/index.tsx +7 -3
  53. package/src/lib/i18n.ts +78 -0
  54. package/src/lib/mappers/apiMapper.ts +20 -0
  55. package/src/state/reducer.ts +12 -6
  56. package/src/state/types.ts +5 -0
  57. package/src/types/post.ts +28 -0
  58. package/src/utils/client.ts +4 -0
  59. package/src/views/CanvasEditor/CanvasEditorView.tsx +164 -20
  60. package/src/views/CanvasEditor/EditorHeader.tsx +93 -18
  61. package/src/views/CanvasEditor/hooks/usePostLoader.ts +15 -4
  62. package/src/views/PostManager/LanguageFlags.tsx +136 -0
  63. package/src/views/PostManager/PostCards.tsx +22 -12
  64. package/src/views/PostManager/PostFilters.tsx +38 -1
  65. package/src/views/PostManager/PostManagerView.tsx +25 -2
  66. package/src/views/PostManager/PostTable.tsx +12 -1
  67. package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +0 -81
@@ -1 +1 @@
1
- {"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../src/api/categories.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAiCxF"}
1
+ {"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../src/api/categories.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgFxF"}
@@ -5,22 +5,53 @@
5
5
  import { NextResponse } from 'next/server';
6
6
  export async function GET(req, config) {
7
7
  try {
8
+ const url = new URL(req.url);
9
+ const language = url.searchParams.get('language');
10
+ const publishedOnly = url.searchParams.get('published') === 'true';
8
11
  const dbConnection = await config.getDb();
9
12
  const db = dbConnection.db();
10
13
  const blogs = db.collection(config.collectionName || 'blogs');
11
- // Get all unique categories from blog posts
12
- const categories = await blogs.distinct('categoryTags.category');
13
- // Also get categories from Hero blocks in contentBlocks
14
- const heroBlocks = await blogs.aggregate([
15
- { $unwind: '$contentBlocks' },
16
- { $match: { 'contentBlocks.type': 'hero' } },
17
- { $project: { category: '$contentBlocks.data.category' } },
18
- { $match: { category: { $exists: true, $ne: null, $nin: [''] } } },
19
- { $group: { _id: '$category' } },
20
- ]).toArray();
21
- const heroCategories = heroBlocks.map((block) => block._id);
14
+ // Build query
15
+ const query = {};
16
+ if (publishedOnly) {
17
+ // If language is specified, check language status, otherwise root status
18
+ if (language) {
19
+ query[`languages.${language}.status`] = 'published';
20
+ }
21
+ else {
22
+ query['publicationData.status'] = 'published';
23
+ }
24
+ }
25
+ if (language) {
26
+ query[`languages.${language}`] = { $exists: true };
27
+ }
28
+ // 1. Get unique categories from language-specific metadata
29
+ let categories = [];
30
+ if (language) {
31
+ const result = await blogs.distinct(`languages.${language}.metadata.categories`, query);
32
+ categories = result.filter(Boolean);
33
+ }
34
+ else {
35
+ const result = await blogs.distinct('categoryTags.category', query);
36
+ categories = result.filter(Boolean);
37
+ }
38
+ // 2. Get categories from Hero blocks in contentBlocks
39
+ let heroCategories = [];
40
+ const pipeline = [];
41
+ if (Object.keys(query).length > 0) {
42
+ pipeline.push({ $match: query });
43
+ }
44
+ if (language) {
45
+ pipeline.push({ $project: { blocks: `$languages.${language}.blocks` } }, { $unwind: '$blocks' }, { $match: { 'blocks.type': 'hero' } }, { $project: { category: '$blocks.data.category' } });
46
+ }
47
+ else {
48
+ pipeline.push({ $unwind: '$contentBlocks' }, { $match: { 'contentBlocks.type': 'hero' } }, { $project: { category: '$contentBlocks.data.category' } });
49
+ }
50
+ pipeline.push({ $match: { category: { $exists: true, $ne: null, $nin: [''] } } }, { $group: { _id: '$category' } });
51
+ const heroBlocks = await blogs.aggregate(pipeline).toArray();
52
+ heroCategories = heroBlocks.map((block) => block._id);
22
53
  // Combine and deduplicate
23
- const allCategories = Array.from(new Set([...categories.filter(Boolean), ...heroCategories.filter(Boolean)])).sort();
54
+ const allCategories = Array.from(new Set([...categories, ...heroCategories])).sort();
24
55
  return NextResponse.json({ categories: allCategories });
25
56
  }
26
57
  catch (err) {
@@ -21,6 +21,7 @@ export interface BlogApiConfig {
21
21
  * GET /api/blogs - List all blog posts
22
22
  * GET /api/blogs?admin=true - List all posts for admin (includes drafts)
23
23
  * GET /api/blogs?status=published - Filter by status
24
+ * GET /api/blogs?language=en - Filter by language (falls back to nl if not found)
24
25
  */
25
26
  export declare function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse>;
26
27
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD,MAAM,WAAW,aAAa;IAC1B,yFAAyF;IACzF,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;IACxC,yDAAyD;IACzD,SAAS,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgExF;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA0GzF;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CAgCvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CA8HvB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CA8BvB"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/api/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGxD,MAAM,WAAW,aAAa;IAC1B,yFAAyF;IACzF,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;IACxC,yDAAyD;IACzD,SAAS,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAqKxF;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA+HzF;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CAiIvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CAiKvB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CA8BvB"}
@@ -12,6 +12,7 @@ import { slugify } from '../lib/utils/slugify';
12
12
  * GET /api/blogs - List all blog posts
13
13
  * GET /api/blogs?admin=true - List all posts for admin (includes drafts)
14
14
  * GET /api/blogs?status=published - Filter by status
15
+ * GET /api/blogs?language=en - Filter by language (falls back to nl if not found)
15
16
  */
16
17
  export async function GET(req, config) {
17
18
  try {
@@ -20,6 +21,7 @@ export async function GET(req, config) {
20
21
  const skip = Number(url.searchParams.get('skip') ?? 0);
21
22
  const statusFilter = url.searchParams.get('status');
22
23
  const isAdminView = url.searchParams.get('admin') === 'true';
24
+ const requestedLanguage = url.searchParams.get('language') || 'nl';
23
25
  const userId = await config.getUserId(req);
24
26
  const dbConnection = await config.getDb();
25
27
  const db = dbConnection.db();
@@ -39,9 +41,10 @@ export async function GET(req, config) {
39
41
  }
40
42
  }
41
43
  else {
42
- // Public view: only published posts
44
+ // Public view: only published posts in the SPECIFIC language
45
+ // This ensures we only fetch blogs that actually have content for this language
43
46
  query = {
44
- 'publicationData.status': 'published',
47
+ [`languages.${requestedLanguage}.status`]: 'published',
45
48
  'publicationData.date': { $lte: new Date() },
46
49
  };
47
50
  if (statusFilter && statusFilter !== 'published') {
@@ -58,13 +61,99 @@ export async function GET(req, config) {
58
61
  .toArray(),
59
62
  blogs.countDocuments(query),
60
63
  ]);
61
- const formatted = data.map((doc) => ({
62
- ...doc,
63
- _id: doc._id.toString(),
64
- }));
64
+ // Supported languages for fallback (in order of preference)
65
+ const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
66
+ const formatted = data.map((doc) => {
67
+ const languages = doc.languages || {};
68
+ // Only use exact language match public views - no fallback for
69
+ // This ensures visitors see content in their language only
70
+ const hasExactLanguage = !!languages[requestedLanguage];
71
+ // Skip this post if no exact language match (for public non-admin views)
72
+ if (!isAdminView && !hasExactLanguage) {
73
+ return null;
74
+ }
75
+ const postPrimaryLang = doc.metadata?.lang || 'nl';
76
+ // Find the best available language for this post
77
+ let bestLanguage = requestedLanguage;
78
+ let isMissingTranslation = false;
79
+ if (!languages[requestedLanguage]) {
80
+ // For admin view, if specific language is requested, show it as missing instead of falling back
81
+ // This ensures the UI is "synced" with the selected language
82
+ if (isAdminView) {
83
+ isMissingTranslation = true;
84
+ }
85
+ else {
86
+ // Public view still uses fallback or filtering
87
+ const fallbackLanguages = [postPrimaryLang, 'nl', 'en'];
88
+ for (const lang of fallbackLanguages) {
89
+ if (languages[lang]) {
90
+ bestLanguage = lang;
91
+ break;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ const langContent = languages[bestLanguage] || {};
97
+ const meta = langContent.metadata || {};
98
+ // Ensure all languages in doc have a status and updatedAt for the dashboard
99
+ const enrichedLanguages = { ...languages };
100
+ Object.keys(enrichedLanguages).forEach(lang => {
101
+ if (!enrichedLanguages[lang].status) {
102
+ enrichedLanguages[lang].status = doc.publicationData?.status === 'concept' ? 'draft' : (doc.publicationData?.status || 'draft');
103
+ }
104
+ if (!enrichedLanguages[lang].updatedAt) {
105
+ enrichedLanguages[lang].updatedAt = doc.updatedAt;
106
+ }
107
+ });
108
+ // Language display names for placeholders
109
+ const langNames = {
110
+ nl: 'Dutch',
111
+ en: 'English',
112
+ sv: 'Swedish',
113
+ de: 'German',
114
+ fr: 'French',
115
+ es: 'Spanish'
116
+ };
117
+ const displayTitle = isMissingTranslation
118
+ ? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
119
+ : (meta.title || doc.title || '');
120
+ const displayStatus = isMissingTranslation
121
+ ? 'not-translated'
122
+ : (langContent.status || doc.publicationData?.status || 'concept');
123
+ return {
124
+ ...doc,
125
+ _id: doc._id.toString(),
126
+ title: displayTitle,
127
+ summary: isMissingTranslation ? '' : (meta.excerpt || doc.summary || ''),
128
+ contentBlocks: isMissingTranslation ? [] : (langContent.blocks || doc.contentBlocks || doc.blocks || []),
129
+ image: isMissingTranslation ? null : (meta.featuredImage || doc.image),
130
+ categoryTags: isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
131
+ category: meta.categories[0] || '',
132
+ tags: meta.tags || doc.categoryTags?.tags || []
133
+ } : (doc.categoryTags || { category: '', tags: [] })),
134
+ publicationData: {
135
+ ...doc.publicationData,
136
+ status: displayStatus,
137
+ },
138
+ seo: isMissingTranslation ? {} : (meta.seo || doc.seo || {}),
139
+ lang: bestLanguage,
140
+ isMissingTranslation,
141
+ requestedLanguage,
142
+ availableLanguages: Object.keys(languages),
143
+ languages: enrichedLanguages,
144
+ updatedAt: isMissingTranslation ? doc.updatedAt : (langContent.updatedAt || doc.updatedAt),
145
+ status: displayStatus,
146
+ };
147
+ }).filter(Boolean); // Remove null entries
148
+ // Sort by updatedAt descending so the UI order makes sense
149
+ formatted.sort((a, b) => {
150
+ const dateA = new Date(a.updatedAt).getTime();
151
+ const dateB = new Date(b.updatedAt).getTime();
152
+ return dateB - dateA;
153
+ });
65
154
  return NextResponse.json({
66
155
  blogs: formatted,
67
- total: totalCount,
156
+ total: formatted.length,
68
157
  });
69
158
  }
70
159
  catch (err) {
@@ -77,6 +166,8 @@ export async function GET(req, config) {
77
166
  */
78
167
  export async function POST(req, config) {
79
168
  try {
169
+ const url = new URL(req.url);
170
+ const language = url.searchParams.get('language') || 'nl';
80
171
  const userId = await config.getUserId(req);
81
172
  if (!userId) {
82
173
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
@@ -149,6 +240,24 @@ export async function POST(req, config) {
149
240
  seo: seo || { title: '', description: '' },
150
241
  slug,
151
242
  authorId: userId,
243
+ languages: {
244
+ [language]: {
245
+ blocks: contentBlocks || [],
246
+ metadata: {
247
+ title: title.trim(),
248
+ excerpt: (summary || '').trim(),
249
+ featuredImage: image,
250
+ categories: categoryTags?.category ? [categoryTags.category] : [],
251
+ tags: categoryTags?.tags || [],
252
+ seo: seo || {},
253
+ },
254
+ updatedAt: new Date().toISOString(),
255
+ status: finalStatus,
256
+ },
257
+ },
258
+ metadata: {
259
+ lang: language,
260
+ },
152
261
  createdAt: new Date(),
153
262
  updatedAt: new Date(),
154
263
  };
@@ -169,6 +278,8 @@ export async function POST(req, config) {
169
278
  */
170
279
  export async function GET_BY_SLUG(req, slug, config) {
171
280
  try {
281
+ const url = new URL(req.url);
282
+ const requestedLanguage = url.searchParams.get('language') || 'nl';
172
283
  const userId = await config.getUserId(req);
173
284
  const dbConnection = await config.getDb();
174
285
  const db = dbConnection.db();
@@ -178,14 +289,100 @@ export async function GET_BY_SLUG(req, slug, config) {
178
289
  return NextResponse.json({ error: 'Blog not found' }, { status: 404 });
179
290
  }
180
291
  // Security check
181
- const isPublished = blog.publicationData?.status === 'published';
182
292
  const isAuthor = userId && blog.authorId === userId;
183
- if (!isPublished && !isAuthor) {
184
- return NextResponse.json({ error: 'Access denied' }, { status: 403 });
293
+ const isAdminView = url.searchParams.get('admin') === 'true';
294
+ // For public view, we must have a published version in the requested language
295
+ if (!isAdminView && !isAuthor) {
296
+ const langData = blog.languages?.[requestedLanguage];
297
+ if (!langData || langData.status !== 'published') {
298
+ return NextResponse.json({ error: 'Blog post not available in this language' }, { status: 404 });
299
+ }
300
+ // Also check publication date
301
+ if (blog.publicationData?.date && new Date(blog.publicationData.date) > new Date()) {
302
+ return NextResponse.json({ error: 'Blog post not yet published' }, { status: 404 });
303
+ }
304
+ }
305
+ const languages = blog.languages || {};
306
+ const postPrimaryLang = blog.metadata?.lang || 'nl';
307
+ // Find the best available language for this post
308
+ let bestLanguage = requestedLanguage;
309
+ let isMissingTranslation = false;
310
+ if (!languages[requestedLanguage]) {
311
+ if (isAdminView) {
312
+ isMissingTranslation = true;
313
+ }
314
+ else {
315
+ // Try fallback languages in order
316
+ const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
317
+ for (const lang of fallbackLanguages) {
318
+ if (languages[lang]) {
319
+ bestLanguage = lang;
320
+ break;
321
+ }
322
+ }
323
+ // If still no match, use primary language from metadata
324
+ if (!languages[bestLanguage] && languages[postPrimaryLang]) {
325
+ bestLanguage = postPrimaryLang;
326
+ }
327
+ }
185
328
  }
329
+ const langContent = languages[bestLanguage] || {};
330
+ const meta = langContent.metadata || {};
331
+ // Language display names for placeholders
332
+ const langNames = {
333
+ nl: 'Dutch',
334
+ en: 'English',
335
+ sv: 'Swedish',
336
+ de: 'German',
337
+ fr: 'French',
338
+ es: 'Spanish'
339
+ };
340
+ let title = isMissingTranslation
341
+ ? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
342
+ : (meta.title || blog.title || '');
343
+ let summary = isMissingTranslation ? '' : (meta.excerpt || blog.summary || '');
344
+ let contentBlocks = isMissingTranslation ? [] : (langContent.blocks || blog.contentBlocks || blog.blocks || []);
345
+ let image = isMissingTranslation ? null : (meta.featuredImage || blog.image);
346
+ let categoryTags = isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
347
+ category: meta.categories[0] || '',
348
+ tags: meta.tags || blog.categoryTags?.tags || []
349
+ } : (blog.categoryTags || { category: '', tags: [] }));
350
+ let seo = isMissingTranslation ? {} : (meta.seo || blog.seo || {});
351
+ let metadata = { ...blog.metadata, ...meta, lang: bestLanguage };
352
+ // Ensure all languages in doc have a status and updatedAt for the dashboard
353
+ const enrichedLanguages = { ...languages };
354
+ Object.keys(enrichedLanguages).forEach(lang => {
355
+ if (!enrichedLanguages[lang].status) {
356
+ enrichedLanguages[lang].status = blog.publicationData?.status === 'concept' ? 'draft' : (blog.publicationData?.status || 'draft');
357
+ }
358
+ if (!enrichedLanguages[lang].updatedAt) {
359
+ enrichedLanguages[lang].updatedAt = blog.updatedAt;
360
+ }
361
+ });
362
+ const displayStatus = isMissingTranslation
363
+ ? 'not-translated'
364
+ : (langContent.status || blog.publicationData?.status || 'concept');
186
365
  return NextResponse.json({
187
366
  ...blog,
188
367
  _id: blog._id.toString(),
368
+ title,
369
+ summary,
370
+ contentBlocks,
371
+ image,
372
+ categoryTags,
373
+ publicationData: {
374
+ ...blog.publicationData,
375
+ status: displayStatus,
376
+ },
377
+ seo,
378
+ metadata,
379
+ languages: enrichedLanguages,
380
+ availableLanguages: Object.keys(languages),
381
+ lang: bestLanguage,
382
+ isMissingTranslation,
383
+ requestedLanguage,
384
+ updatedAt: isMissingTranslation ? blog.updatedAt : (langContent.updatedAt || blog.updatedAt),
385
+ status: displayStatus,
189
386
  });
190
387
  }
191
388
  catch (err) {
@@ -198,6 +395,8 @@ export async function GET_BY_SLUG(req, slug, config) {
198
395
  */
199
396
  export async function PUT_BY_SLUG(req, slug, config) {
200
397
  try {
398
+ const url = new URL(req.url);
399
+ const language = url.searchParams.get('language') || 'nl';
201
400
  const userId = await config.getUserId(req);
202
401
  if (!userId) {
203
402
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
@@ -253,15 +452,9 @@ export async function PUT_BY_SLUG(req, slug, config) {
253
452
  }, { status: 400 });
254
453
  }
255
454
  }
256
- // Slug logic
257
- let finalSlug = slug;
258
- if (isPublishing) {
259
- finalSlug = slugify(title);
260
- }
261
- else if (publicationData?.status === 'concept' && !slug.includes('-draft-')) {
262
- finalSlug = `${slugify(title)}-draft-${Date.now().toString().slice(-4)}`;
263
- }
264
- // Update data
455
+ // Slug logic - DON'T change slug for existing posts, keep original
456
+ // The slug should remain constant across all language versions
457
+ const finalSlug = slug;
265
458
  // Determine the final status: if publishing, set to 'published', otherwise preserve or convert draft to concept
266
459
  let finalStatus = publicationData?.status;
267
460
  if (isPublishing) {
@@ -270,30 +463,64 @@ export async function PUT_BY_SLUG(req, slug, config) {
270
463
  else if (publicationData?.status === 'draft') {
271
464
  finalStatus = 'concept';
272
465
  }
273
- const updateData = {
274
- title: title.trim(),
275
- summary: (summary || '').trim(),
276
- contentBlocks: contentBlocks || [],
277
- content: content || [],
278
- image: image || {},
279
- categoryTags: {
280
- category: categoryTags?.category?.trim() || '',
281
- tags: categoryTags?.tags || [],
466
+ // Preserve existing languages or initialize
467
+ const existingLanguages = existingBlog.languages || {};
468
+ const primaryLanguage = existingBlog.metadata?.lang || 'nl';
469
+ // Update the specific language content
470
+ // Only the language being saved gets a new updatedAt timestamp
471
+ const now = new Date();
472
+ const updatedLanguages = {
473
+ ...existingLanguages,
474
+ [language]: {
475
+ blocks: contentBlocks || [],
476
+ metadata: {
477
+ title: title.trim(),
478
+ excerpt: (summary || '').trim(),
479
+ featuredImage: image,
480
+ categories: categoryTags?.category ? [categoryTags.category] : [],
481
+ tags: categoryTags?.tags || [],
482
+ seo: seo || {},
483
+ },
484
+ updatedAt: now.toISOString(),
485
+ status: finalStatus,
282
486
  },
487
+ };
488
+ // For root-level fields, only update if this is the primary language
489
+ // Otherwise preserve existing root values to maintain backward compatibility
490
+ const isPrimaryLanguage = language === primaryLanguage || !existingLanguages[primaryLanguage];
491
+ const updateData = {
492
+ // Only update root-level title/summary if saving in primary language
493
+ // This maintains backward compatibility
494
+ ...(isPrimaryLanguage ? {
495
+ title: title.trim(),
496
+ summary: (summary || '').trim(),
497
+ contentBlocks: contentBlocks || [],
498
+ content: content || [],
499
+ image: image || {},
500
+ categoryTags: {
501
+ category: categoryTags?.category?.trim() || '',
502
+ tags: categoryTags?.tags || [],
503
+ },
504
+ seo: seo || {},
505
+ } : {}),
283
506
  publicationData: {
507
+ ...existingBlog.publicationData,
284
508
  ...publicationData,
285
509
  status: finalStatus,
286
- date: publicationData?.date ? new Date(publicationData.date) : new Date(),
510
+ date: publicationData?.date ? new Date(publicationData.date) : existingBlog.publicationData?.date || new Date(),
287
511
  },
288
- seo: seo || {},
289
- slug: finalSlug,
290
512
  authorId: userId,
513
+ languages: updatedLanguages,
514
+ metadata: {
515
+ ...existingBlog.metadata,
516
+ lang: primaryLanguage,
517
+ },
291
518
  updatedAt: new Date(),
292
519
  };
293
520
  await blogs.updateOne({ slug }, { $set: updateData });
294
521
  return NextResponse.json({
295
522
  message: 'Blog updated successfully',
296
- slug: finalSlug,
523
+ slug: slug, // Return original slug, not finalSlug
297
524
  });
298
525
  }
299
526
  catch (err) {
@@ -10,6 +10,8 @@ export interface UseBlogsOptions {
10
10
  skip?: number;
11
11
  /** Filter by status (published, draft, concept) */
12
12
  status?: string;
13
+ /** Filter by language */
14
+ language?: string;
13
15
  /** Whether to fetch all posts for admin (includes drafts) */
14
16
  admin?: boolean;
15
17
  /** API base URL (default: '/api/blogs') */
@@ -1 +1 @@
1
- {"version":3,"file":"useBlogs.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlogs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,MAAM,WAAW,eAAe;IAC5B,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC3B,0BAA0B;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CA8EtE"}
1
+ {"version":3,"file":"useBlogs.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlogs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,MAAM,WAAW,eAAe;IAC5B,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC3B,0BAA0B;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CAuFtE"}
@@ -13,12 +13,18 @@ import { apiToBlogPost } from '../lib/mappers/apiMapper';
13
13
  * ```
14
14
  */
15
15
  export function useBlogs(options = {}) {
16
- const { limit = 10, skip = 0, status, admin = false, apiBaseUrl = '/api/plugin-blog', } = options;
16
+ const { limit = 10, skip = 0, status, language, admin = false, apiBaseUrl = '/api/plugin-blog', } = options;
17
17
  const [blogs, setBlogs] = useState([]);
18
18
  const [loading, setLoading] = useState(true);
19
19
  const [error, setError] = useState(null);
20
20
  const [total, setTotal] = useState(0);
21
21
  const fetchBlogs = async () => {
22
+ // If language is expected but not provided, wait for it to be ready
23
+ // (Prevents fetching all languages if the hook is called with a language but it's not yet loaded)
24
+ if (!language && !admin) {
25
+ // In public view, we always want a specific language
26
+ return;
27
+ }
22
28
  try {
23
29
  setLoading(true);
24
30
  setError(null);
@@ -29,6 +35,8 @@ export function useBlogs(options = {}) {
29
35
  params.set('skip', skip.toString());
30
36
  if (status)
31
37
  params.set('status', status);
38
+ if (language)
39
+ params.set('language', language);
32
40
  if (admin)
33
41
  params.set('admin', 'true');
34
42
  const url = `${apiBaseUrl}?${params.toString()}`;
@@ -71,7 +79,7 @@ export function useBlogs(options = {}) {
71
79
  };
72
80
  useEffect(() => {
73
81
  fetchBlogs();
74
- }, [limit, skip, status, admin, apiBaseUrl]);
82
+ }, [limit, skip, status, language, admin, apiBaseUrl]);
75
83
  return {
76
84
  blogs,
77
85
  loading,
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export interface PluginProps {
36
36
  export default function BlogPlugin(props: PluginProps): import("react/jsx-runtime").JSX.Element;
37
37
  export { BlogPlugin as Index };
38
38
  export type { Block, BlockTypeDefinition, ClientBlockDefinition, RichTextFormattingConfig, BlockEditProps, BlockPreviewProps, IBlockComponent, } from './types';
39
- export type { SEOMetadata, PublicationData, PostStatus, PostMetadata, BlogPost, PostListItem, PostFilterOptions, } from './types/post';
39
+ export type { SEOMetadata, PublicationData, PostStatus, PostMetadata, BlogPost, PostListItem, PostFilterOptions, BlogPostLanguageContent, BlogPostLanguages, } from './types/post';
40
40
  export { initBlogPlugin } from './init';
41
41
  export type { BlogPluginConfig } from './init';
42
42
  export { RichTextEditor, RichTextPreview } from './lib/rich-text';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAStD;;;GAGG;AACH,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;QACd,gDAAgD;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACL;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,KAAK,EAAE,WAAW,2CA4NpD;AAID,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,CAAC;AAG/B,YAAY,EACR,KAAK,EACL,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,eAAe,GAClB,MAAM,SAAS,CAAC;AAGjB,YAAY,EACR,WAAW,EACX,eAAe,EACf,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,iBAAiB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC5C,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG9F,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAG5F,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG3E,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGpF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAStD;;;GAGG;AACH,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,gBAAgB,CAAC,EAAE;QACf,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;QACd,gDAAgD;QAChD,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACL;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,KAAK,EAAE,WAAW,2CA8NpD;AAID,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,CAAC;AAG/B,YAAY,EACR,KAAK,EACL,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,eAAe,GAClB,MAAM,SAAS,CAAC;AAGjB,YAAY,EACR,WAAW,EACX,eAAe,EACf,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAC5C,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG9F,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAG5F,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG3E,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGpF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -140,9 +140,10 @@ export default function BlogPlugin(props) {
140
140
  if (!originalSlug) {
141
141
  throw new Error('Cannot save: no post identifier available. Please reload the page.');
142
142
  }
143
- console.log('[BlogPlugin] Saving post with slug:', originalSlug);
143
+ const language = state.currentLanguage || locale;
144
+ console.log('[BlogPlugin] Saving post with slug:', originalSlug, 'language:', language);
144
145
  const apiData = editorStateToAPI(state, undefined, heroBlock);
145
- const response = await fetch(`/api/plugin-blog/${originalSlug}`, {
146
+ const response = await fetch(`/api/plugin-blog/${originalSlug}?language=${language}`, {
146
147
  method: 'PUT',
147
148
  headers: { 'Content-Type': 'application/json' },
148
149
  credentials: 'include', // Include cookies for authentication
@@ -173,8 +174,9 @@ export default function BlogPlugin(props) {
173
174
  case 'new':
174
175
  return (_jsx(EditorProvider, { customBlocks: customBlocks, darkMode: darkMode, backgroundColors: backgroundColors, onSave: async (state) => {
175
176
  // Save to API - create new post
177
+ const language = state.currentLanguage || locale;
176
178
  const apiData = editorStateToAPI(state);
177
- const response = await fetch('/api/plugin-blog/new', {
179
+ const response = await fetch(`/api/plugin-blog/new?language=${language}`, {
178
180
  method: 'POST',
179
181
  headers: { 'Content-Type': 'application/json' },
180
182
  credentials: 'include', // Include cookies for authentication
@@ -0,0 +1,14 @@
1
+ /**
2
+ * i18n Utility
3
+ * Simple translation loader for the blog plugin
4
+ */
5
+ export declare function getTranslations(locale: string): any;
6
+ export declare function getEditorTranslations(language: string): {
7
+ language: string;
8
+ selectLanguage: string;
9
+ addLanguage: string;
10
+ switchLanguage: string;
11
+ availableLanguages: string;
12
+ primaryLanguage: string;
13
+ };
14
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/lib/i18n.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CASnD;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CAC3B,CAYA"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * i18n Utility
3
+ * Simple translation loader for the blog plugin
4
+ */
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ function getLocalesDir() {
8
+ const possiblePaths = [
9
+ path.join(process.cwd(), 'node_modules', '@jhits', 'plugin-blog', 'data', 'locales'),
10
+ path.join(__dirname, '..', 'data', 'locales'),
11
+ path.join(__dirname, '..', '..', 'data', 'locales'),
12
+ ];
13
+ for (const localesPath of possiblePaths) {
14
+ if (fs.existsSync(localesPath)) {
15
+ return localesPath;
16
+ }
17
+ }
18
+ return possiblePaths[0];
19
+ }
20
+ const localesDir = getLocalesDir();
21
+ const cache = {};
22
+ function loadLocale(locale) {
23
+ if (cache[locale]) {
24
+ return cache[locale];
25
+ }
26
+ try {
27
+ const localePath = path.join(localesDir, locale, 'common.json');
28
+ if (fs.existsSync(localePath)) {
29
+ const content = fs.readFileSync(localePath, 'utf-8');
30
+ cache[locale] = JSON.parse(content);
31
+ return cache[locale];
32
+ }
33
+ }
34
+ catch (error) {
35
+ console.error(`[i18n] Failed to load locale ${locale}:`, error);
36
+ }
37
+ return null;
38
+ }
39
+ export function getTranslations(locale) {
40
+ const translations = loadLocale(locale);
41
+ if (translations) {
42
+ return translations;
43
+ }
44
+ const fallback = loadLocale('en');
45
+ return fallback || {};
46
+ }
47
+ export function getEditorTranslations(language) {
48
+ const translations = getTranslations(language);
49
+ const enTranslations = getTranslations('en');
50
+ return translations.editor || enTranslations.editor || {
51
+ language: 'Language',
52
+ selectLanguage: 'Select Language',
53
+ addLanguage: 'Add Language',
54
+ switchLanguage: 'Switch Language',
55
+ availableLanguages: 'Available Languages',
56
+ primaryLanguage: 'Primary Language',
57
+ };
58
+ }