@djangocfg/nextjs 2.1.109 → 2.1.111

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 (60) hide show
  1. package/README.md +176 -7
  2. package/dist/config/index.d.mts +16 -1
  3. package/dist/config/index.mjs +83 -14
  4. package/dist/config/index.mjs.map +1 -1
  5. package/dist/i18n/client.d.mts +123 -0
  6. package/dist/i18n/client.mjs +104 -0
  7. package/dist/i18n/client.mjs.map +1 -0
  8. package/dist/i18n/components.d.mts +22 -0
  9. package/dist/i18n/components.mjs +133 -0
  10. package/dist/i18n/components.mjs.map +1 -0
  11. package/dist/i18n/index.d.mts +17 -0
  12. package/dist/i18n/index.mjs +269 -0
  13. package/dist/i18n/index.mjs.map +1 -0
  14. package/dist/i18n/navigation.d.mts +1095 -0
  15. package/dist/i18n/navigation.mjs +45 -0
  16. package/dist/i18n/navigation.mjs.map +1 -0
  17. package/dist/i18n/plugin.d.mts +41 -0
  18. package/dist/i18n/plugin.mjs +17 -0
  19. package/dist/i18n/plugin.mjs.map +1 -0
  20. package/dist/i18n/provider.d.mts +18 -0
  21. package/dist/i18n/provider.mjs +54 -0
  22. package/dist/i18n/provider.mjs.map +1 -0
  23. package/dist/i18n/proxy.d.mts +40 -0
  24. package/dist/i18n/proxy.mjs +42 -0
  25. package/dist/i18n/proxy.mjs.map +1 -0
  26. package/dist/i18n/request.d.mts +42 -0
  27. package/dist/i18n/request.mjs +63 -0
  28. package/dist/i18n/request.mjs.map +1 -0
  29. package/dist/i18n/routing.d.mts +79 -0
  30. package/dist/i18n/routing.mjs +33 -0
  31. package/dist/i18n/routing.mjs.map +1 -0
  32. package/dist/i18n/server.d.mts +90 -0
  33. package/dist/i18n/server.mjs +79 -0
  34. package/dist/i18n/server.mjs.map +1 -0
  35. package/dist/index.d.mts +3 -1
  36. package/dist/index.mjs +176 -30
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/sitemap/index.d.mts +22 -3
  39. package/dist/sitemap/index.mjs +92 -15
  40. package/dist/sitemap/index.mjs.map +1 -1
  41. package/dist/types-Cy349X20.d.mts +60 -0
  42. package/package.json +54 -4
  43. package/src/config/constants.ts +1 -0
  44. package/src/config/createNextConfig.ts +39 -17
  45. package/src/i18n/client.ts +221 -0
  46. package/src/i18n/components/LocaleSwitcher.tsx +124 -0
  47. package/src/i18n/components/index.ts +7 -0
  48. package/src/i18n/index.ts +149 -0
  49. package/src/i18n/navigation.ts +90 -0
  50. package/src/i18n/plugin.ts +66 -0
  51. package/src/i18n/provider.tsx +91 -0
  52. package/src/i18n/proxy.ts +81 -0
  53. package/src/i18n/request.ts +141 -0
  54. package/src/i18n/routing.ts +84 -0
  55. package/src/i18n/server.ts +175 -0
  56. package/src/i18n/types.ts +88 -0
  57. package/src/sitemap/generator.ts +84 -9
  58. package/src/sitemap/index.ts +1 -1
  59. package/src/sitemap/route.ts +71 -8
  60. package/src/sitemap/types.ts +9 -0
@@ -5,11 +5,19 @@ import { S as SitemapUrl, C as ChangeFreq } from '../types-CwhXnEbK.mjs';
5
5
  * Sitemap types
6
6
  */
7
7
 
8
+ interface SitemapI18nOptions {
9
+ /** Supported locales (e.g., ['en', 'ru', 'ko']) */
10
+ locales: string[];
11
+ /** Default locale for x-default hreflang */
12
+ defaultLocale: string;
13
+ }
8
14
  interface SitemapGeneratorOptions {
9
15
  siteUrl: string;
10
16
  staticPages?: SitemapUrl[];
11
17
  dynamicPages?: (() => Promise<SitemapUrl[]>) | SitemapUrl[];
12
18
  cacheControl?: string;
19
+ /** i18n configuration for hreflang support */
20
+ i18n?: SitemapI18nOptions;
13
21
  }
