@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 +21 -0
- package/README.md +244 -0
- package/dist/actions.d.ts +29 -0
- package/dist/actions.js +37 -0
- package/dist/chunk-OTLHL53Z.js +75 -0
- package/dist/cli/index.js +222 -0
- package/dist/client.js +86 -0
- package/dist/index-KVK-m3p1.d.ts +29 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +122 -0
- package/dist/middleware.d.ts +31 -0
- package/dist/middleware.js +75 -0
- package/llm.txt +254 -0
- package/package.json +70 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/client/use-translations.ts
|
|
5
|
+
import { useContext } from "react";
|
|
6
|
+
|
|
7
|
+
// src/client/translations-context.tsx
|
|
8
|
+
import { createContext } from "react";
|
|
9
|
+
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
var TranslationsContext = createContext({
|
|
11
|
+
messages: {},
|
|
12
|
+
locale: ""
|
|
13
|
+
});
|
|
14
|
+
function ClientTranslationsProvider({
|
|
15
|
+
messages,
|
|
16
|
+
locale,
|
|
17
|
+
children
|
|
18
|
+
}) {
|
|
19
|
+
return /* @__PURE__ */ jsx(TranslationsContext.Provider, { value: { messages, locale }, children });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/shared/interpolate.ts
|
|
23
|
+
function interpolate(template, variables) {
|
|
24
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
25
|
+
if (Object.prototype.hasOwnProperty.call(variables, key)) {
|
|
26
|
+
return String(variables[key]);
|
|
27
|
+
}
|
|
28
|
+
return match;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/shared/nested-key-utils.ts
|
|
33
|
+
function getNestedValue(obj, path) {
|
|
34
|
+
const keys = path.split(".");
|
|
35
|
+
let value = obj;
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
if (value === null || value === void 0) return void 0;
|
|
38
|
+
value = value[key];
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function isReactNode(value) {
|
|
43
|
+
return value !== null && value !== void 0 && (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "object" && value.$$typeof !== void 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/client/use-translations.ts
|
|
47
|
+
function useTranslations(namespace) {
|
|
48
|
+
const { messages } = useContext(TranslationsContext);
|
|
49
|
+
function t(key, variables) {
|
|
50
|
+
const namespaceMessages = messages[namespace];
|
|
51
|
+
const value = getNestedValue(namespaceMessages, key);
|
|
52
|
+
if (value === void 0 || value === null) {
|
|
53
|
+
console.warn(
|
|
54
|
+
`i18n-llm: Translation for key "${String(namespace)}.${key}" not found.`
|
|
55
|
+
);
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
if (variables) {
|
|
60
|
+
return interpolate(value, variables);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (isReactNode(value)) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
console.warn(
|
|
68
|
+
`i18n-llm: Translation for key "${String(namespace)}.${key}" is not a valid type.`
|
|
69
|
+
);
|
|
70
|
+
return key;
|
|
71
|
+
}
|
|
72
|
+
return t;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/client/use-current-language.ts
|
|
76
|
+
import { useContext as useContext2 } from "react";
|
|
77
|
+
function useCurrentLanguage() {
|
|
78
|
+
const { locale } = useContext2(TranslationsContext);
|
|
79
|
+
return locale;
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
ClientTranslationsProvider,
|
|
83
|
+
TranslationsContext,
|
|
84
|
+
useCurrentLanguage,
|
|
85
|
+
useTranslations
|
|
86
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface I18nConfig {
|
|
4
|
+
defaultLocale: string;
|
|
5
|
+
locales: readonly string[];
|
|
6
|
+
messagesPath: string;
|
|
7
|
+
}
|
|
8
|
+
type Messages = Record<string, unknown>;
|
|
9
|
+
type NestedKeyOf<TObj extends object> = {
|
|
10
|
+
[Key in keyof TObj & (string | number)]: TObj[Key] extends object ? `${Key}` | `${Key}.${NestedKeyOf<TObj[Key]>}` : `${Key}`;
|
|
11
|
+
}[keyof TObj & (string | number)];
|
|
12
|
+
type NestedValueOf<TObj extends object, TPath extends string> = TPath extends `${infer Key}.${infer Rest}` ? Key extends keyof TObj ? TObj[Key] extends object ? NestedValueOf<TObj[Key], Rest> : never : never : TPath extends keyof TObj ? TObj[TPath] : never;
|
|
13
|
+
type InterpolationVariables = Record<string, string | number>;
|
|
14
|
+
type TranslationValue = string | ReactNode;
|
|
15
|
+
interface TranslationsContextType<TMessages = Messages> {
|
|
16
|
+
messages: TMessages;
|
|
17
|
+
locale: string;
|
|
18
|
+
}
|
|
19
|
+
interface MiddlewareConfig {
|
|
20
|
+
locales: string[];
|
|
21
|
+
defaultLocale: string;
|
|
22
|
+
localePrefix?: "always" | "as-needed" | "never";
|
|
23
|
+
redirects?: Array<{
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type { InterpolationVariables as I, Messages as M, NestedKeyOf as N, TranslationValue as T, NestedValueOf as a, I18nConfig as b, MiddlewareConfig as c, TranslationsContextType as d };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { M as Messages, N as NestedKeyOf, I as InterpolationVariables, a as NestedValueOf, b as I18nConfig } from './index-KVK-m3p1.js';
|
|
2
|
+
export { c as MiddlewareConfig, T as TranslationValue, d as TranslationsContextType } from './index-KVK-m3p1.js';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
5
|
+
import { NextRequest } from 'next/server';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Gets translations for a specific namespace in server components.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // In a Server Component
|
|
12
|
+
* const t = await getTranslations('common', lang);
|
|
13
|
+
* return <h1>{t('title')}</h1>;
|
|
14
|
+
*
|
|
15
|
+
* // With variables
|
|
16
|
+
* return <p>{t('greeting', { name: 'World' })}</p>;
|
|
17
|
+
*/
|
|
18
|
+
declare function getTranslations<TMessages extends Messages, Namespace extends keyof TMessages>(namespace: Namespace, locale: string): Promise<(<Key extends NestedKeyOf<TMessages[Namespace] & object>>(key: Key, variables?: InterpolationVariables) => NestedValueOf<TMessages[Namespace] & object, Key> extends string ? string : NestedValueOf<TMessages[Namespace] & object, Key> extends ReactNode ? ReactNode : string)>;
|
|
19
|
+
|
|
20
|
+
interface TranslationsProviderProps {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
locale: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Server-side translations provider.
|
|
26
|
+
* Loads translations and passes them to the client context.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // In your root layout
|
|
30
|
+
* export default async function RootLayout({ children, params }) {
|
|
31
|
+
* const { lang } = await params;
|
|
32
|
+
* return (
|
|
33
|
+
* <html lang={lang}>
|
|
34
|
+
* <body>
|
|
35
|
+
* <TranslationsProvider locale={lang}>
|
|
36
|
+
* {children}
|
|
37
|
+
* </TranslationsProvider>
|
|
38
|
+
* </body>
|
|
39
|
+
* </html>
|
|
40
|
+
* );
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
declare function TranslationsProvider<TMessages extends Messages = Messages>({ children, locale }: TranslationsProviderProps): Promise<react_jsx_runtime.JSX.Element>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gets the user's preferred language from the request.
|
|
47
|
+
* Checks in order: cookie preference, Accept-Language header, default locale.
|
|
48
|
+
*/
|
|
49
|
+
declare function getPreferredLanguage(request: NextRequest): string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sets the i18n configuration for message loading.
|
|
53
|
+
* This should be called once at application startup.
|
|
54
|
+
*/
|
|
55
|
+
declare function setI18nConfig(config: {
|
|
56
|
+
defaultLocale: string;
|
|
57
|
+
locales: string[];
|
|
58
|
+
messagesPath: string;
|
|
59
|
+
}): void;
|
|
60
|
+
/**
|
|
61
|
+
* Gets the current i18n configuration.
|
|
62
|
+
*/
|
|
63
|
+
declare function getI18nConfig(): {
|
|
64
|
+
defaultLocale: string;
|
|
65
|
+
locales: string[];
|
|
66
|
+
messagesPath: string;
|
|
67
|
+
} | null;
|
|
68
|
+
/**
|
|
69
|
+
* Loads translation messages for a specific locale.
|
|
70
|
+
* Falls back to the default locale if the requested locale is not found.
|
|
71
|
+
*/
|
|
72
|
+
declare function loadTranslations<T extends Messages = Messages>(locale: string): Promise<T>;
|
|
73
|
+
/**
|
|
74
|
+
* Creates a message loader function bound to a specific messages path.
|
|
75
|
+
* Use this if you need custom message loading logic.
|
|
76
|
+
*/
|
|
77
|
+
declare function createMessageLoader<T extends Messages = Messages>(messagesPath: string, defaultLocale: string, locales: string[]): (locale: string) => Promise<T>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Helper function to define and initialize i18n configuration.
|
|
81
|
+
* Call this in your i18n.config.ts file.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // i18n.config.ts
|
|
85
|
+
* import { defineI18nConfig } from 'i18n-llm';
|
|
86
|
+
*
|
|
87
|
+
* export default defineI18nConfig({
|
|
88
|
+
* defaultLocale: 'en',
|
|
89
|
+
* locales: ['en', 'ko', 'ja'] as const,
|
|
90
|
+
* messagesPath: './messages',
|
|
91
|
+
* });
|
|
92
|
+
*/
|
|
93
|
+
declare function defineI18nConfig<T extends readonly string[]>(config: {
|
|
94
|
+
defaultLocale: T[number];
|
|
95
|
+
locales: T;
|
|
96
|
+
messagesPath: string;
|
|
97
|
+
}): I18nConfig;
|
|
98
|
+
|
|
99
|
+
export { I18nConfig, InterpolationVariables, Messages, NestedKeyOf, NestedValueOf, TranslationsProvider, createMessageLoader, defineI18nConfig, getI18nConfig, getPreferredLanguage, getTranslations, loadTranslations, setI18nConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMessageLoader,
|
|
3
|
+
getI18nConfig,
|
|
4
|
+
loadTranslations,
|
|
5
|
+
setI18nConfig
|
|
6
|
+
} from "./chunk-OTLHL53Z.js";
|
|
7
|
+
|
|
8
|
+
// src/shared/interpolate.ts
|
|
9
|
+
function interpolate(template, variables) {
|
|
10
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
11
|
+
if (Object.prototype.hasOwnProperty.call(variables, key)) {
|
|
12
|
+
return String(variables[key]);
|
|
13
|
+
}
|
|
14
|
+
return match;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/shared/nested-key-utils.ts
|
|
19
|
+
function getNestedValue(obj, path) {
|
|
20
|
+
const keys = path.split(".");
|
|
21
|
+
let value = obj;
|
|
22
|
+
for (const key of keys) {
|
|
23
|
+
if (value === null || value === void 0) return void 0;
|
|
24
|
+
value = value[key];
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/server/get-translations.ts
|
|
30
|
+
async function getTranslations(namespace, locale) {
|
|
31
|
+
const messages = await loadTranslations(locale);
|
|
32
|
+
function t(key, variables) {
|
|
33
|
+
const namespaceMessages = messages[namespace];
|
|
34
|
+
const value = getNestedValue(namespaceMessages, key);
|
|
35
|
+
if (value === void 0 || value === null) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`i18n-llm: Translation for key "${String(namespace)}.${key}" not found.`
|
|
38
|
+
);
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === "string" && variables) {
|
|
42
|
+
return interpolate(value, variables);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
return t;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/client/translations-context.tsx
|
|
50
|
+
import { createContext } from "react";
|
|
51
|
+
import { jsx } from "react/jsx-runtime";
|
|
52
|
+
var TranslationsContext = createContext({
|
|
53
|
+
messages: {},
|
|
54
|
+
locale: ""
|
|
55
|
+
});
|
|
56
|
+
function ClientTranslationsProvider({
|
|
57
|
+
messages,
|
|
58
|
+
locale,
|
|
59
|
+
children
|
|
60
|
+
}) {
|
|
61
|
+
return /* @__PURE__ */ jsx(TranslationsContext.Provider, { value: { messages, locale }, children });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/server/translations-provider.tsx
|
|
65
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
66
|
+
async function TranslationsProvider({ children, locale }) {
|
|
67
|
+
const messages = await loadTranslations(locale);
|
|
68
|
+
return /* @__PURE__ */ jsx2(ClientTranslationsProvider, { messages, locale, children });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/server/get-preferred-language.ts
|
|
72
|
+
var LANGUAGE_COOKIE_NAME = "preferred-language";
|
|
73
|
+
function getPreferredLanguage(request) {
|
|
74
|
+
const config = getI18nConfig();
|
|
75
|
+
if (!config) {
|
|
76
|
+
console.warn(
|
|
77
|
+
"i18n-llm: Configuration not set. Returning 'en' as default."
|
|
78
|
+
);
|
|
79
|
+
return "en";
|
|
80
|
+
}
|
|
81
|
+
const { defaultLocale, locales } = config;
|
|
82
|
+
const cookieValue = request.cookies.get(LANGUAGE_COOKIE_NAME)?.value;
|
|
83
|
+
if (cookieValue && locales.includes(cookieValue)) {
|
|
84
|
+
return cookieValue;
|
|
85
|
+
}
|
|
86
|
+
const acceptLanguage = request.headers.get("accept-language");
|
|
87
|
+
if (acceptLanguage) {
|
|
88
|
+
const langs = acceptLanguage.split(",").map((lang) => lang.split(";")[0]);
|
|
89
|
+
for (const lang of langs) {
|
|
90
|
+
const shortLang = lang.slice(0, 2).toLowerCase();
|
|
91
|
+
if (locales.includes(shortLang)) {
|
|
92
|
+
return shortLang;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return defaultLocale;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/index.ts
|
|
100
|
+
function defineI18nConfig(config) {
|
|
101
|
+
const i18nConfig = {
|
|
102
|
+
defaultLocale: config.defaultLocale,
|
|
103
|
+
locales: config.locales,
|
|
104
|
+
messagesPath: config.messagesPath
|
|
105
|
+
};
|
|
106
|
+
setI18nConfig({
|
|
107
|
+
defaultLocale: config.defaultLocale,
|
|
108
|
+
locales: [...config.locales],
|
|
109
|
+
messagesPath: config.messagesPath
|
|
110
|
+
});
|
|
111
|
+
return i18nConfig;
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
TranslationsProvider,
|
|
115
|
+
createMessageLoader,
|
|
116
|
+
defineI18nConfig,
|
|
117
|
+
getI18nConfig,
|
|
118
|
+
getPreferredLanguage,
|
|
119
|
+
getTranslations,
|
|
120
|
+
loadTranslations,
|
|
121
|
+
setI18nConfig
|
|
122
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { c as MiddlewareConfig } from './index-KVK-m3p1.js';
|
|
3
|
+
import 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Defines the i18n middleware configuration.
|
|
7
|
+
* Call this once in your middleware.ts file.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // middleware.ts
|
|
11
|
+
* import { defineMiddleware, middleware } from 'i18n-llm/middleware';
|
|
12
|
+
*
|
|
13
|
+
* defineMiddleware({
|
|
14
|
+
* locales: ['en', 'ko', 'ja'],
|
|
15
|
+
* defaultLocale: 'en',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* export { middleware };
|
|
19
|
+
*
|
|
20
|
+
* export const config = {
|
|
21
|
+
* matcher: ['/((?!api|_next|.*\\..*).*)'],
|
|
22
|
+
* };
|
|
23
|
+
*/
|
|
24
|
+
declare function defineMiddleware(config: MiddlewareConfig): void;
|
|
25
|
+
/**
|
|
26
|
+
* The i18n middleware function.
|
|
27
|
+
* Make sure to call defineMiddleware() before exporting this.
|
|
28
|
+
*/
|
|
29
|
+
declare function middleware(request: NextRequest): NextResponse;
|
|
30
|
+
|
|
31
|
+
export { defineMiddleware, middleware };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
var LANGUAGE_COOKIE_NAME = "preferred-language";
|
|
4
|
+
var middlewareConfig = null;
|
|
5
|
+
function defineMiddleware(config) {
|
|
6
|
+
middlewareConfig = config;
|
|
7
|
+
}
|
|
8
|
+
function middleware(request) {
|
|
9
|
+
if (!middlewareConfig) {
|
|
10
|
+
console.error(
|
|
11
|
+
"i18n-llm: Middleware not configured. Call defineMiddleware() first."
|
|
12
|
+
);
|
|
13
|
+
return NextResponse.next();
|
|
14
|
+
}
|
|
15
|
+
const {
|
|
16
|
+
locales,
|
|
17
|
+
defaultLocale,
|
|
18
|
+
localePrefix = "always",
|
|
19
|
+
redirects = []
|
|
20
|
+
} = middlewareConfig;
|
|
21
|
+
const { pathname } = request.nextUrl;
|
|
22
|
+
if (pathname.startsWith("/_next") || pathname.startsWith("/api") || pathname.includes(".")) {
|
|
23
|
+
return NextResponse.next();
|
|
24
|
+
}
|
|
25
|
+
const pathnameLocale = locales.find(
|
|
26
|
+
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
|
|
27
|
+
);
|
|
28
|
+
const preferredLocale = getPreferredLocale(request, locales, defaultLocale);
|
|
29
|
+
if (pathname === "/") {
|
|
30
|
+
if (localePrefix === "always") {
|
|
31
|
+
return NextResponse.redirect(
|
|
32
|
+
new URL(`/${preferredLocale}`, request.url)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return NextResponse.next();
|
|
36
|
+
}
|
|
37
|
+
if (!pathnameLocale) {
|
|
38
|
+
if (localePrefix === "always") {
|
|
39
|
+
return NextResponse.redirect(
|
|
40
|
+
new URL(`/${preferredLocale}${pathname}`, request.url)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return NextResponse.next();
|
|
44
|
+
}
|
|
45
|
+
for (const redirect of redirects) {
|
|
46
|
+
const pathWithoutLocale = pathname.replace(`/${pathnameLocale}`, "");
|
|
47
|
+
if (pathWithoutLocale === redirect.from) {
|
|
48
|
+
return NextResponse.redirect(
|
|
49
|
+
new URL(`/${pathnameLocale}${redirect.to}`, request.url)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return NextResponse.next();
|
|
54
|
+
}
|
|
55
|
+
function getPreferredLocale(request, locales, defaultLocale) {
|
|
56
|
+
const cookieValue = request.cookies.get(LANGUAGE_COOKIE_NAME)?.value;
|
|
57
|
+
if (cookieValue && locales.includes(cookieValue)) {
|
|
58
|
+
return cookieValue;
|
|
59
|
+
}
|
|
60
|
+
const acceptLanguage = request.headers.get("accept-language");
|
|
61
|
+
if (acceptLanguage) {
|
|
62
|
+
const langs = acceptLanguage.split(",").map((lang) => lang.split(";")[0]);
|
|
63
|
+
for (const lang of langs) {
|
|
64
|
+
const shortLang = lang.slice(0, 2).toLowerCase();
|
|
65
|
+
if (locales.includes(shortLang)) {
|
|
66
|
+
return shortLang;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return defaultLocale;
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
defineMiddleware,
|
|
74
|
+
middleware
|
|
75
|
+
};
|
package/llm.txt
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# i18n-llm Setup Guide
|
|
2
|
+
|
|
3
|
+
You are setting up i18n-llm for a Next.js App Router project.
|
|
4
|
+
Follow these steps exactly in order.
|
|
5
|
+
|
|
6
|
+
## Step 1: Install the package
|
|
7
|
+
|
|
8
|
+
Run this command:
|
|
9
|
+
```bash
|
|
10
|
+
npm install @meursyphus/i18n-llm
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Step 2: Create i18n.config.ts in project root
|
|
14
|
+
|
|
15
|
+
Create file `i18n.config.ts`:
|
|
16
|
+
```typescript
|
|
17
|
+
import { defineI18nConfig } from '@meursyphus/i18n-llm';
|
|
18
|
+
|
|
19
|
+
export default defineI18nConfig({
|
|
20
|
+
defaultLocale: 'en',
|
|
21
|
+
locales: ['en', 'ko'] as const,
|
|
22
|
+
messagesPath: './messages',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type Locale = 'en' | 'ko';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Step 3: Create middleware.ts in project root
|
|
29
|
+
|
|
30
|
+
Create file `middleware.ts`:
|
|
31
|
+
```typescript
|
|
32
|
+
import { defineMiddleware, middleware } from '@meursyphus/i18n-llm/middleware';
|
|
33
|
+
|
|
34
|
+
defineMiddleware({
|
|
35
|
+
locales: ['en', 'ko'],
|
|
36
|
+
defaultLocale: 'en',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export { middleware };
|
|
40
|
+
|
|
41
|
+
export const config = {
|
|
42
|
+
matcher: ['/((?!api|_next|.*\\..*).*)'],
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Step 4: Create messages directory structure
|
|
47
|
+
|
|
48
|
+
Create these files:
|
|
49
|
+
|
|
50
|
+
`messages/types.ts`:
|
|
51
|
+
```typescript
|
|
52
|
+
export interface Messages {
|
|
53
|
+
common: {
|
|
54
|
+
title: string;
|
|
55
|
+
greeting: string; // "Hello, {name}!"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`messages/en/index.ts`:
|
|
61
|
+
```typescript
|
|
62
|
+
import type { Messages } from '../types';
|
|
63
|
+
|
|
64
|
+
const messages: Messages = {
|
|
65
|
+
common: {
|
|
66
|
+
title: 'Welcome',
|
|
67
|
+
greeting: 'Hello, {name}!',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default messages;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`messages/ko/index.ts`:
|
|
75
|
+
```typescript
|
|
76
|
+
import type { Messages } from '../types';
|
|
77
|
+
|
|
78
|
+
const messages: Messages = {
|
|
79
|
+
common: {
|
|
80
|
+
title: '환영합니다',
|
|
81
|
+
greeting: '안녕하세요, {name}님!',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default messages;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Step 5: Update app directory structure
|
|
89
|
+
|
|
90
|
+
Rename `app/` contents to `app/[lang]/`:
|
|
91
|
+
|
|
92
|
+
Before:
|
|
93
|
+
```
|
|
94
|
+
app/
|
|
95
|
+
├── layout.tsx
|
|
96
|
+
├── page.tsx
|
|
97
|
+
└── about/page.tsx
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
After:
|
|
101
|
+
```
|
|
102
|
+
app/
|
|
103
|
+
└── [lang]/
|
|
104
|
+
├── layout.tsx
|
|
105
|
+
├── page.tsx
|
|
106
|
+
└── about/page.tsx
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Step 6: Update layout.tsx
|
|
110
|
+
|
|
111
|
+
Replace `app/[lang]/layout.tsx`:
|
|
112
|
+
```typescript
|
|
113
|
+
import { TranslationsProvider } from '@meursyphus/i18n-llm';
|
|
114
|
+
|
|
115
|
+
interface LayoutProps {
|
|
116
|
+
children: React.ReactNode;
|
|
117
|
+
params: Promise<{ lang: string }>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default async function RootLayout({ children, params }: LayoutProps) {
|
|
121
|
+
const { lang } = await params;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<html lang={lang}>
|
|
125
|
+
<body>
|
|
126
|
+
<TranslationsProvider locale={lang}>
|
|
127
|
+
{children}
|
|
128
|
+
</TranslationsProvider>
|
|
129
|
+
</body>
|
|
130
|
+
</html>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Step 7: Use translations in components
|
|
136
|
+
|
|
137
|
+
Server Component:
|
|
138
|
+
```typescript
|
|
139
|
+
import { getTranslations } from '@meursyphus/i18n-llm';
|
|
140
|
+
|
|
141
|
+
export default async function Page({ params }: { params: Promise<{ lang: string }> }) {
|
|
142
|
+
const { lang } = await params;
|
|
143
|
+
const t = await getTranslations('common', lang);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div>
|
|
147
|
+
<h1>{t('title')}</h1>
|
|
148
|
+
<p>{t('greeting', { name: 'World' })}</p>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Client Component:
|
|
155
|
+
```typescript
|
|
156
|
+
'use client';
|
|
157
|
+
|
|
158
|
+
import { useTranslations } from '@meursyphus/i18n-llm/client';
|
|
159
|
+
|
|
160
|
+
export function Greeting({ name }: { name: string }) {
|
|
161
|
+
const t = useTranslations('common');
|
|
162
|
+
|
|
163
|
+
return <p>{t('greeting', { name })}</p>;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Step 8: Language Switcher (Optional)
|
|
168
|
+
|
|
169
|
+
Create a language switcher component:
|
|
170
|
+
```typescript
|
|
171
|
+
'use client';
|
|
172
|
+
|
|
173
|
+
import { useCurrentLanguage } from '@meursyphus/i18n-llm/client';
|
|
174
|
+
import { setLanguagePreference } from '@meursyphus/i18n-llm/actions';
|
|
175
|
+
import { usePathname } from 'next/navigation';
|
|
176
|
+
|
|
177
|
+
export function LanguageSwitcher() {
|
|
178
|
+
const currentLang = useCurrentLanguage();
|
|
179
|
+
const pathname = usePathname();
|
|
180
|
+
|
|
181
|
+
const handleChange = async (locale: string) => {
|
|
182
|
+
await setLanguagePreference(locale, pathname);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<select value={currentLang} onChange={(e) => handleChange(e.target.value)}>
|
|
187
|
+
<option value="en">English</option>
|
|
188
|
+
<option value="ko">한국어</option>
|
|
189
|
+
</select>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Step 9: Generate Metadata (Optional)
|
|
195
|
+
|
|
196
|
+
Use translations in generateMetadata:
|
|
197
|
+
```typescript
|
|
198
|
+
import { getTranslations } from '@meursyphus/i18n-llm';
|
|
199
|
+
import type { Metadata } from 'next';
|
|
200
|
+
|
|
201
|
+
interface PageProps {
|
|
202
|
+
params: Promise<{ lang: string }>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
206
|
+
const { lang } = await params;
|
|
207
|
+
const t = await getTranslations('common', lang);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
title: t('title'),
|
|
211
|
+
description: t('description'),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Setup Complete!
|
|
217
|
+
|
|
218
|
+
i18n-llm has been configured successfully.
|
|
219
|
+
The app now supports English (en) and Korean (ko).
|
|
220
|
+
|
|
221
|
+
To add more locales:
|
|
222
|
+
1. Add locale to `locales` array in `i18n.config.ts`
|
|
223
|
+
2. Add locale to `locales` array in `middleware.ts` (in defineMiddleware)
|
|
224
|
+
3. Create new message file at `messages/{locale}/index.ts`
|
|
225
|
+
|
|
226
|
+
## Variable Interpolation
|
|
227
|
+
|
|
228
|
+
Use `{variableName}` in translations:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// In messages file
|
|
232
|
+
"greeting": "Hello, {name}! You have {count} new messages."
|
|
233
|
+
|
|
234
|
+
// In component
|
|
235
|
+
t('greeting', { name: 'John', count: 5 })
|
|
236
|
+
// Output: "Hello, John! You have 5 new messages."
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Nested Keys
|
|
240
|
+
|
|
241
|
+
Access nested translations with dot notation:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// In messages file
|
|
245
|
+
{
|
|
246
|
+
nav: {
|
|
247
|
+
home: 'Home',
|
|
248
|
+
about: 'About',
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// In component
|
|
253
|
+
t('nav.home') // "Home"
|
|
254
|
+
```
|