@jhits/plugin-blog 0.0.17 → 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 (66) 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 +3 -3
  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
@@ -23,6 +23,7 @@ export interface BlogApiConfig {
23
23
  * GET /api/blogs - List all blog posts
24
24
  * GET /api/blogs?admin=true - List all posts for admin (includes drafts)
25
25
  * GET /api/blogs?status=published - Filter by status
26
+ * GET /api/blogs?language=en - Filter by language (falls back to nl if not found)
26
27
  */
27
28
  export async function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
28
29
  try {
@@ -31,6 +32,7 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
31
32
  const skip = Number(url.searchParams.get('skip') ?? 0);
32
33
  const statusFilter = url.searchParams.get('status');
33
34
  const isAdminView = url.searchParams.get('admin') === 'true';
35
+ const requestedLanguage = url.searchParams.get('language') || 'nl';
34
36
 
35
37
  const userId = await config.getUserId(req);
36
38
  const dbConnection = await config.getDb();
@@ -51,11 +53,13 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
51
53
  query = { authorId: userId };
52
54
  }
53
55
  } else {
54
- // Public view: only published posts
56
+ // Public view: only published posts in the SPECIFIC language
57
+ // This ensures we only fetch blogs that actually have content for this language
55
58
  query = {
56
- 'publicationData.status': 'published',
59
+ [`languages.${requestedLanguage}.status`]: 'published',
57
60
  'publicationData.date': { $lte: new Date() },
58
61
  };
62
+
59
63
  if (statusFilter && statusFilter !== 'published') {
60
64
  // Non-admin can't filter by non-published status
61
65
  return NextResponse.json({ error: 'Invalid status filter' }, { status: 400 });
@@ -72,14 +76,112 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
72
76
  blogs.countDocuments(query),
73
77
  ]);
74
78
 
