@meursyphus/i18n-llm 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 MeurSyphus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # i18n-llm
2
+
3
+ LLM-friendly Next.js i18n library with type-safe translations.
4
+
5
+ ## Features
6
+
7
+ - **LLM-readable documentation**: Copy-paste `llm.txt` to set up i18n instantly
8
+ - **Type-safe translations**: Full TypeScript support with key autocomplete
9
+ - **Variable interpolation**: `t("greeting", { name: "John" })`
10
+ - **Zero dependencies**: Only requires Next.js and React
11
+ - **App Router only**: Built specifically for Next.js 14+ App Router
12
+ - **Server & Client components**: Works seamlessly in both
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @meursyphus/i18n-llm
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### 1. Initialize (CLI)
23
+
24
+ ```bash
25
+ npx i18n-llm init --locales en,ko --default en
26
+ ```
27
+
28
+ Or follow the manual setup in [llm.txt](./llm.txt).
29
+
30
+ ### 2. Wrap your app
31
+
32
+ ```tsx
33
+ // app/[lang]/layout.tsx
34
+ import { TranslationsProvider } from '@meursyphus/i18n-llm';
35
+
36
+ export default async function RootLayout({ children, params }) {
37
+ const { lang } = await params;
38
+ return (
39
+ <html lang={lang}>
40
+ <body>
41
+ <TranslationsProvider locale={lang}>
42
+ {children}
43
+ </TranslationsProvider>
44
+ </body>
45
+ </html>
46
+ );
47
+ }
48
+ ```
49
+
50
+ ### 3. Use translations
51
+
52
+ **Server Component:**
53
+ ```tsx
54
+ import { getTranslations } from '@meursyphus/i18n-llm';
55
+
56
+ export default async function Page({ params }) {
57
+ const { lang } = await params;
58
+ const t = await getTranslations('common', lang);
59
+
60
+ return <h1>{t('greeting', { name: 'World' })}</h1>;
61
+ }
62
+ ```
63
+
64
+ **Client Component:**
65
+ ```tsx
66
+ 'use client';
67
+ import { useTranslations } from '@meursyphus/i18n-llm/client';
68
+
69
+ export function Greeting({ name }) {
70
+ const t = useTranslations('common');
71
+ return <p>{t('greeting', { name })}</p>;
72
+ }
73
+ ```
74
+
75
+ ## API Reference
76
+
77
+ ### Server Exports (`i18n-llm`)
78
+
79
+ | Export | Description |
80
+ |--------|-------------|
81
+ | `getTranslations(namespace, locale)` | Get translation function for server components |
82
+ | `TranslationsProvider` | Provider component for client components |
83
+ | `defineI18nConfig(config)` | Define i18n configuration |
84
+ | `getPreferredLanguage(request)` | Get user's preferred language from request |
85
+
86
+ ### Client Exports (`i18n-llm/client`)
87
+
88
+ | Export | Description |
89
+ |--------|-------------|
90
+ | `useTranslations(namespace)` | Hook to get translation function |
91
+ | `useCurrentLanguage()` | Hook to get current locale |
92
+
93
+ ### Middleware (`i18n-llm/middleware`)
94
+
95
+ | Export | Description |
96
+ |--------|-------------|
97
+ | `defineMiddleware(config)` | Configure i18n middleware |
98
+ | `middleware` | The middleware function to export |
99
+
100
+ ### Actions (`i18n-llm/actions`)
101
+
102
+ | Export | Description |
103
+ |--------|-------------|
104
+ | `setLanguagePreference(locale, path)` | Server action to change language |
105
+ | `getLanguagePreference()` | Get stored language preference |
106
+
107
+ ## Configuration
108
+
109
+ ### i18n.config.ts
110
+
111
+ ```typescript
112
+ import { defineI18nConfig } from '@meursyphus/i18n-llm';
113
+
114
+ export default defineI18nConfig({
115
+ defaultLocale: 'en',
116
+ locales: ['en', 'ko', 'ja'] as const,
117
+ messagesPath: './messages',
118
+ });
119
+ ```
120
+
121
+ ### middleware.ts
122
+
123
+ ```typescript
124
+ import { defineMiddleware, middleware } from '@meursyphus/i18n-llm/middleware';
125
+
126
+ defineMiddleware({
127
+ locales: ['en', 'ko', 'ja'],
128
+ defaultLocale: 'en',
129
+ });
130
+
131
+ export { middleware };
132
+
133
+ export const config = {
134
+ matcher: ['/((?!api|_next|.*\\..*).*)'],
135
+ };
136
+ ```
137
+
138
+ ## Message Files
139
+
140
+ ### Structure
141
+
142
+ ```
143
+ messages/
144
+ ├── types.ts # Type definitions
145
+ ├── en/
146
+ │ └── index.ts # English translations
147
+ ├── ko/
148
+ │ └── index.ts # Korean translations
149
+ └── ja/
150
+ └── index.ts # Japanese translations
151
+ ```
152
+
153
+ ### Type Definition
154
+
155
+ ```typescript
156
+ // messages/types.ts
157
+ export interface Messages {
158
+ common: {
159
+ title: string;
160
+ greeting: string; // "Hello, {name}!"
161
+ nav: {
162
+ home: string;
163
+ about: string;
164
+ };
165
+ };
166
+ }
167
+ ```
168
+
169
+ ### Translation File
170
+
171
+ ```typescript
172
+ // messages/en/index.ts
173
+ import type { Messages } from '../types';
174
+
175
+ const messages: Messages = {
176
+ common: {
177
+ title: 'Welcome',
178
+ greeting: 'Hello, {name}!',
179
+ nav: {
180
+ home: 'Home',
181
+ about: 'About',
182
+ },
183
+ },
184
+ };
185
+
186
+ export default messages;
187
+ ```
188
+
189
+ ## Variable Interpolation
190
+
191
+ Use `{variableName}` placeholders in your translations:
192
+
193
+ ```typescript
194
+ // Message
195
+ "greeting": "Hello, {name}! You have {count} messages."
196
+
197
+ // Usage
198
+ t('greeting', { name: 'John', count: 5 });
199
+ // Output: "Hello, John! You have 5 messages."
200
+ ```
201
+
202
+ ## Language Switcher
203
+
204
+ ```tsx
205
+ 'use client';
206
+ import { useCurrentLanguage } from '@meursyphus/i18n-llm/client';
207
+ import { setLanguagePreference } from '@meursyphus/i18n-llm/actions';
208
+ import { usePathname } from 'next/navigation';
209
+
210
+ export function LanguageSwitcher() {
211
+ const currentLang = useCurrentLanguage();
212
+ const pathname = usePathname();
213
+
214
+ const handleChange = async (locale: string) => {
215
+ await setLanguagePreference(locale, pathname);
216
+ };
217
+
218
+ return (
219
+ <select value={currentLang} onChange={(e) => handleChange(e.target.value)}>
220
+ <option value="en">English</option>
221
+ <option value="ko">한국어</option>
222
+ <option value="ja">日本語</option>
223
+ </select>
224
+ );
225
+ }
226
+ ```
227
+
228
+ ## CLI Commands
229
+
230
+ ```bash
231
+ # Initialize i18n-llm in your project
232
+ npx i18n-llm init --locales en,ko --default en
233
+
234
+ # Add a new locale
235
+ npx i18n-llm add-locale ja --name "日本語"
236
+ ```
237
+
238
+ ## LLM Setup
239
+
240
+ For AI-assisted setup, copy the contents of [llm.txt](./llm.txt) to your AI assistant.
241
+
242
+ ## License
243
+
244
+ MIT
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Server action to set the user's language preference.
3
+ * Updates the cookie and redirects to the new locale path.
4
+ *
5
+ * @example
6
+ * 'use client';
7
+ * import { setLanguagePreference } from 'i18n-llm/actions';
8
+ *
9
+ * function LanguageSwitcher() {
10
+ * const handleChange = async (locale: string) => {
11
+ * await setLanguagePreference(locale, window.location.pathname);
12
+ * };
13
+ *
14
+ * return (
15
+ * <select onChange={(e) => handleChange(e.target.value)}>
16
+ * <option value="en">English</option>
17
+ * <option value="ko">한국어</option>
18
+ * </select>
19
+ * );
20
+ * }
21
+ */
22
+ declare function setLanguagePreference(locale: string, currentPath: string): Promise<void>;
23
+ /**
24
+ * Gets the user's stored language preference from cookies.
25
+ * Returns null if no preference is set.
26
+ */
27
+ declare function getLanguagePreference(): Promise<string | null>;
28
+
29
+ export { getLanguagePreference, setLanguagePreference };
@@ -0,0 +1,37 @@
1
+ "use server";
2
+ import {
3
+ getI18nConfig
4
+ } from "./chunk-OTLHL53Z.js";
5
+
6
+ // src/actions.ts
7
+ import { cookies } from "next/headers";
8
+ import { redirect } from "next/navigation";
9
+ var LANGUAGE_COOKIE_NAME = "preferred-language";
10
+ var COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
11
+ async function setLanguagePreference(locale, currentPath) {
12
+ const config = getI18nConfig();
13
+ if (config && !config.locales.includes(locale)) {
14
+ throw new Error(`i18n-llm: Unsupported locale: ${locale}`);
15
+ }
16
+ const cookieStore = await cookies();
17
+ cookieStore.set(LANGUAGE_COOKIE_NAME, locale, {
18
+ httpOnly: true,
19
+ secure: process.env.NODE_ENV === "production",
20
+ sameSite: "lax",
21
+ maxAge: COOKIE_MAX_AGE,
22
+ path: "/"
23
+ });
24
+ const pathSegments = currentPath.split("/");
25
+ pathSegments[1] = locale;
26
+ const newPath = pathSegments.join("/");
27
+ redirect(newPath);
28
+ }
29
+ async function getLanguagePreference() {
30
+ const cookieStore = await cookies();
31
+ const cookie = cookieStore.get(LANGUAGE_COOKIE_NAME);
32
+ return cookie?.value || null;
33
+ }
34
+ export {
35
+ getLanguagePreference,
36
+ setLanguagePreference
37
+ };
@@ -0,0 +1,75 @@
1
+ // src/shared/load-translations.ts
2
+ var cachedConfig = null;
3
+ function setI18nConfig(config) {
4
+ cachedConfig = config;
5
+ }
6
+ function getI18nConfig() {
7
+ return cachedConfig;
8
+ }
9
+ async function loadTranslations(locale) {
10
+ if (!cachedConfig) {
11
+ throw new Error(
12
+ "i18n-llm: Configuration not set. Call setI18nConfig() first or use defineI18nConfig() in your i18n.config.ts file."
13
+ );
14
+ }
15
+ const { defaultLocale, locales, messagesPath } = cachedConfig;
16
+ const isSupported = locales.includes(locale);
17
+ const localeToLoad = isSupported ? locale : defaultLocale;
18
+ try {
19
+ const messages = await import(
20
+ /* @vite-ignore */
21
+ `${messagesPath}/${localeToLoad}`
22
+ ).then((module) => module.default);
23
+ return messages;
24
+ } catch (error) {
25
+ console.error(
26
+ `i18n-llm: Failed to load messages for locale: ${localeToLoad}`,
27
+ error
28
+ );
29
+ if (localeToLoad !== defaultLocale) {
30
+ const fallbackMessages = await import(
31
+ /* @vite-ignore */
32
+ `${messagesPath}/${defaultLocale}`
33
+ ).then((module) => module.default);
34
+ return fallbackMessages;
35
+ }
36
+ throw new Error(
37
+ `i18n-llm: Failed to load default locale messages (${defaultLocale}).`
38
+ );
39
+ }
40
+ }
41
+ function createMessageLoader(messagesPath, defaultLocale, locales) {
42
+ return async (locale) => {
43
+ const isSupported = locales.includes(locale);
44
+ const localeToLoad = isSupported ? locale : defaultLocale;
45
+ try {
46
+ const messages = await import(
47
+ /* @vite-ignore */
48
+ `${messagesPath}/${localeToLoad}`
49
+ ).then((module) => module.default);
50
+ return messages;
51
+ } catch (error) {
52
+ console.error(
53
+ `i18n-llm: Failed to load messages for locale: ${localeToLoad}`,
54
+ error
55
+ );
56
+ if (localeToLoad !== defaultLocale) {
57
+ const fallbackMessages = await import(
58
+ /* @vite-ignore */
59
+ `${messagesPath}/${defaultLocale}`
60
+ ).then((module) => module.default);
61
+ return fallbackMessages;
62
+ }
63
+ throw new Error(
64
+ `i18n-llm: Failed to load default locale messages (${defaultLocale}).`
65
+ );
66
+ }
67
+ };
68
+ }
69
+
70
+ export {
71
+ setI18nConfig,
72
+ getI18nConfig,
73
+ loadTranslations,
74
+ createMessageLoader
75
+ };
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // cli/commands/init.ts
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ function init(options) {
10
+ const locales = options.locales.split(",").map((l) => l.trim());
11
+ const defaultLocale = options.default;
12
+ console.log("\n\u{1F30D} Initializing i18n-llm...\n");
13
+ const dirs = ["messages", ...locales.map((l) => `messages/${l}`)];
14
+ for (const dir of dirs) {
15
+ if (!fs.existsSync(dir)) {
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ console.log(` \u2713 Created ${dir}/`);
18
+ }
19
+ }
20
+ const configContent = `import { defineI18nConfig } from '@meursyphus/i18n-llm';
21
+
22
+ export default defineI18nConfig({
23
+ defaultLocale: '${defaultLocale}',
24
+ locales: [${locales.map((l) => `'${l}'`).join(", ")}] as const,
25
+ messagesPath: './messages',
26
+ });
27
+
28
+ export type Locale = ${locales.map((l) => `'${l}'`).join(" | ")};
29
+ `;
30
+ fs.writeFileSync("i18n.config.ts", configContent);
31
+ console.log(" \u2713 Created i18n.config.ts");
32
+ const middlewareContent = `import { defineMiddleware, middleware } from '@meursyphus/i18n-llm/middleware';
33
+
34
+ defineMiddleware({
35
+ locales: [${locales.map((l) => `'${l}'`).join(", ")}],
36
+ defaultLocale: '${defaultLocale}',
37
+ });
38
+
39
+ export { middleware };
40
+
41
+ export const config = {
42
+ matcher: ['/((?!api|_next|.*\\\\..*).*)'],
43
+ };
44
+ `;
45
+ fs.writeFileSync("middleware.ts", middlewareContent);
46
+ console.log(" \u2713 Created middleware.ts");
47
+ const typesContent = `export interface Messages {
48
+ common: CommonMessages;
49
+ }
50
+
51
+ export interface CommonMessages {
52
+ title: string;
53
+ greeting: string; // "Hello, {name}!"
54
+ }
55
+ `;
56
+ fs.writeFileSync("messages/types.ts", typesContent);
57
+ console.log(" \u2713 Created messages/types.ts");
58
+ const messageTemplates = {
59
+ en: `import type { Messages } from '../types';
60
+
61
+ const messages: Messages = {
62
+ common: {
63
+ title: 'Welcome',
64
+ greeting: 'Hello, {name}!',
65
+ },
66
+ };
67
+
68
+ export default messages;
69
+ `,
70
+ ko: `import type { Messages } from '../types';
71
+
72
+ const messages: Messages = {
73
+ common: {
74
+ title: '\uD658\uC601\uD569\uB2C8\uB2E4',
75
+ greeting: '\uC548\uB155\uD558\uC138\uC694, {name}\uB2D8!',
76
+ },
77
+ };
78
+
79
+ export default messages;
80
+ `,
81
+ ja: `import type { Messages } from '../types';
82
+
83
+ const messages: Messages = {
84
+ common: {
85
+ title: '\u3088\u3046\u3053\u305D',
86
+ greeting: '\u3053\u3093\u306B\u3061\u306F\u3001{name}\u3055\u3093\uFF01',
87
+ },
88
+ };
89
+
90
+ export default messages;
91
+ `,
92
+ zh: `import type { Messages } from '../types';
93
+
94
+ const messages: Messages = {
95
+ common: {
96
+ title: '\u6B22\u8FCE',
97
+ greeting: '\u4F60\u597D\uFF0C{name}\uFF01',
98
+ },
99
+ };
100
+
101
+ export default messages;
102
+ `
103
+ };
104
+ const defaultTemplate = `import type { Messages } from '../types';
105
+
106
+ const messages: Messages = {
107
+ common: {
108
+ title: 'Welcome',
109
+ greeting: 'Hello, {name}!',
110
+ },
111
+ };
112
+
113
+ export default messages;
114
+ `;
115
+ for (const locale of locales) {
116
+ const content = messageTemplates[locale] || defaultTemplate;
117
+ const filePath = path.join("messages", locale, "index.ts");
118
+ fs.writeFileSync(filePath, content);
119
+ console.log(` \u2713 Created ${filePath}`);
120
+ }
121
+ console.log(`
122
+ \u2705 i18n-llm initialized successfully!
123
+
124
+ Next steps:
125
+ 1. Move your app/ contents to app/[lang]/
126
+ 2. Update your root layout to use TranslationsProvider:
127
+
128
+ import { TranslationsProvider } from '@meursyphus/i18n-llm';
129
+
130
+ export default async function RootLayout({ children, params }) {
131
+ const { lang } = await params;
132
+ return (
133
+ <html lang={lang}>
134
+ <body>
135
+ <TranslationsProvider locale={lang}>
136
+ {children}
137
+ </TranslationsProvider>
138
+ </body>
139
+ </html>
140
+ );
141
+ }
142
+
143
+ 3. Use translations in your components:
144
+
145
+ // Server Component
146
+ import { getTranslations } from '@meursyphus/i18n-llm';
147
+ const t = await getTranslations('common', lang);
148
+
149
+ // Client Component
150
+ import { useTranslations } from '@meursyphus/i18n-llm/client';
151
+ const t = useTranslations('common');
152
+ `);
153
+ }
154
+
155
+ // cli/commands/add-locale.ts
156
+ import * as fs2 from "fs";
157
+ import * as path2 from "path";
158
+ function addLocale(locale, options) {
159
+ const displayName = options.name || locale.toUpperCase();
160
+ console.log(`
161
+ \u{1F30D} Adding locale: ${locale} (${displayName})
162
+ `);
163
+ if (!fs2.existsSync("messages")) {
164
+ console.error("\u274C Error: messages/ directory not found.");
165
+ console.error(" Run 'npx i18n-llm init' first to set up your project.");
166
+ process.exit(1);
167
+ }
168
+ const localeDir = path2.join("messages", locale);
169
+ if (fs2.existsSync(localeDir)) {
170
+ console.error(`\u274C Error: Locale '${locale}' already exists.`);
171
+ process.exit(1);
172
+ }
173
+ fs2.mkdirSync(localeDir, { recursive: true });
174
+ console.log(` \u2713 Created ${localeDir}/`);
175
+ const existingLocales = fs2.readdirSync("messages").filter(
176
+ (f) => fs2.statSync(path2.join("messages", f)).isDirectory() && f !== locale
177
+ );
178
+ if (existingLocales.length > 0) {
179
+ const sourceLocale = existingLocales[0];
180
+ const sourceDir = path2.join("messages", sourceLocale);
181
+ const files = fs2.readdirSync(sourceDir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
182
+ for (const file of files) {
183
+ const sourcePath = path2.join(sourceDir, file);
184
+ const targetPath = path2.join(localeDir, file);
185
+ const content = fs2.readFileSync(sourcePath, "utf-8");
186
+ const modifiedContent = `// TODO: Translate this file to ${displayName}
187
+ ${content}`;
188
+ fs2.writeFileSync(targetPath, modifiedContent);
189
+ console.log(` \u2713 Created ${targetPath}`);
190
+ }
191
+ } else {
192
+ const defaultContent = `// TODO: Translate this file to ${displayName}
193
+ import type { Messages } from '../types';
194
+
195
+ const messages: Messages = {
196
+ common: {
197
+ title: 'Welcome',
198
+ greeting: 'Hello, {name}!',
199
+ },
200
+ };
201
+
202
+ export default messages;
203
+ `;
204
+ fs2.writeFileSync(path2.join(localeDir, "index.ts"), defaultContent);
205
+ console.log(` \u2713 Created ${path2.join(localeDir, "index.ts")}`);
206
+ }
207
+ console.log(`
208
+ \u2705 Locale '${locale}' added successfully!
209
+
210
+ Next steps:
211
+ 1. Update your i18n.config.ts to include '${locale}' in the locales array
212
+ 2. Update your middleware.ts to include '${locale}' in the locales array
213
+ 3. Translate the message files in messages/${locale}/
214
+ `);
215
+ }
216
+
217
+ // cli/index.ts
218
+ var program = new Command();
219
+ program.name("i18n-llm").description("LLM-friendly i18n library for Next.js").version("0.1.0");
220
+ program.command("init").description("Initialize i18n-llm in your Next.js project").option("-l, --locales <locales>", "Comma-separated list of locales", "en,ko").option("-d, --default <locale>", "Default locale", "en").action(init);
221
+ program.command("add-locale").description("Add a new locale to your project").argument("<locale>", "Locale code to add (e.g., ja, zh, fr)").option("-n, --name <name>", "Display name for the locale").action(addLocale);
222
+ program.parse();