14
22
  interface SitemapRoute {
15
23
  path: string;
@@ -33,7 +41,6 @@ interface SitemapRoute {
33
41
  * { loc: '/about', changefreq: 'monthly', priority: 0.8 },
34
42
  * ],
35
43
  * dynamicPages: async () => {
36
- * // Fetch dynamic pages from API
37
44
  * const posts = await fetchPosts();
38
45
  * return posts.map(post => ({
39
46
  * loc: `/posts/${post.slug}`,
@@ -42,6 +49,11 @@ interface SitemapRoute {
42
49
  * priority: 0.7,
43
50
  * }));
44
51
  * },
52
+ * // i18n support with hreflang
53
+ * i18n: {
54
+ * locales: ['en', 'ru', 'ko'],
55
+ * defaultLocale: 'en',
56
+ * },
45
57
  * });
46
58
  * ```
47
59
  */
@@ -52,15 +64,22 @@ declare function createSitemapHandler(options: SitemapGeneratorOptions): () => P
52
64
  * Sitemap Generator
53
65
  *
54
66
  * Generates XML sitemap from configuration
67
+ * Supports i18n with hreflang tags for multilingual sites
55
68
  */
56
69
 
70
+ interface GenerateSitemapXmlOptions {
71
+ urls: SitemapUrl[];
72
+ i18n?: SitemapI18nOptions;
73
+ siteUrl: string;
74
+ }
57
75
  /**
58
76
  * Generate XML sitemap string from URLs
77
+ * Supports i18n with hreflang alternate links
59
78
  */
60
- declare function generateSitemapXml(urls: SitemapUrl[]): string;
79
+ declare function generateSitemapXml(urlsOrOptions: SitemapUrl[] | GenerateSitemapXmlOptions): string;
61
80
  /**
62
81
  * Normalize URL (ensure absolute)
63
82
  */
64
83
  declare function normalizeUrl(url: string, siteUrl: string): string;
65
84
 
66
- export { type SitemapGeneratorOptions, type SitemapRoute, createSitemapHandler, generateSitemapXml, normalizeUrl };
85
+ export { type SitemapGeneratorOptions, type SitemapI18nOptions, type SitemapRoute, createSitemapHandler, generateSitemapXml, normalizeUrl };
@@ -2,22 +2,58 @@
2
2
  import { NextResponse } from "next/server";
3
3
 
4
4
  // src/sitemap/generator.ts
5
- function generateSitemapXml(urls) {
6
- return `<?xml version="1.0" encoding="UTF-8"?>
7
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
5
+ function generateSitemapXml(urlsOrOptions) {
6
+ const isOptionsObject = !Array.isArray(urlsOrOptions);
7
+ const urls = isOptionsObject ? urlsOrOptions.urls : urlsOrOptions;
8
+ const i18n = isOptionsObject ? urlsOrOptions.i18n : void 0;
9
+ const siteUrl = isOptionsObject ? urlsOrOptions.siteUrl : "";
10
+ const namespaces = i18n ? `xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
11
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
12
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
13
+ xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
14
+ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"` : `xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
8
15
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
9
16
  xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
10
- http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
11
- ${urls.map(
12
- ({ loc, lastmod, changefreq, priority }) => ` <url>
13
- <loc>${escapeXml(loc)}</loc>
17
+ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"`;
18
+ return `<?xml version="1.0" encoding="UTF-8"?>
19
+ <urlset ${namespaces}>
20
+ ${urls.map(({ loc, lastmod, changefreq, priority }) => {
21
+ const hreflangLinks = i18n ? generateHreflangLinks(loc, i18n, siteUrl) : "";
22
+ return ` <url>
23
+ <loc>${escapeXml(loc)}</loc>${hreflangLinks}
14
24
  ${lastmod ? `<lastmod>${formatDate(lastmod)}</lastmod>` : ""}
15
25
  ${changefreq ? `<changefreq>${changefreq}</changefreq>` : ""}
16
26
  ${priority !== void 0 ? `<priority>${priority.toFixed(1)}</priority>` : ""}
17
- </url>`
18
- ).join("\n")}
27
+ </url>`;
28
+ }).join("\n")}
19
29
  </urlset>`;
20
30
  }
31
+ function generateHreflangLinks(loc, i18n, siteUrl) {
32
+ const { locales, defaultLocale } = i18n;
33
+ const baseSiteUrl = siteUrl.endsWith("/") ? siteUrl.slice(0, -1) : siteUrl;
34
+ let path = loc.replace(baseSiteUrl, "");
35
+ for (const locale of locales) {
36
+ const localePrefix = `/${locale}`;
37
+ if (path === localePrefix || path.startsWith(`${localePrefix}/`)) {
38
+ path = path.slice(localePrefix.length) || "/";
39
+ break;
40
+ }
41
+ }
42
+ const links = [];
43
+ for (const locale of locales) {
44
+ const localePath = path === "/" ? `/${locale}` : `/${locale}${path}`;
45
+ const fullUrl = `${baseSiteUrl}${localePath}`;
46
+ links.push(
47
+ ` <xhtml:link rel="alternate" hreflang="${locale}" href="${escapeXml(fullUrl)}"/>`
48
+ );
49
+ }
50
+ const defaultPath = path === "/" ? `/${defaultLocale}` : `/${defaultLocale}${path}`;
51
+ const defaultUrl = `${baseSiteUrl}${defaultPath}`;
52
+ links.push(
53
+ ` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(defaultUrl)}"/>`
54
+ );
55
+ return "\n" + links.join("\n");
56
+ }
21
57
  function formatDate(date) {
22
58
  if (typeof date === "string") {
23
59
  return date;
@@ -42,7 +78,8 @@ function createSitemapHandler(options) {
42
78
  siteUrl,
43
79
  staticPages = [],
44
80
  dynamicPages = [],
45
- cacheControl = "public, s-maxage=86400, stale-while-revalidate"
81
+ cacheControl = "public, s-maxage=86400, stale-while-revalidate",
82
+ i18n
46
83
  } = options;
47
84
  return async function GET() {
48
85
  const urls = [...staticPages];
@@ -54,11 +91,20 @@ function createSitemapHandler(options) {
54
91
  urls.push(...dynamicPages);
55
92
  }
56
93
  }
57
- const normalizedUrls = urls.map((url) => ({
58
- ...url,
59
- loc: normalizeUrl(url.loc, siteUrl)
60
- }));
61
- const sitemap = generateSitemapXml(normalizedUrls);
94
+ let expandedUrls;
95
+ if (i18n) {
96
+ expandedUrls = expandUrlsForLocales(urls, i18n.locales, siteUrl);
97
+ } else {
98
+ expandedUrls = urls.map((url) => ({
99
+ ...url,
100
+ loc: normalizeUrl(url.loc, siteUrl)
101
+ }));
102
+ }
103
+ const sitemap = generateSitemapXml({
104
+ urls: expandedUrls,
105
+ i18n,
106
+ siteUrl
107
+ });
62
108
  return new NextResponse(sitemap, {
63
109
  status: 200,
64
110
  headers: {
@@ -68,6 +114,37 @@ function createSitemapHandler(options) {
68
114
  });
69
115
  };
70
116
  }
117
+ function expandUrlsForLocales(urls, locales, siteUrl) {
118
+ const baseSiteUrl = siteUrl.endsWith("/") ? siteUrl.slice(0, -1) : siteUrl;
119
+ const expandedUrls = [];
120
+ for (const url of urls) {
121
+ let path = url.loc;
122
+ if (path.startsWith(baseSiteUrl)) {
123
+ path = path.replace(baseSiteUrl, "");
124
+ }
125
+ if (!path.startsWith("/")) {
126
+ path = "/" + path;
127
+ }
128
+ const hasLocalePrefix = locales.some(
129
+ (locale) => path === `/${locale}` || path.startsWith(`/${locale}/`)
130
+ );
131
+ if (hasLocalePrefix) {
132
+ expandedUrls.push({
133
+ ...url,
134
+ loc: normalizeUrl(path, siteUrl)
135
+ });
136
+ } else {
137
+ for (const locale of locales) {
138
+ const localePath = path === "/" ? `/${locale}` : `/${locale}${path}`;
139
+ expandedUrls.push({
140
+ ...url,
141
+ loc: `${baseSiteUrl}${localePath}`
142
+ });
143
+ }
144
+ }
145
+ }
146
+ return expandedUrls;
147
+ }
71
148
  export {
72
149
  createSitemapHandler,
73
150
  generateSitemapXml,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sitemap/route.ts","../../src/sitemap/generator.ts"],"sourcesContent":["/**\n * Sitemap Route Handler for Next.js App Router\n *\n * Usage:\n * ```tsx\n * // app/sitemap.ts\n * import { createSitemapHandler } from '@djangocfg/nextjs/sitemap';\n *\n * export default createSitemapHandler({\n * siteUrl: 'https://example.com',\n * staticPages: [\n * { loc: '/', changefreq: 'daily', priority: 1.0 },\n * { loc: '/about', changefreq: 'monthly', priority: 0.8 },\n * ],\n * dynamicPages: async () => {\n * // Fetch dynamic pages from API\n * const posts = await fetchPosts();\n * return posts.map(post => ({\n * loc: `/posts/${post.slug}`,\n * lastmod: post.updatedAt,\n * changefreq: 'weekly',\n * priority: 0.7,\n * }));\n * },\n * });\n * ```\n */\n\nimport { NextResponse } from 'next/server';\n\nimport { generateSitemapXml, normalizeUrl } from './generator';\n\nimport type { SitemapGeneratorOptions } from './types';\nimport type { SitemapUrl } from '../types';\n\nexport function createSitemapHandler(options: SitemapGeneratorOptions) {\n const {\n siteUrl,\n staticPages = [],\n dynamicPages = [],\n cacheControl = 'public, s-maxage=86400, stale-while-revalidate',\n } = options;\n\n return async function GET() {\n const urls: SitemapUrl[] = [...staticPages];\n\n // Add dynamic pages\n if (dynamicPages) {\n if (typeof dynamicPages === 'function') {\n const dynamicUrls = await dynamicPages();\n urls.push(...dynamicUrls);\n } else {\n urls.push(...dynamicPages);\n }\n }\n\n // Normalize all URLs\n const normalizedUrls = urls.map((url) => ({\n ...url,\n loc: normalizeUrl(url.loc, siteUrl),\n }));\n\n // Generate XML\n const sitemap = generateSitemapXml(normalizedUrls);\n\n // Return response\n return new NextResponse(sitemap, {\n status: 200,\n headers: {\n 'Content-Type': 'application/xml',\n 'Cache-Control': cacheControl,\n },\n });\n };\n}\n\n","/**\n * Sitemap Generator\n *\n * Generates XML sitemap from configuration\n */\n\nimport type { SitemapUrl } from '../types';\n\n/**\n * Generate XML sitemap string from URLs\n */\nexport function generateSitemapXml(urls: SitemapUrl[]): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9\n http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\">\n${urls\n .map(\n ({ loc, lastmod, changefreq, priority }) => ` <url>\n <loc>${escapeXml(loc)}</loc>\n ${lastmod ? `<lastmod>${formatDate(lastmod)}</lastmod>` : ''}\n ${changefreq ? `<changefreq>${changefreq}</changefreq>` : ''}\n ${priority !== undefined ? `<priority>${priority.toFixed(1)}</priority>` : ''}\n </url>`\n )\n .join('\\n')}\n</urlset>`;\n}\n\n/**\n * Format date for sitemap (ISO 8601)\n */\nfunction formatDate(date: string | Date): string {\n if (typeof date === 'string') {\n return date;\n }\n return date.toISOString().split('T')[0];\n}\n\n/**\n * Escape XML special characters\n */\nfunction escapeXml(unsafe: string): string {\n return unsafe\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n/**\n * Normalize URL (ensure absolute)\n */\nexport function normalizeUrl(url: string, siteUrl: string): string {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n const baseUrl = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;\n const path = url.startsWith('/') ? url : `/${url}`;\n return `${baseUrl}${path}`;\n}\n\n"],"mappings":";AA4BA,SAAS,oBAAoB;;;ACjBtB,SAAS,mBAAmB,MAA4B;AAC7D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP,KACC;AAAA,IACC,CAAC,EAAE,KAAK,SAAS,YAAY,SAAS,MAAM;AAAA,WACrC,UAAU,GAAG,CAAC;AAAA,MACnB,UAAU,YAAY,WAAW,OAAO,CAAC,eAAe,EAAE;AAAA,MAC1D,aAAa,eAAe,UAAU,kBAAkB,EAAE;AAAA,MAC1D,aAAa,SAAY,aAAa,SAAS,QAAQ,CAAC,CAAC,gBAAgB,EAAE;AAAA;AAAA,EAE/E,EACC,KAAK,IAAI,CAAC;AAAA;AAEb;AAKA,SAAS,WAAW,MAA6B;AAC/C,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAO;AAAA,EACT;AACA,SAAO,KAAK,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AACxC;AAKA,SAAS,UAAU,QAAwB;AACzC,SAAO,OACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ;AAC3B;AAKO,SAAS,aAAa,KAAa,SAAyB;AACjE,MAAI,IAAI,WAAW,SAAS,KAAK,IAAI,WAAW,UAAU,GAAG;AAC3D,WAAO;AAAA,EACT;AACA,QAAM,UAAU,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,EAAE,IAAI;AAC/D,QAAM,OAAO,IAAI,WAAW,GAAG,IAAI,MAAM,IAAI,GAAG;AAChD,SAAO,GAAG,OAAO,GAAG,IAAI;AAC1B;;;AD3BO,SAAS,qBAAqB,SAAkC;AACrE,QAAM;AAAA,IACJ;AAAA,IACA,cAAc,CAAC;AAAA,IACf,eAAe,CAAC;AAAA,IAChB,eAAe;AAAA,EACjB,IAAI;AAEJ,SAAO,eAAe,MAAM;AAC1B,UAAM,OAAqB,CAAC,GAAG,WAAW;AAG1C,QAAI,cAAc;AAChB,UAAI,OAAO,iBAAiB,YAAY;AACtC,cAAM,cAAc,MAAM,aAAa;AACvC,aAAK,KAAK,GAAG,WAAW;AAAA,MAC1B,OAAO;AACL,aAAK,KAAK,GAAG,YAAY;AAAA,MAC3B;AAAA,IACF;AAGA,UAAM,iBAAiB,KAAK,IAAI,CAAC,SAAS;AAAA,MACxC,GAAG;AAAA,MACH,KAAK,aAAa,IAAI,KAAK,OAAO;AAAA,IACpC,EAAE;AAGF,UAAM,UAAU,mBAAmB,cAAc;AAGjD,WAAO,IAAI,aAAa,SAAS;AAAA,MAC/B,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,iBAAiB;AAAA,MACnB;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/sitemap/route.ts","../../src/sitemap/generator.ts"],"sourcesContent":["/**\n * Sitemap Route Handler for Next.js App Router\n *\n * Usage:\n * ```tsx\n * // app/sitemap.ts\n * import { createSitemapHandler } from '@djangocfg/nextjs/sitemap';\n *\n * export default createSitemapHandler({\n * siteUrl: 'https://example.com',\n * staticPages: [\n * { loc: '/', changefreq: 'daily', priority: 1.0 },\n * { loc: '/about', changefreq: 'monthly', priority: 0.8 },\n * ],\n * dynamicPages: async () => {\n * const posts = await fetchPosts();\n * return posts.map(post => ({\n * loc: `/posts/${post.slug}`,\n * lastmod: post.updatedAt,\n * changefreq: 'weekly',\n * priority: 0.7,\n * }));\n * },\n * // i18n support with hreflang\n * i18n: {\n * locales: ['en', 'ru', 'ko'],\n * defaultLocale: 'en',\n * },\n * });\n * ```\n */\n\nimport { NextResponse } from 'next/server';\n\nimport { generateSitemapXml, normalizeUrl } from './generator';\n\nimport type { SitemapGeneratorOptions } from './types';\nimport type { SitemapUrl } from '../types';\n\nexport function createSitemapHandler(options: SitemapGeneratorOptions) {\n const {\n siteUrl,\n staticPages = [],\n dynamicPages = [],\n cacheControl = 'public, s-maxage=86400, stale-while-revalidate',\n i18n,\n } = options;\n\n return async function GET() {\n const urls: SitemapUrl[] = [...staticPages];\n\n // Add dynamic pages\n if (dynamicPages) {\n if (typeof dynamicPages === 'function') {\n const dynamicUrls = await dynamicPages();\n urls.push(...dynamicUrls);\n } else {\n urls.push(...dynamicPages);\n }\n }\n\n // Expand URLs for each locale if i18n is enabled\n let expandedUrls: SitemapUrl[];\n if (i18n) {\n expandedUrls = expandUrlsForLocales(urls, i18n.locales, siteUrl);\n } else {\n expandedUrls = urls.map((url) => ({\n ...url,\n loc: normalizeUrl(url.loc, siteUrl),\n }));\n }\n\n // Generate XML with i18n support\n const sitemap = generateSitemapXml({\n urls: expandedUrls,\n i18n,\n siteUrl,\n });\n\n // Return response\n return new NextResponse(sitemap, {\n status: 200,\n headers: {\n 'Content-Type': 'application/xml',\n 'Cache-Control': cacheControl,\n },\n });\n };\n}\n\n/**\n * Expand URLs to include all locale variants\n * Input: [{ loc: '/page' }] with locales ['en', 'ru']\n * Output: [{ loc: 'https://example.com/en/page' }, { loc: 'https://example.com/ru/page' }]\n */\nfunction expandUrlsForLocales(\n urls: SitemapUrl[],\n locales: string[],\n siteUrl: string\n): SitemapUrl[] {\n const baseSiteUrl = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;\n const expandedUrls: SitemapUrl[] = [];\n\n for (const url of urls) {\n // Normalize the path (remove leading slash if present for consistency)\n let path = url.loc;\n if (path.startsWith(baseSiteUrl)) {\n path = path.replace(baseSiteUrl, '');\n }\n if (!path.startsWith('/')) {\n path = '/' + path;\n }\n\n // Check if the path already has a locale prefix\n const hasLocalePrefix = locales.some(\n (locale) => path === `/${locale}` || path.startsWith(`/${locale}/`)\n );\n\n if (hasLocalePrefix) {\n // URL already has locale prefix, just normalize it\n expandedUrls.push({\n ...url,\n loc: normalizeUrl(path, siteUrl),\n });\n } else {\n // Create a URL for each locale\n for (const locale of locales) {\n const localePath = path === '/' ? `/${locale}` : `/${locale}${path}`;\n expandedUrls.push({\n ...url,\n loc: `${baseSiteUrl}${localePath}`,\n });\n }\n }\n }\n\n return expandedUrls;\n}\n\n","/**\n * Sitemap Generator\n *\n * Generates XML sitemap from configuration\n * Supports i18n with hreflang tags for multilingual sites\n */\n\nimport type { SitemapUrl } from '../types';\nimport type { SitemapI18nOptions } from './types';\n\nexport interface GenerateSitemapXmlOptions {\n urls: SitemapUrl[];\n i18n?: SitemapI18nOptions;\n siteUrl: string;\n}\n\n/**\n * Generate XML sitemap string from URLs\n * Supports i18n with hreflang alternate links\n */\nexport function generateSitemapXml(\n urlsOrOptions: SitemapUrl[] | GenerateSitemapXmlOptions\n): string {\n // Support both old signature (just urls) and new signature (options object)\n const isOptionsObject = !Array.isArray(urlsOrOptions);\n const urls = isOptionsObject ? urlsOrOptions.urls : urlsOrOptions;\n const i18n = isOptionsObject ? urlsOrOptions.i18n : undefined;\n const siteUrl = isOptionsObject ? urlsOrOptions.siteUrl : '';\n\n // Add xhtml namespace if i18n is enabled\n const namespaces = i18n\n ? `xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9\n http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\"`\n : `xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9\n http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\"`;\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset ${namespaces}>\n${urls\n .map(({ loc, lastmod, changefreq, priority }) => {\n const hreflangLinks = i18n\n ? generateHreflangLinks(loc, i18n, siteUrl)\n : '';\n\n return ` <url>\n <loc>${escapeXml(loc)}</loc>${hreflangLinks}\n ${lastmod ? `<lastmod>${formatDate(lastmod)}</lastmod>` : ''}\n ${changefreq ? `<changefreq>${changefreq}</changefreq>` : ''}\n ${priority !== undefined ? `<priority>${priority.toFixed(1)}</priority>` : ''}\n </url>`;\n })\n .join('\\n')}\n</urlset>`;\n}\n\n/**\n * Generate hreflang links for a URL\n */\nfunction generateHreflangLinks(\n loc: string,\n i18n: SitemapI18nOptions,\n siteUrl: string\n): string {\n const { locales, defaultLocale } = i18n;\n\n // Extract the path without locale prefix from the URL\n // e.g., https://example.com/en/page -> /page\n const baseSiteUrl = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;\n let path = loc.replace(baseSiteUrl, '');\n\n // Remove locale prefix if present\n for (const locale of locales) {\n const localePrefix = `/${locale}`;\n if (path === localePrefix || path.startsWith(`${localePrefix}/`)) {\n path = path.slice(localePrefix.length) || '/';\n break;\n }\n }\n\n const links: string[] = [];\n\n // Add hreflang for each locale\n for (const locale of locales) {\n const localePath = path === '/' ? `/${locale}` : `/${locale}${path}`;\n const fullUrl = `${baseSiteUrl}${localePath}`;\n links.push(\n ` <xhtml:link rel=\"alternate\" hreflang=\"${locale}\" href=\"${escapeXml(fullUrl)}\"/>`\n );\n }\n\n // Add x-default pointing to default locale\n const defaultPath = path === '/' ? `/${defaultLocale}` : `/${defaultLocale}${path}`;\n const defaultUrl = `${baseSiteUrl}${defaultPath}`;\n links.push(\n ` <xhtml:link rel=\"alternate\" hreflang=\"x-default\" href=\"${escapeXml(defaultUrl)}\"/>`\n );\n\n return '\\n' + links.join('\\n');\n}\n\n/**\n * Format date for sitemap (ISO 8601)\n */\nfunction formatDate(date: string | Date): string {\n if (typeof date === 'string') {\n return date;\n }\n return date.toISOString().split('T')[0];\n}\n\n/**\n * Escape XML special characters\n */\nfunction escapeXml(unsafe: string): string {\n return unsafe\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n/**\n * Normalize URL (ensure absolute)\n */\nexport function normalizeUrl(url: string, siteUrl: string): string {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n const baseUrl = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;\n const path = url.startsWith('/') ? url : `/${url}`;\n return `${baseUrl}${path}`;\n}\n\n"],"mappings":"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","names":[]}
@@ -0,0 +1,60 @@
1
+ import { LocaleCode, I18nTranslations } from '@djangocfg/i18n';
2
+
3
+ /**
4
+ * i18n Types for Next.js App Router
5
+ *
6
+ * Integrates next-intl with @djangocfg/i18n
7
+ */
8
+
9
+ interface I18nConfig {
10
+ /** Supported locales */
11
+ locales: LocaleCode[];
12
+ /** Default locale (fallback) */
13
+ defaultLocale: LocaleCode;
14
+ /** Locale prefix strategy */
15
+ localePrefix?: 'always' | 'as-needed' | 'never';
16
+ /** Cookie name for locale preference */
17
+ cookieName?: string;
18
+ /** Paths to exclude from locale routing (e.g., '/api', '/_next') */
19
+ excludedPaths?: string[];
20
+ }
21
+ interface I18nPluginOptions extends I18nConfig {
22
+ /** Path to i18n request config file (default: './src/i18n/request.ts' or './i18n/request.ts') */
23
+ requestConfig?: string;
24
+ }
25
+ /** Messages structure - compatible with next-intl */
26
+ type Messages = I18nTranslations & {
27
+ [namespace: string]: Record<string, unknown>;
28
+ };
29
+ /** Locale-specific messages loader */
30
+ type MessagesLoader = (locale: LocaleCode) => Promise<Messages> | Messages;
31
+ /** Extension translations that can be merged */
32
+ interface ExtensionMessages {
33
+ namespace: string;
34
+ messages: Record<LocaleCode, Record<string, unknown>>;
35
+ }
36
+ interface I18nProviderProps {
37
+ /** Current locale */
38
+ locale: LocaleCode;
39
+ /** Translation messages */
40
+ messages: Messages;
41
+ /** Time zone for date/time formatting */
42
+ timeZone?: string;
43
+ /** Now value for relative time */
44
+ now?: Date;
45
+ /** Children */
46
+ children: React.ReactNode;
47
+ }
48
+ interface LocaleParams {
49
+ locale: string;
50
+ }
51
+ interface LocaleLayoutProps {
52
+ children: React.ReactNode;
53
+ params: Promise<LocaleParams>;
54
+ }
55
+ interface LocalePageProps {
56
+ params: Promise<LocaleParams>;
57
+ searchParams?: Promise<Record<string, string | string[] | undefined>>;
58
+ }
59
+
60
+ export type { ExtensionMessages as E, I18nConfig as I, LocaleParams as L, Messages as M, I18nPluginOptions as a, I18nProviderProps as b, MessagesLoader as c, LocaleLayoutProps as d, LocalePageProps as e };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "2.1.109",
3
+ "version": "2.1.111",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -98,6 +98,46 @@
98
98
  "types": "./dist/pwa/server/routes.d.mts",
99
99
  "import": "./dist/pwa/server/routes.mjs",
100
100
  "default": "./dist/pwa/server/routes.mjs"
101
+ },
102
+ "./i18n": {
103
+ "types": "./dist/i18n/index.d.mts",
104
+ "import": "./dist/i18n/index.mjs",
105
+ "default": "./dist/i18n/index.mjs"
106
+ },
107
+ "./i18n/server": {
108
+ "types": "./dist/i18n/server.d.mts",
109
+ "import": "./dist/i18n/server.mjs",
110
+ "default": "./dist/i18n/server.mjs"
111
+ },
112
+ "./i18n/client": {
113
+ "types": "./dist/i18n/client.d.mts",
114
+ "import": "./dist/i18n/client.mjs",
115
+ "default": "./dist/i18n/client.mjs"
116
+ },
117
+ "./i18n/proxy": {
118
+ "types": "./dist/i18n/proxy.d.mts",
119
+ "import": "./dist/i18n/proxy.mjs",
120
+ "default": "./dist/i18n/proxy.mjs"
121
+ },
122
+ "./i18n/navigation": {
123
+ "types": "./dist/i18n/navigation.d.mts",
124
+ "import": "./dist/i18n/navigation.mjs",
125
+ "default": "./dist/i18n/navigation.mjs"
126
+ },
127
+ "./i18n/routing": {
128
+ "types": "./dist/i18n/routing.d.mts",
129
+ "import": "./dist/i18n/routing.mjs",
130
+ "default": "./dist/i18n/routing.mjs"
131
+ },
132
+ "./i18n/request": {
133
+ "types": "./dist/i18n/request.d.mts",
134
+ "import": "./dist/i18n/request.mjs",
135
+ "default": "./dist/i18n/request.mjs"
136
+ },
137
+ "./i18n/components": {
138
+ "types": "./dist/i18n/components.d.mts",
139
+ "import": "./dist/i18n/components.mjs",
140
+ "default": "./dist/i18n/components.mjs"
101
141
  }
102
142
  },
103
143
  "files": [
@@ -120,22 +160,32 @@
120
160
  "pwa": "tsx src/pwa/cli.ts"
121
161
  },
122
162
  "peerDependencies": {
163
+ "@djangocfg/i18n": "^2.1.111",
164
+ "@djangocfg/ui-core": "^2.1.111",
123
165
  "next": "^16.0.10"
124
166
  },
167
+ "peerDependenciesMeta": {
168
+ "@djangocfg/ui-core": {
169
+ "optional": true
170
+ }
171
+ },
125
172
  "dependencies": {
126
173
  "@serwist/next": "^9.2.3",
127
174
  "@serwist/sw": "^9.2.3",
128
175
  "chalk": "^5.3.0",
129
176
  "conf": "^15.0.2",
130
177
  "consola": "^3.4.2",
178
+ "next-intl": "^4.1.0",
131
179
  "semver": "^7.7.3",
132
180
  "serwist": "^9.2.3",
133
181
  "web-push": "^3.6.7"
134
182
  },
135
183
  "devDependencies": {
136
- "@djangocfg/imgai": "^2.1.109",
137
- "@djangocfg/layouts": "^2.1.109",
138
- "@djangocfg/typescript-config": "^2.1.109",
184
+ "@djangocfg/i18n": "^2.1.111",
185
+ "@djangocfg/ui-core": "^2.1.111",
186
+ "@djangocfg/imgai": "^2.1.111",
187
+ "@djangocfg/layouts": "^2.1.111",
188
+ "@djangocfg/typescript-config": "^2.1.111",
139
189
  "@types/node": "^24.7.2",
140
190
  "@types/react": "19.2.2",
141
191
  "@types/react-dom": "19.2.1",
@@ -33,6 +33,7 @@ export const DJANGOCFG_PACKAGES = [
33
33
  // Default packages to transpile
34
34
  // Required for proper RSC (React Server Components) handling
35
35
  export const DEFAULT_TRANSPILE_PACKAGES = [
36
+ '@djangocfg/i18n',
36
37
  '@djangocfg/ui-core',
37
38
  '@djangocfg/ui-nextjs',
38
39
  '@djangocfg/layouts',
@@ -24,6 +24,7 @@
24
24
  import type { NextConfig } from 'next';
25
25
  import type { Configuration as WebpackConfig } from 'webpack';
26
26
 
27
+ import { type I18nPluginOptions, withI18n } from '../i18n/plugin';
27
28
  import { type PWAPluginOptions, withPWA } from '../pwa/plugin';
28
29
  import { DEFAULT_OPTIMIZE_PACKAGES, DEFAULT_TRANSPILE_PACKAGES } from './constants';
29
30
  import { addCompressionPlugins } from './plugins/compression';
@@ -63,6 +64,19 @@ export interface BaseNextConfigOptions {
63
64
  * @default { enabled: true (in production), disable: true (in development) }
64
65
  */
65
66
  pwa?: PWAPluginOptions | false;
67
+ /**
68
+ * i18n configuration options for internationalization
69
+ * Enables URL-based locale routing (e.g., /en/about, /ru/about)
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * i18n: {
74
+ * locales: ['en', 'ru', 'ko'],
75
+ * defaultLocale: 'en',
76
+ * }
77
+ * ```
78
+ */
79
+ i18n?: I18nPluginOptions;
66
80
  /** Turbopack configuration (Next.js 16+ default bundler) */
67
81
  turbopack?: NextConfig['turbopack'];
68
82
  /** Custom webpack configuration function (called after base webpack logic) */
@@ -282,27 +296,35 @@ export function createBaseNextConfig(
282
296
  },
283
297
  };
284
298
 
285
- // Deep merge user options with base config
286
- let finalConfig = deepMerge(baseConfig, options);
287
-
288
- // Cleanup: Remove custom options that are not part of NextConfig
289
- delete (finalConfig as any).optimizePackageImports;
290
- delete (finalConfig as any).isDefaultCfgAdmin;
291
- delete (finalConfig as any).checkUpdates;
292
- delete (finalConfig as any).autoUpdate;
293
- delete (finalConfig as any).forceCheckWorkspace;
294
- delete (finalConfig as any).checkPackages;
295
- delete (finalConfig as any).autoInstall;
296
- delete (finalConfig as any).allowIframeFrom;
297
- delete (finalConfig as any).openBrowser;
298
- // Note: turbopack is a valid NextConfig option, don't delete it
299
+ // Extract plugin options before merge (to avoid conflicts with Next.js config)
300
+ const {
301
+ i18n: i18nOptions,
302
+ pwa: pwaOptions,
303
+ optimizePackageImports,
304
+ isDefaultCfgAdmin,
305
+ checkUpdates,
306
+ autoUpdate,
307
+ forceCheckWorkspace,
308
+ checkPackages,
309
+ autoInstall,
310
+ allowIframeFrom,
311
+ ...nextConfigOptions
312
+ } = options;
313
+
314
+ // Deep merge only valid Next.js options with base config
315
+ let finalConfig = deepMerge(baseConfig, nextConfigOptions);
299
316
 
300
317
  // Apply PWA wrapper only if explicitly configured (opt-in)
301
318
  // PWA requires sw.ts file in the app, so apps must explicitly enable it
302
- if (options.pwa) {
303
- finalConfig = withPWA(finalConfig, options.pwa);
319
+ if (pwaOptions) {
320
+ finalConfig = withPWA(finalConfig, pwaOptions);
321
+ }
322
+
323
+ // Apply i18n wrapper if configured
324
+ // Enables next-intl for locale routing and translations
325
+ if (i18nOptions) {
326
+ finalConfig = withI18n(finalConfig, i18nOptions);
304
327
  }
305
- delete (finalConfig as any).pwa;
306
328
 
307
329
  return finalConfig;
308
330
  }