75
- const formatted = data.map((doc: any) => ({
76
- ...doc,
77
- _id: doc._id.toString(),
78
- }));
79
+ // Supported languages for fallback (in order of preference)
80
+ const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
81
+
82
+ const formatted = data.map((doc: any) => {
83
+ const languages = doc.languages || {};
84
+
85
+ // Only use exact language match public views - no fallback for
86
+ // This ensures visitors see content in their language only
87
+ const hasExactLanguage = !!languages[requestedLanguage];
88
+
89
+ // Skip this post if no exact language match (for public non-admin views)
90
+ if (!isAdminView && !hasExactLanguage) {
91
+ return null;
92
+ }
93
+
94
+ const postPrimaryLang = doc.metadata?.lang || 'nl';
95
+
96
+ // Find the best available language for this post
97
+ let bestLanguage = requestedLanguage;
98
+ let isMissingTranslation = false;
99
+
100
+ if (!languages[requestedLanguage]) {
101
+ // For admin view, if specific language is requested, show it as missing instead of falling back
102
+ // This ensures the UI is "synced" with the selected language
103
+ if (isAdminView) {
104
+ isMissingTranslation = true;
105
+ } else {
106
+ // Public view still uses fallback or filtering
107
+ const fallbackLanguages = [postPrimaryLang, 'nl', 'en'];
108
+ for (const lang of fallbackLanguages) {
109
+ if (languages[lang]) {
110
+ bestLanguage = lang;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ const langContent = languages[bestLanguage] || {};
118
+ const meta = langContent.metadata || {};
119
+
120
+ // Ensure all languages in doc have a status and updatedAt for the dashboard
121
+ const enrichedLanguages = { ...languages };
122
+ Object.keys(enrichedLanguages).forEach(lang => {
123
+ if (!enrichedLanguages[lang].status) {
124
+ enrichedLanguages[lang].status = doc.publicationData?.status === 'concept' ? 'draft' : (doc.publicationData?.status || 'draft');
125
+ }
126
+ if (!enrichedLanguages[lang].updatedAt) {
127
+ enrichedLanguages[lang].updatedAt = doc.updatedAt;
128
+ }
129
+ });
130
+
131
+ // Language display names for placeholders
132
+ const langNames: Record<string, string> = {
133
+ nl: 'Dutch',
134
+ en: 'English',
135
+ sv: 'Swedish',
136
+ de: 'German',
137
+ fr: 'French',
138
+ es: 'Spanish'
139
+ };
140
+
141
+ const displayTitle = isMissingTranslation
142
+ ? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
143
+ : (meta.title || doc.title || '');
144
+
145
+ const displayStatus = isMissingTranslation
146
+ ? 'not-translated'
147
+ : (langContent.status || doc.publicationData?.status || 'concept');
148
+
149
+ return {
150
+ ...doc,
151
+ _id: doc._id.toString(),
152
+ title: displayTitle,
153
+ summary: isMissingTranslation ? '' : (meta.excerpt || doc.summary || ''),
154
+ contentBlocks: isMissingTranslation ? [] : (langContent.blocks || doc.contentBlocks || doc.blocks || []),
155
+ image: isMissingTranslation ? null : (meta.featuredImage || doc.image),
156
+ categoryTags: isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
157
+ category: meta.categories[0] || '',
158
+ tags: meta.tags || doc.categoryTags?.tags || []
159
+ } : (doc.categoryTags || { category: '', tags: [] })),
160
+ publicationData: {
161
+ ...doc.publicationData,
162
+ status: displayStatus,
163
+ },
164
+ seo: isMissingTranslation ? {} : (meta.seo || doc.seo || {}),
165
+ lang: bestLanguage,
166
+ isMissingTranslation,
167
+ requestedLanguage,
168
+ availableLanguages: Object.keys(languages),
169
+ languages: enrichedLanguages,
170
+ updatedAt: isMissingTranslation ? doc.updatedAt : (langContent.updatedAt || doc.updatedAt),
171
+ status: displayStatus,
172
+ };
173
+ }).filter(Boolean); // Remove null entries
174
+
175
+ // Sort by updatedAt descending so the UI order makes sense
176
+ formatted.sort((a: any, b: any) => {
177
+ const dateA = new Date(a.updatedAt).getTime();
178
+ const dateB = new Date(b.updatedAt).getTime();
179
+ return dateB - dateA;
180
+ });
79
181
 
80
182
  return NextResponse.json({
81
183
  blogs: formatted,
82
- total: totalCount,
184
+ total: formatted.length,
83
185
  });
84
186
  } catch (err: any) {
85
187
  console.error('[BlogAPI] GET error:', err);
@@ -95,6 +197,9 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
95
197
  */
96
198
  export async function POST(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
97
199
  try {
200
+ const url = new URL(req.url);
201
+ const language = url.searchParams.get('language') || 'nl';
202
+
98
203
  const userId = await config.getUserId(req);
99
204
  if (!userId) {
100
205
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
@@ -181,6 +286,24 @@ export async function POST(req: NextRequest, config: BlogApiConfig): Promise<Nex
181
286
  seo: seo || { title: '', description: '' },
182
287
  slug,
183
288
  authorId: userId,
289
+ languages: {
290
+ [language]: {
291
+ blocks: contentBlocks || [],
292
+ metadata: {
293
+ title: title.trim(),
294
+ excerpt: (summary || '').trim(),
295
+ featuredImage: image,
296
+ categories: categoryTags?.category ? [categoryTags.category] : [],
297
+ tags: categoryTags?.tags || [],
298
+ seo: seo || {},
299
+ },
300
+ updatedAt: new Date().toISOString(),
301
+ status: finalStatus,
302
+ },
303
+ },
304
+ metadata: {
305
+ lang: language,
306
+ },
184
307
  createdAt: new Date(),
185
308
  updatedAt: new Date(),
186
309
  };
@@ -210,6 +333,9 @@ export async function GET_BY_SLUG(
210
333
  config: BlogApiConfig
211
334
  ): Promise<NextResponse> {
212
335
  try {
336
+ const url = new URL(req.url);
337
+ const requestedLanguage = url.searchParams.get('language') || 'nl';
338
+
213
339
  const userId = await config.getUserId(req);
214
340
  const dbConnection = await config.getDb();
215
341
  const db = dbConnection.db();
@@ -222,16 +348,110 @@ export async function GET_BY_SLUG(
222
348
  }
223
349
 
224
350
  // Security check
225
- const isPublished = blog.publicationData?.status === 'published';
226
351
  const isAuthor = userId && blog.authorId === userId;
352
+ const isAdminView = url.searchParams.get('admin') === 'true';
353
+
354
+ // For public view, we must have a published version in the requested language
355
+ if (!isAdminView && !isAuthor) {
356
+ const langData = blog.languages?.[requestedLanguage];
357
+ if (!langData || langData.status !== 'published') {
358
+ return NextResponse.json({ error: 'Blog post not available in this language' }, { status: 404 });
359
+ }
360
+
361
+ // Also check publication date
362
+ if (blog.publicationData?.date && new Date(blog.publicationData.date) > new Date()) {
363
+ return NextResponse.json({ error: 'Blog post not yet published' }, { status: 404 });
364
+ }
365
+ }
227
366
 
228
- if (!isPublished && !isAuthor) {
229
- return NextResponse.json({ error: 'Access denied' }, { status: 403 });
367
+ const languages = blog.languages || {};
368
+ const postPrimaryLang = blog.metadata?.lang || 'nl';
369
+
370
+ // Find the best available language for this post
371
+ let bestLanguage = requestedLanguage;
372
+ let isMissingTranslation = false;
373
+
374
+ if (!languages[requestedLanguage]) {
375
+ if (isAdminView) {
376
+ isMissingTranslation = true;
377
+ } else {
378
+ // Try fallback languages in order
379
+ const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
380
+ for (const lang of fallbackLanguages) {
381
+ if (languages[lang]) {
382
+ bestLanguage = lang;
383
+ break;
384
+ }
385
+ }
386
+ // If still no match, use primary language from metadata
387
+ if (!languages[bestLanguage] && languages[postPrimaryLang]) {
388
+ bestLanguage = postPrimaryLang;
389
+ }
390
+ }
230
391
  }
392
+
393
+ const langContent = languages[bestLanguage] || {};
394
+ const meta = langContent.metadata || {};
395
+
396
+ // Language display names for placeholders
397
+ const langNames: Record<string, string> = {
398
+ nl: 'Dutch',
399
+ en: 'English',
400
+ sv: 'Swedish',
401
+ de: 'German',
402
+ fr: 'French',
403
+ es: 'Spanish'
404
+ };
405
+
406
+ let title = isMissingTranslation
407
+ ? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
408
+ : (meta.title || blog.title || '');
409
+ let summary = isMissingTranslation ? '' : (meta.excerpt || blog.summary || '');
410
+ let contentBlocks = isMissingTranslation ? [] : (langContent.blocks || blog.contentBlocks || blog.blocks || []);
411
+ let image = isMissingTranslation ? null : (meta.featuredImage || blog.image);
412
+ let categoryTags = isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
413
+ category: meta.categories[0] || '',
414
+ tags: meta.tags || blog.categoryTags?.tags || []
415
+ } : (blog.categoryTags || { category: '', tags: [] }));
416
+ let seo = isMissingTranslation ? {} : (meta.seo || blog.seo || {});
417
+ let metadata = { ...blog.metadata, ...meta, lang: bestLanguage };
418
+
419
+ // Ensure all languages in doc have a status and updatedAt for the dashboard
420
+ const enrichedLanguages = { ...languages };
421
+ Object.keys(enrichedLanguages).forEach(lang => {
422
+ if (!enrichedLanguages[lang].status) {
423
+ enrichedLanguages[lang].status = blog.publicationData?.status === 'concept' ? 'draft' : (blog.publicationData?.status || 'draft');
424
+ }
425
+ if (!enrichedLanguages[lang].updatedAt) {
426
+ enrichedLanguages[lang].updatedAt = blog.updatedAt;
427
+ }
428
+ });
429
+
430
+ const displayStatus = isMissingTranslation
431
+ ? 'not-translated'
432
+ : (langContent.status || blog.publicationData?.status || 'concept');
231
433
 
232
434
  return NextResponse.json({
233
435
  ...blog,
234
436
  _id: blog._id.toString(),
437
+ title,
438
+ summary,
439
+ contentBlocks,
440
+ image,
441
+ categoryTags,
442
+ publicationData: {
443
+ ...blog.publicationData,
444
+ status: displayStatus,
445
+ },
446
+ seo,
447
+ metadata,
448
+ languages: enrichedLanguages,
449
+ availableLanguages: Object.keys(languages),
450
+ lang: bestLanguage,
451
+ isMissingTranslation,
452
+ requestedLanguage,
453
+ updatedAt: isMissingTranslation ? blog.updatedAt : (langContent.updatedAt || blog.updatedAt),
454
+ status: displayStatus,
235
455
  });
236
456
  } catch (err: any) {
237
457
  console.error('[BlogAPI] GET_BY_SLUG error:', err);
@@ -251,6 +471,9 @@ export async function PUT_BY_SLUG(
251
471
  config: BlogApiConfig
252
472
  ): Promise<NextResponse> {
253
473
  try {
474
+ const url = new URL(req.url);
475
+ const language = url.searchParams.get('language') || 'nl';
476
+
254
477
  const userId = await config.getUserId(req);
255
478
  if (!userId) {
256
479
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
@@ -324,15 +547,10 @@ export async function PUT_BY_SLUG(
324
547
  }
325
548
  }
326
549
 
327
- // Slug logic
328
- let finalSlug = slug;
329
- if (isPublishing) {
330
- finalSlug = slugify(title);
331
- } else if (publicationData?.status === 'concept' && !slug.includes('-draft-')) {
332
- finalSlug = `${slugify(title)}-draft-${Date.now().toString().slice(-4)}`;
333
- }
550
+ // Slug logic - DON'T change slug for existing posts, keep original
551
+ // The slug should remain constant across all language versions
552
+ const finalSlug = slug;
334
553
 
335
- // Update data
336
554
  // Determine the final status: if publishing, set to 'published', otherwise preserve or convert draft to concept
337
555
  let finalStatus = publicationData?.status;
338
556
  if (isPublishing) {
@@ -341,24 +559,61 @@ export async function PUT_BY_SLUG(
341
559
  finalStatus = 'concept';
342
560
  }
343
561
 
344
- const updateData = {
345
- title: title.trim(),
346
- summary: (summary || '').trim(),
347
- contentBlocks: contentBlocks || [],
348
- content: content || [],
349
- image: image || {},
350
- categoryTags: {
351
- category: categoryTags?.category?.trim() || '',
352
- tags: categoryTags?.tags || [],
562
+ // Preserve existing languages or initialize
563
+ const existingLanguages = existingBlog.languages || {};
564
+ const primaryLanguage = existingBlog.metadata?.lang || 'nl';
565
+
566
+ // Update the specific language content
567
+ // Only the language being saved gets a new updatedAt timestamp
568
+ const now = new Date();
569
+ const updatedLanguages = {
570
+ ...existingLanguages,
571
+ [language]: {
572
+ blocks: contentBlocks || [],
573
+ metadata: {
574
+ title: title.trim(),
575
+ excerpt: (summary || '').trim(),
576
+ featuredImage: image,
577
+ categories: categoryTags?.category ? [categoryTags.category] : [],
578
+ tags: categoryTags?.tags || [],
579
+ seo: seo || {},
580
+ },
581
+ updatedAt: now.toISOString(),
582
+ status: finalStatus,
353
583
  },
584
+ };
585
+
586
+ // For root-level fields, only update if this is the primary language
587
+ // Otherwise preserve existing root values to maintain backward compatibility
588
+ const isPrimaryLanguage = language === primaryLanguage || !existingLanguages[primaryLanguage];
589
+
590
+ const updateData: any = {
591
+ // Only update root-level title/summary if saving in primary language
592
+ // This maintains backward compatibility
593
+ ...(isPrimaryLanguage ? {
594
+ title: title.trim(),
595
+ summary: (summary || '').trim(),
596
+ contentBlocks: contentBlocks || [],
597
+ content: content || [],
598
+ image: image || {},
599
+ categoryTags: {
600
+ category: categoryTags?.category?.trim() || '',
601
+ tags: categoryTags?.tags || [],
602
+ },
603
+ seo: seo || {},
604
+ } : {}),
354
605
  publicationData: {
606
+ ...existingBlog.publicationData,
355
607
  ...publicationData,
356
608
  status: finalStatus,
357
- date: publicationData?.date ? new Date(publicationData.date) : new Date(),
609
+ date: publicationData?.date ? new Date(publicationData.date) : existingBlog.publicationData?.date || new Date(),
358
610
  },
359
- seo: seo || {},
360
- slug: finalSlug,
361
611
  authorId: userId,
612
+ languages: updatedLanguages,
613
+ metadata: {
614
+ ...existingBlog.metadata,
615
+ lang: primaryLanguage,
616
+ },
362
617
  updatedAt: new Date(),
363
618
  };
364
619
 
@@ -366,7 +621,7 @@ export async function PUT_BY_SLUG(
366
621
 
367
622
  return NextResponse.json({
368
623
  message: 'Blog updated successfully',
369
- slug: finalSlug,
624
+ slug: slug, // Return original slug, not finalSlug
370
625
  });
371
626
  } catch (err: any) {
372
627
  console.error('[BlogAPI] PUT_BY_SLUG error:', err);
@@ -14,6 +14,8 @@ export interface UseBlogsOptions {
14
14
  skip?: number;
15
15
  /** Filter by status (published, draft, concept) */
16
16
  status?: string;
17
+ /** Filter by language */
18
+ language?: string;
17
19
  /** Whether to fetch all posts for admin (includes drafts) */
18
20
  admin?: boolean;
19
21
  /** API base URL (default: '/api/blogs') */
@@ -46,6 +48,7 @@ export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
46
48
  limit = 10,
47
49
  skip = 0,
48
50
  status,
51
+ language,
49
52
  admin = false,
50
53
  apiBaseUrl = '/api/plugin-blog',
51
54
  } = options;
@@ -56,6 +59,13 @@ export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
56
59
  const [total, setTotal] = useState(0);
57
60
 
58
61
  const fetchBlogs = async () => {
62
+ // If language is expected but not provided, wait for it to be ready
63
+ // (Prevents fetching all languages if the hook is called with a language but it's not yet loaded)
64
+ if (!language && !admin) {
65
+ // In public view, we always want a specific language
66
+ return;
67
+ }
68
+
59
69
  try {
60
70
  setLoading(true);
61
71
  setError(null);
@@ -64,6 +74,7 @@ export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
64
74
  if (limit) params.set('limit', limit.toString());
65
75
  if (skip) params.set('skip', skip.toString());
66
76
  if (status) params.set('status', status);
77
+ if (language) params.set('language', language);
67
78
  if (admin) params.set('admin', 'true');
68
79
 
69
80
  const url = `${apiBaseUrl}?${params.toString()}`;
@@ -110,7 +121,7 @@ export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
110
121
 
111
122
  useEffect(() => {
112
123
  fetchBlogs();
113
- }, [limit, skip, status, admin, apiBaseUrl]);
124
+ }, [limit, skip, status, language, admin, apiBaseUrl]);
114
125
 
115
126
  return {
116
127
  blogs,
package/src/index.tsx CHANGED
@@ -186,9 +186,10 @@ export default function BlogPlugin(props: PluginProps) {
186
186
  if (!originalSlug) {
187
187
  throw new Error('Cannot save: no post identifier available. Please reload the page.');
188
188
  }
189
- console.log('[BlogPlugin] Saving post with slug:', originalSlug);
189
+ const language = state.currentLanguage || locale;
190
+ console.log('[BlogPlugin] Saving post with slug:', originalSlug, 'language:', language);
190
191
  const apiData = editorStateToAPI(state, undefined, heroBlock);
191
- const response = await fetch(`/api/plugin-blog/${originalSlug}`, {
192
+ const response = await fetch(`/api/plugin-blog/${originalSlug}?language=${language}`, {
192
193
  method: 'PUT',
193
194
  headers: { 'Content-Type': 'application/json' },
194
195
  credentials: 'include', // Include cookies for authentication
@@ -229,8 +230,9 @@ export default function BlogPlugin(props: PluginProps) {
229
230
  backgroundColors={backgroundColors}
230
231
  onSave={async (state) => {
231
232
  // Save to API - create new post
233
+ const language = state.currentLanguage || locale;
232
234
  const apiData = editorStateToAPI(state);
233
- const response = await fetch('/api/plugin-blog/new', {
235
+ const response = await fetch(`/api/plugin-blog/new?language=${language}`, {
234
236
  method: 'POST',
235
237
  headers: { 'Content-Type': 'application/json' },
236
238
  credentials: 'include', // Include cookies for authentication
@@ -293,6 +295,8 @@ export type {
293
295
  BlogPost,
294
296
  PostListItem,
295
297
  PostFilterOptions,
298
+ BlogPostLanguageContent,
299
+ BlogPostLanguages,
296
300
  } from './types/post';
297
301
 
298
302
  // Export initialization utility for easy setup
@@ -0,0 +1,78 @@
1
+ /**
2
+ * i18n Utility
3
+ * Simple translation loader for the blog plugin
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ function getLocalesDir(): string {
10
+ const possiblePaths = [
11
+ path.join(process.cwd(), 'node_modules', '@jhits', 'plugin-blog', 'data', 'locales'),
12
+ path.join(__dirname, '..', 'data', 'locales'),
13
+ path.join(__dirname, '..', '..', 'data', 'locales'),
14
+ ];
15
+
16
+ for (const localesPath of possiblePaths) {
17
+ if (fs.existsSync(localesPath)) {
18
+ return localesPath;
19
+ }
20
+ }
21
+
22
+ return possiblePaths[0];
23
+ }
24
+
25
+ const localesDir = getLocalesDir();
26
+ const cache: Record<string, any> = {};
27
+
28
+ function loadLocale(locale: string): any {
29
+ if (cache[locale]) {
30
+ return cache[locale];
31
+ }
32
+
33
+ try {
34
+ const localePath = path.join(localesDir, locale, 'common.json');
35
+
36
+ if (fs.existsSync(localePath)) {
37
+ const content = fs.readFileSync(localePath, 'utf-8');
38
+ cache[locale] = JSON.parse(content);
39
+ return cache[locale];
40
+ }
41
+ } catch (error) {
42
+ console.error(`[i18n] Failed to load locale ${locale}:`, error);
43
+ }
44
+
45
+ return null;
46
+ }
47
+
48
+ export function getTranslations(locale: string): any {
49
+ const translations = loadLocale(locale);
50
+
51
+ if (translations) {
52
+ return translations;
53
+ }
54
+
55
+ const fallback = loadLocale('en');
56
+ return fallback || {};
57
+ }
58
+
59
+ export function getEditorTranslations(language: string): {
60
+ language: string;
61
+ selectLanguage: string;
62
+ addLanguage: string;
63
+ switchLanguage: string;
64
+ availableLanguages: string;
65
+ primaryLanguage: string;
66
+ } {
67
+ const translations = getTranslations(language);
68
+ const enTranslations = getTranslations('en');
69
+
70
+ return translations.editor || enTranslations.editor || {
71
+ language: 'Language',
72
+ selectLanguage: 'Select Language',
73
+ addLanguage: 'Add Language',
74
+ switchLanguage: 'Switch Language',
75
+ availableLanguages: 'Available Languages',
76
+ primaryLanguage: 'Primary Language',
77
+ };
78
+ }
@@ -43,6 +43,25 @@ export interface APIBlogDocument {
43
43
  authorId?: string;
44
44
  createdAt?: string | Date;
45
45
  updatedAt?: string | Date;
46
+ // Multilingual fields
47
+ languages?: {
48
+ [key: string]: {
49
+ blocks: Block[];
50
+ metadata: {
51
+ title?: string;
52
+ excerpt?: string;
53
+ featuredImage?: any;
54
+ categories?: string[];
55
+ tags?: string[];
56
+ seo?: any;
57
+ };
58
+ };
59
+ };
60
+ availableLanguages?: string[];
61
+ lang?: string;
62
+ metadata?: {
63
+ lang?: string;
64
+ };
46
65
  }
47
66
 
48
67
  /**
@@ -108,6 +127,7 @@ export function apiToBlogPost(doc: APIBlogDocument): BlogPost {
108
127
  seo,
109
128
  publication,
110
129
  metadata,
130
+ languages: doc.languages,
111
131
  createdAt: doc.createdAt
112
132
  ? (typeof doc.createdAt === 'string' ? doc.createdAt : doc.createdAt.toISOString())
113
133
  : new Date().toISOString(),
@@ -652,12 +652,12 @@ export function editorReducer(
652
652
  const post = action.payload;
653
653
  return {
654
654
  ...state,
655
- blocks: post.blocks,
656
- title: post.title,
657
- slug: post.slug,
658
- seo: post.seo,
659
- metadata: post.metadata,
660
- status: post.publication.status,
655
+ blocks: post.blocks || [],
656
+ title: post.title || '',
657
+ slug: post.slug || '',
658
+ seo: post.seo || {},
659
+ metadata: post.metadata || {},
660
+ status: post.publication?.status || 'draft',
661
661
  postId: post.id,
662
662
  isDirty: false,
663
663
  selectedBlockId: null,
@@ -681,6 +681,12 @@ export function editorReducer(
681
681
  isDirty: true,
682
682
  };
683
683
 
684
+ case 'SET_CURRENT_LANGUAGE':
685
+ return {
686
+ ...state,
687
+ currentLanguage: action.payload,
688
+ };
689
+
684
690
  case 'UNDO':
685
691
  case 'REDO':
686
692
  case 'SAVE_HISTORY':
@@ -43,6 +43,9 @@ export interface EditorState {
43
43
 
44
44
  /** Post ID (if editing existing post) */
45
45
  postId: string | null;
46
+
47
+ /** Currently selected language for editing */
48
+ currentLanguage: string | null;
46
49
  }
47
50
 
48
51
  /**
@@ -68,6 +71,7 @@ export type EditorAction =
68
71
  | { type: 'RESET_EDITOR' }
69
72
  | { type: 'MARK_CLEAN' }
70
73
  | { type: 'MARK_DIRTY' }
74
+ | { type: 'SET_CURRENT_LANGUAGE'; payload: string }
71
75
  | { type: 'UNDO' }
72
76
  | { type: 'REDO' }
73
77
  | { type: 'SAVE_HISTORY' };
@@ -156,5 +160,6 @@ export const initialEditorState: EditorState = {
156
160
  selectedBlockId: null,
157
161
  draggedBlockId: null,
158
162
  postId: null,
163
+ currentLanguage: null,
159
164
  };
160
165