@ingram-tech/nk-i18n 0.1.0

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/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @ingram-tech/nk-i18n
2
+
3
+ Type-safe, English-as-key i18n for Ingram Next.js sites. The English source text
4
+ **is** the key (no `en.json`), translations are ICU MessageFormat, and catalogs
5
+ are plain colocated JSON. Routing is left to the site — the translator only needs
6
+ a locale string, so it works with URL-prefixed (`/fr/…`, `[locale]`, middleware
7
+ rewrite) or cookieless (cookie + `Accept-Language`) setups alike.
8
+
9
+ ```bash
10
+ bun add @ingram-tech/nk-i18n
11
+ ```
12
+
13
+ ## Locale config (one table, derived constants)
14
+
15
+ ```ts
16
+ // lib/i18n/locales.ts
17
+ import { defineI18nConfig, deriveLocaleConstants, localeMap } from "@ingram-tech/nk-i18n";
18
+
19
+ export const i18nConfig = defineI18nConfig({
20
+ baseLocale: "en",
21
+ locales: {
22
+ en: { label: "English", htmlLang: "en", ogLocale: "en_US" },
23
+ fr: { label: "Français", htmlLang: "fr-BE", ogLocale: "fr_BE" },
24
+ nl: { label: "Nederlands", htmlLang: "nl-BE", ogLocale: "nl_BE" },
25
+ },
26
+ });
27
+
28
+ export type Locale = keyof typeof i18nConfig.locales & string;
29
+ export const { SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_NAMES } =
30
+ deriveLocaleConstants(i18nConfig);
31
+ export const HTML_LANG = localeMap(i18nConfig, (def) => def.htmlLang);
32
+ export const OG_LOCALE = localeMap(i18nConfig, (def) => def.ogLocale);
33
+ ```
34
+
35
+ ## Translating
36
+
37
+ Server components / metadata:
38
+
39
+ ```ts
40
+ import { createT } from "@ingram-tech/nk-i18n";
41
+ import { siteScope } from "@/lib/i18n/scopes/site";
42
+
43
+ const t = createT(locale, siteScope);
44
+ t("Back to directory");
45
+ t('Results for "{query}"', { query });
46
+ t("Showing {from}-{to} of {total, number} codes", { from, to, total });
47
+ ```
48
+
49
+ Client components:
50
+
51
+ ```tsx
52
+ "use client";
53
+ import { useT } from "@ingram-tech/nk-i18n/client";
54
+ import fr from "./i18n.fr.json";
55
+ import nl from "./i18n.nl.json";
56
+
57
+ const t = useT({ fr, nl });
58
+ ```
59
+
60
+ When the source is a concrete scope or `{ fr, nl }`, `t("…")` is **type-checked**
61
+ against the intersection of the catalogs — a missing translation is a compile
62
+ error, not a silent English fallback. For data-driven keys (a runtime `string`),
63
+ widen the source: `useT<Messages>({ fr, nl })`.
64
+
65
+ ### Scopes
66
+
67
+ ```ts
68
+ // lib/i18n/scopes/site.ts
69
+ import { defineI18nScope } from "@ingram-tech/nk-i18n";
70
+ import fr from "../messages/site.fr.json";
71
+ import nl from "../messages/site.nl.json";
72
+
73
+ export const siteScope = defineI18nScope({ name: "site", messages: { fr, nl } });
74
+ ```
75
+
76
+ ## Locale provider & resolution
77
+
78
+ Wrap the app once with the server-resolved locale:
79
+
80
+ ```tsx
81
+ // app/layout.tsx
82
+ import { LocaleProvider } from "@ingram-tech/nk-i18n/client";
83
+
84
+ <LocaleProvider value={locale}>{children}</LocaleProvider>;
85
+ ```
86
+
87
+ Read it in client components (pass the site's `Locale` to narrow it):
88
+
89
+ ```ts
90
+ import { useLocale } from "@ingram-tech/nk-i18n/client";
91
+ const locale = useLocale<Locale>();
92
+ ```
93
+
94
+ Resolve the locale on the server however the site routes. `negotiateAcceptLanguage`
95
+ handles the `Accept-Language` step:
96
+
97
+ ```ts
98
+ import { cookies, headers } from "next/headers";
99
+ import { cache } from "react";
100
+ import { negotiateAcceptLanguage } from "@ingram-tech/nk-i18n";
101
+ import { DEFAULT_LOCALE, SUPPORTED_LOCALES, type Locale } from "./locales";
102
+
103
+ export const resolveLocale = cache(async (): Promise<Locale> => {
104
+ const cookie = (await cookies()).get("locale")?.value;
105
+ if (cookie && (SUPPORTED_LOCALES as string[]).includes(cookie)) return cookie as Locale;
106
+ const negotiated = negotiateAcceptLanguage(
107
+ (await headers()).get("accept-language"),
108
+ SUPPORTED_LOCALES,
109
+ );
110
+ return (negotiated as Locale) ?? DEFAULT_LOCALE;
111
+ });
112
+ ```
113
+
114
+ ## Exports
115
+
116
+ - `@ingram-tech/nk-i18n` (server-safe, no React): `createT`, `defineI18nScope`,
117
+ `defineMessages`, `defineI18nConfig`, `deriveLocaleConstants`, `localeMap`,
118
+ `negotiateAcceptLanguage`, and the `Messages` / `I18nScope` / `Translator` /
119
+ `TranslationKey` / `I18nConfig` / `LocaleDefinition` types.
120
+ - `@ingram-tech/nk-i18n/client` (`"use client"`): `LocaleProvider`, `useLocale`,
121
+ `useT`.
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from "react";
2
+ import { type I18nScope, type Messages, type Translator } from "./core";
3
+ /**
4
+ * Provide the active locale to client components. Wrap the app once (in the root
5
+ * layout) with the server-resolved locale.
6
+ */
7
+ export declare function LocaleProvider({ value, children, }: {
8
+ value: string;
9
+ children: ReactNode;
10
+ }): import("react").JSX.Element;
11
+ /**
12
+ * Read the active locale. Pass the site's `Locale` union as the type argument
13
+ * to get it back narrowed: `const locale = useLocale<Locale>()`.
14
+ */
15
+ export declare function useLocale<TLocale extends string = string>(): TLocale;
16
+ /**
17
+ * Client translator bound to the active locale from {@link LocaleProvider}.
18
+ * Message sources are usually passed as fresh object literals each render
19
+ * (`useT({ fr, nl })`), so they are held in a ref and the translator identity
20
+ * only changes when the locale changes — safe to list in hook deps.
21
+ */
22
+ export declare function useT<const TSource extends Messages | I18nScope | undefined>(messagesOrScope?: TSource, runtimeMessages?: Messages | I18nScope): Translator<TSource>;
23
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,SAAS,EAA+B,MAAM,OAAO,CAAC;AACnF,OAAO,EAAW,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAIjF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC9B,KAAK,EACL,QAAQ,GACR,EAAE;IACF,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;CACpB,+BAEA;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,KAAK,OAAO,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,OAAO,SAAS,QAAQ,GAAG,SAAS,GAAG,SAAS,EAC1E,eAAe,CAAC,EAAE,OAAO,EACzB,eAAe,CAAC,EAAE,QAAQ,GAAG,SAAS,GACpC,UAAU,CAAC,OAAO,CAAC,CAarB"}
package/dist/client.js ADDED
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useMemo, useRef } from "react";
4
+ import { createT } from "./core";
5
+ const LocaleContext = createContext("en");
6
+ /**
7
+ * Provide the active locale to client components. Wrap the app once (in the root
8
+ * layout) with the server-resolved locale.
9
+ */
10
+ export function LocaleProvider({ value, children, }) {
11
+ return _jsx(LocaleContext.Provider, { value: value, children: children });
12
+ }
13
+ /**
14
+ * Read the active locale. Pass the site's `Locale` union as the type argument
15
+ * to get it back narrowed: `const locale = useLocale<Locale>()`.
16
+ */
17
+ export function useLocale() {
18
+ return useContext(LocaleContext);
19
+ }
20
+ /**
21
+ * Client translator bound to the active locale from {@link LocaleProvider}.
22
+ * Message sources are usually passed as fresh object literals each render
23
+ * (`useT({ fr, nl })`), so they are held in a ref and the translator identity
24
+ * only changes when the locale changes — safe to list in hook deps.
25
+ */
26
+ export function useT(messagesOrScope, runtimeMessages) {
27
+ const locale = useContext(LocaleContext);
28
+ const sources = useRef({ messagesOrScope, runtimeMessages });
29
+ sources.current = { messagesOrScope, runtimeMessages };
30
+ return useMemo(() => createT(locale, sources.current.messagesOrScope, sources.current.runtimeMessages), [locale]);
31
+ }
32
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACnF,OAAO,EAAE,OAAO,EAAkD,MAAM,QAAQ,CAAC;AAEjF,MAAM,aAAa,GAAG,aAAa,CAAS,IAAI,CAAC,CAAC;AAElD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,EAC9B,KAAK,EACL,QAAQ,GAIR;IACA,OAAO,KAAC,aAAa,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA0B,CAAC;AAClF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS;IACxB,OAAO,UAAU,CAAC,aAAa,CAAY,CAAC;AAC7C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,IAAI,CACnB,eAAyB,EACzB,eAAsC;IAEtC,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,MAAM,CAAC,EAAE,eAAe,EAAE,eAAe,EAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,OAAO,GAAG,EAAE,eAAe,EAAE,eAAe,EAAE,CAAC;IACvD,OAAO,OAAO,CACb,GAAG,EAAE,CACJ,OAAO,CACN,MAAM,EACN,OAAO,CAAC,OAAO,CAAC,eAAe,EAC/B,OAAO,CAAC,OAAO,CAAC,eAAe,CAC/B,EACF,CAAC,MAAM,CAAC,CACR,CAAC;AACH,CAAC"}
@@ -0,0 +1,42 @@
1
+ export type MissingKeysPolicy = "error" | "warn" | "ignore";
2
+ /**
3
+ * Metadata for one locale. `label` (the display name, usually native) is
4
+ * required; sites may add their own fields (e.g. `htmlLang`, `ogLocale`) and
5
+ * read them back type-safely with {@link localeMap}.
6
+ */
7
+ export type LocaleDefinition = {
8
+ label: string;
9
+ missingKeys?: MissingKeysPolicy;
10
+ };
11
+ export type I18nConfig<TLocales extends Record<string, LocaleDefinition> = Record<string, LocaleDefinition>, TBaseLocale extends string = string> = {
12
+ baseLocale: TBaseLocale;
13
+ locales: TLocales;
14
+ };
15
+ type AnyI18nConfig = {
16
+ baseLocale: string;
17
+ locales: Record<string, LocaleDefinition>;
18
+ };
19
+ /**
20
+ * Identity helper that captures the locale table as literal types. Declare the
21
+ * `Locale` union from the result: `type Locale = keyof typeof config.locales`.
22
+ * Extra per-locale fields beyond `label`/`missingKeys` are preserved.
23
+ */
24
+ export declare function defineI18nConfig<const TConfig extends AnyI18nConfig>(config: TConfig): TConfig;
25
+ /**
26
+ * Derive the common runtime constants from a config: the ordered locale list,
27
+ * the base locale, and the code→label map.
28
+ */
29
+ export declare function deriveLocaleConstants<const TConfig extends AnyI18nConfig>(config: TConfig): {
30
+ SUPPORTED_LOCALES: Array<keyof TConfig["locales"] & string>;
31
+ DEFAULT_LOCALE: TConfig["baseLocale"];
32
+ LOCALE_NAMES: Record<keyof TConfig["locales"] & string, string>;
33
+ };
34
+ /**
35
+ * Build a `Record<Locale, T>` from the config — e.g. an `HTML_LANG` or
36
+ * `OG_LOCALE` map from fields stored on each locale definition:
37
+ *
38
+ * export const HTML_LANG = localeMap(i18nConfig, (def) => def.htmlLang);
39
+ */
40
+ export declare function localeMap<const TConfig extends AnyI18nConfig, TValue>(config: TConfig, pick: (def: TConfig["locales"][keyof TConfig["locales"] & string], locale: keyof TConfig["locales"] & string) => TValue): Record<keyof TConfig["locales"] & string, TValue>;
41
+ export {};
42
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,UAAU,CACrB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,MAAM,CACzD,MAAM,EACN,gBAAgB,CAChB,EACD,WAAW,SAAS,MAAM,GAAG,MAAM,IAChC;IACH,UAAU,EAAE,WAAW,CAAC;IACxB,OAAO,EAAE,QAAQ,CAAC;CAClB,CAAC;AAEF,KAAK,aAAa,GAAG;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC1C,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,OAAO,SAAS,aAAa,EACnE,MAAM,EAAE,OAAO,GACb,OAAO,CAET;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,OAAO,SAAS,aAAa,EACxE,MAAM,EAAE,OAAO,GACb;IACF,iBAAiB,EAAE,KAAK,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC;IAC5D,cAAc,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACtC,YAAY,EAAE,MAAM,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,EAAE,MAAM,CAAC,CAAC;CAChE,CAUA;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,CAAC,OAAO,SAAS,aAAa,EAAE,MAAM,EACpE,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,CACL,GAAG,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,EAC1D,MAAM,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,KACrC,MAAM,GACT,MAAM,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,EAAE,MAAM,CAAC,CAQnD"}
package/dist/config.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Identity helper that captures the locale table as literal types. Declare the
3
+ * `Locale` union from the result: `type Locale = keyof typeof config.locales`.
4
+ * Extra per-locale fields beyond `label`/`missingKeys` are preserved.
5
+ */
6
+ export function defineI18nConfig(config) {
7
+ return config;
8
+ }
9
+ /**
10
+ * Derive the common runtime constants from a config: the ordered locale list,
11
+ * the base locale, and the code→label map.
12
+ */
13
+ export function deriveLocaleConstants(config) {
14
+ const entries = Object.entries(config.locales);
15
+ return {
16
+ SUPPORTED_LOCALES: entries.map(([locale]) => locale),
17
+ DEFAULT_LOCALE: config.baseLocale,
18
+ LOCALE_NAMES: Object.fromEntries(entries.map(([locale, def]) => [locale, def.label])),
19
+ };
20
+ }
21
+ /**
22
+ * Build a `Record<Locale, T>` from the config — e.g. an `HTML_LANG` or
23
+ * `OG_LOCALE` map from fields stored on each locale definition:
24
+ *
25
+ * export const HTML_LANG = localeMap(i18nConfig, (def) => def.htmlLang);
26
+ */
27
+ export function localeMap(config, pick) {
28
+ const entries = Object.entries(config.locales);
29
+ return Object.fromEntries(entries.map(([locale, def]) => [locale, pick(def, locale)]));
30
+ }
31
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA4BA;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC/B,MAAe;IAEf,OAAO,MAAM,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACpC,MAAe;IAOf,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAsC,CAAC;IACpF,OAAO;QACN,iBAAiB,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC;QACpD,cAAc,EAAE,MAAM,CAAC,UAAU;QACjC,YAAY,EAAE,MAAM,CAAC,WAAW,CAC/B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CACzB;KAC3B,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACxB,MAAe,EACf,IAGW;IAGX,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAE5C,CAAC;IACF,OAAO,MAAM,CAAC,WAAW,CACxB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CACjC,CAAC;AAC7B,CAAC"}
package/dist/core.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ type MessageCatalog = Record<string, string>;
2
+ declare const I18N_KEY_BRAND: unique symbol;
3
+ /**
4
+ * A set of per-locale message catalogs, keyed by locale code. The English
5
+ * source text is used as the key, so there is no `en` catalog: a missing lookup
6
+ * falls back to the key itself.
7
+ */
8
+ export type Messages = Record<string, MessageCatalog>;
9
+ type I18nKey<TValue extends string = string> = TValue & {
10
+ readonly [I18N_KEY_BRAND]: "I18nKey";
11
+ };
12
+ /** A named bundle of catalogs, produced by {@link defineI18nScope}. */
13
+ export type I18nScope<TMessages extends Messages = Messages> = {
14
+ readonly name: string;
15
+ readonly messages: TMessages;
16
+ };
17
+ export type MessageSource<TMessages extends Messages = Messages> = TMessages | I18nScope<TMessages>;
18
+ type SharedMessageKey<TMessages extends Messages> = keyof TMessages[keyof TMessages] & string;
19
+ type KnownMessageKey<TSource extends MessageSource> = TSource extends I18nScope<infer TMessages> ? SharedMessageKey<TMessages> : TSource extends Messages ? SharedMessageKey<TSource> : never;
20
+ type Unbrand<TValue extends string> = TValue extends I18nKey<infer TRaw> ? TRaw : TValue;
21
+ type LiteralString<TValue extends string> = string extends TValue ? never : TValue;
22
+ type SafeLiteralOrKey<TValue extends string> = TValue extends I18nKey<string> ? TValue : LiteralString<TValue>;
23
+ export type TranslationKey<TSource extends MessageSource> = KnownMessageKey<TSource>;
24
+ type AllowedMessageKey<TSource extends MessageSource | undefined, TValue extends string> = TSource extends MessageSource ? Unbrand<TValue> extends KnownMessageKey<TSource> ? TValue : KnownMessageKey<TSource> : SafeLiteralOrKey<TValue>;
25
+ type DeepMessageKeys<TValue> = TValue extends string ? I18nKey<TValue> : TValue extends (...args: never[]) => unknown ? TValue : TValue extends readonly unknown[] ? {
26
+ readonly [K in keyof TValue]: DeepMessageKeys<TValue[K]>;
27
+ } : TValue extends Record<PropertyKey, unknown> ? {
28
+ [K in keyof TValue]: DeepMessageKeys<TValue[K]>;
29
+ } : TValue;
30
+ /**
31
+ * Translate an English source string to the active locale. With a known message
32
+ * source the key is checked against the catalogs at compile time; without one,
33
+ * any literal string is allowed. The optional `values` are interpolated with
34
+ * ICU MessageFormat (`{name}`, `{count, number}`, plurals, …).
35
+ */
36
+ export type Translator<TSource extends MessageSource | undefined = undefined> = <const TValue extends string>(english: AllowedMessageKey<TSource, TValue>, values?: Record<string, unknown>) => string;
37
+ /**
38
+ * Brand a tree of English source strings as translation keys so the compiler
39
+ * can check them. Returns the value unchanged at runtime.
40
+ */
41
+ export declare function defineMessages<const TValue>(value: TValue): DeepMessageKeys<TValue>;
42
+ /** Group catalogs under a name so they can be passed to {@link createT}/`useT`. */
43
+ export declare function defineI18nScope<const TMessages extends Messages>(scope: {
44
+ name: string;
45
+ messages: TMessages;
46
+ }): I18nScope<TMessages>;
47
+ /**
48
+ * Build a translator bound to `locale`. The English source is the key, so the
49
+ * base/source locale needs no catalog — a missing lookup returns the key. Pass
50
+ * a scope or raw `{ fr, nl, … }` catalogs as the source; `runtimeMessages`
51
+ * overrides them (e.g. request-scoped data).
52
+ */
53
+ export declare function createT<const TSource extends MessageSource | undefined>(locale: string, messagesOrScope?: TSource, runtimeMessages?: MessageSource): Translator<TSource>;
54
+ export {};
55
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
package/dist/core.js ADDED
@@ -0,0 +1,51 @@
1
+ import IntlMessageFormat from "intl-messageformat";
2
+ const cache = new Map();
3
+ function format(message, locale, values) {
4
+ const key = `${locale}:${message}`;
5
+ let fmt = cache.get(key);
6
+ if (!fmt) {
7
+ fmt = new IntlMessageFormat(message, locale);
8
+ cache.set(key, fmt);
9
+ }
10
+ return fmt.format(values);
11
+ }
12
+ function isI18nScope(source) {
13
+ return (typeof source === "object" &&
14
+ source !== null &&
15
+ "name" in source &&
16
+ typeof source.name === "string" &&
17
+ "messages" in source);
18
+ }
19
+ function resolveMessages(source) {
20
+ if (!source)
21
+ return undefined;
22
+ return isI18nScope(source) ? source.messages : source;
23
+ }
24
+ /**
25
+ * Brand a tree of English source strings as translation keys so the compiler
26
+ * can check them. Returns the value unchanged at runtime.
27
+ */
28
+ export function defineMessages(value) {
29
+ return value;
30
+ }
31
+ /** Group catalogs under a name so they can be passed to {@link createT}/`useT`. */
32
+ export function defineI18nScope(scope) {
33
+ return scope;
34
+ }
35
+ /**
36
+ * Build a translator bound to `locale`. The English source is the key, so the
37
+ * base/source locale needs no catalog — a missing lookup returns the key. Pass
38
+ * a scope or raw `{ fr, nl, … }` catalogs as the source; `runtimeMessages`
39
+ * overrides them (e.g. request-scoped data).
40
+ */
41
+ export function createT(locale, messagesOrScope, runtimeMessages) {
42
+ const resolvedMessages = resolveMessages(runtimeMessages) ?? resolveMessages(messagesOrScope);
43
+ const translate = (english, values) => {
44
+ const translated = resolvedMessages?.[locale]?.[english] ?? english;
45
+ if (!values)
46
+ return translated;
47
+ return format(translated, locale, values);
48
+ };
49
+ return translate;
50
+ }
51
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,MAAM,oBAAoB,CAAC;AA0EnD,MAAM,KAAK,GAAG,IAAI,GAAG,EAA6B,CAAC;AAEnD,SAAS,MAAM,CACd,OAAe,EACf,MAAc,EACd,MAA+B;IAE/B,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC;IACnC,IAAI,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,IAAI,CAAC,GAAG,EAAE,CAAC;QACV,GAAG,GAAG,IAAI,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7C,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,CAAW,CAAC;AACrC,CAAC;AAED,SAAS,WAAW,CAAC,MAAqB;IACzC,OAAO,CACN,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,KAAK,IAAI;QACf,MAAM,IAAI,MAAM;QAChB,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;QAC/B,UAAU,IAAI,MAAM,CACpB,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,MAAsB;IAC9C,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAe,KAAa;IACzD,OAAO,KAAgC,CAAC;AACzC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,eAAe,CAAmC,KAGjE;IACA,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CACtB,MAAc,EACd,eAAyB,EACzB,eAA+B;IAE/B,MAAM,gBAAgB,GACrB,eAAe,CAAC,eAAe,CAAC,IAAI,eAAe,CAAC,eAAe,CAAC,CAAC;IAEtE,MAAM,SAAS,GAAG,CAAC,OAAe,EAAE,MAAgC,EAAU,EAAE;QAC/E,MAAM,UAAU,GAAG,gBAAgB,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC;QACpE,IAAI,CAAC,MAAM;YAAE,OAAO,UAAU,CAAC;QAC/B,OAAO,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC,CAAC;IAEF,OAAO,SAAgC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { createT, defineI18nScope, defineMessages, type I18nScope, type Messages, type MessageSource, type TranslationKey, type Translator, } from "./core";
2
+ export { defineI18nConfig, deriveLocaleConstants, type I18nConfig, localeMap, type LocaleDefinition, type MissingKeysPolicy, } from "./config";
3
+ export { negotiateAcceptLanguage } from "./negotiate";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,OAAO,EACP,eAAe,EACf,cAAc,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,UAAU,GACf,MAAM,QAAQ,CAAC;AAChB,OAAO,EACN,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,UAAU,EACf,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Server-safe entry — no React. Client hooks live at "@ingram-tech/nk-i18n/client".
2
+ export { createT, defineI18nScope, defineMessages, } from "./core";
3
+ export { defineI18nConfig, deriveLocaleConstants, localeMap, } from "./config";
4
+ export { negotiateAcceptLanguage } from "./negotiate";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,OAAO,EACN,OAAO,EACP,eAAe,EACf,cAAc,GAMd,MAAM,QAAQ,CAAC;AAChB,OAAO,EACN,gBAAgB,EAChB,qBAAqB,EAErB,SAAS,GAGT,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Pick the best supported locale from an `Accept-Language` header value.
3
+ * Compares on the primary subtag only (`fr-BE` → `fr`), case-insensitively, and
4
+ * returns the first supported match in header order, or `undefined` if none
5
+ * match. Pure — compose it inside a framework-specific locale resolver.
6
+ */
7
+ export declare function negotiateAcceptLanguage(header: string | null | undefined, supported: readonly string[]): string | undefined;
8
+ //# sourceMappingURL=negotiate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negotiate.d.ts","sourceRoot":"","sources":["../src/negotiate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACtC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,SAAS,EAAE,SAAS,MAAM,EAAE,GAC1B,MAAM,GAAG,SAAS,CAOpB"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Pick the best supported locale from an `Accept-Language` header value.
3
+ * Compares on the primary subtag only (`fr-BE` → `fr`), case-insensitively, and
4
+ * returns the first supported match in header order, or `undefined` if none
5
+ * match. Pure — compose it inside a framework-specific locale resolver.
6
+ */
7
+ export function negotiateAcceptLanguage(header, supported) {
8
+ if (!header)
9
+ return undefined;
10
+ for (const part of header.split(",")) {
11
+ const lang = part.split(";")[0]?.trim().split("-")[0]?.toLowerCase();
12
+ if (lang && supported.includes(lang))
13
+ return lang;
14
+ }
15
+ return undefined;
16
+ }
17
+ //# sourceMappingURL=negotiate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negotiate.js","sourceRoot":"","sources":["../src/negotiate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACtC,MAAiC,EACjC,SAA4B;IAE5B,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QACrE,IAAI,IAAI,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACnD,CAAC;IACD,OAAO,SAAS,CAAC;AAClB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@ingram-tech/nk-i18n",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe, English-as-key i18n for Ingram Next.js sites — ICU translator, scopes, locale config, and Accept-Language negotiation.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ingram-technologies/nextkit.git",
10
+ "directory": "packages/nk-i18n"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./client": {
24
+ "types": "./dist/client.d.ts",
25
+ "import": "./dist/client.js"
26
+ }
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "type-check": "tsc -p tsconfig.json --noEmit",
31
+ "test": "vitest run"
32
+ },
33
+ "dependencies": {
34
+ "intl-messageformat": "^11.2.8"
35
+ },
36
+ "devDependencies": {
37
+ "@ingram-tech/typescript-config": "workspace:*",
38
+ "@types/react": "^19.0.0",
39
+ "react": "^19.0.0",
40
+ "typescript": "^6.0.3",
41
+ "vitest": "^4.1.6"
42
+ },
43
+ "peerDependencies": {
44
+ "react": "^18.0.0 || ^19.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "react": {
48
+ "optional": true
49
+ }
50
+ }
51
+ }