@ayinza_dev/i18n-config 1.3.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.
@@ -0,0 +1,163 @@
1
+ // Default locale mappings
2
+ const defaultLocaleMapping = {
3
+ en: "en-US",
4
+ ar: "ar-SA",
5
+ es: "es-ES",
6
+ fr: "fr-FR",
7
+ de: "de-DE",
8
+ zh: "zh-CN",
9
+ ja: "ja-JP",
10
+ pt: "pt-BR",
11
+ it: "it-IT",
12
+ ru: "ru-RU",
13
+ };
14
+ export class I18nFormatters {
15
+ normalizeLanguage(code) {
16
+ const lower = code.toLowerCase();
17
+ const [base] = lower.split(/[\-_]/);
18
+ return { full: lower, base };
19
+ }
20
+ //Initialize formatter configs
21
+ constructor(config = {}) {
22
+ this.fallbackLocale =
23
+ config.fallbackLocale || defaultLocaleMapping.en || "en-US";
24
+ this.currencyConfig = {
25
+ defaultCurrency: config.currency?.defaultCurrency || "USD",
26
+ localeMapping: {
27
+ ...defaultLocaleMapping,
28
+ ...config.currency?.localeMapping,
29
+ },
30
+ };
31
+ this.numberConfig = {
32
+ localeMapping: {
33
+ ...defaultLocaleMapping,
34
+ ...config.number?.localeMapping,
35
+ },
36
+ };
37
+ this.dateConfig = {
38
+ localeMapping: {
39
+ ...defaultLocaleMapping,
40
+ ...config.date?.localeMapping,
41
+ },
42
+ defaultFormat: config.date?.defaultFormat || {
43
+ year: "numeric",
44
+ month: "long",
45
+ day: "numeric",
46
+ },
47
+ };
48
+ }
49
+ getLocale(language, mapping) {
50
+ const { full, base } = this.normalizeLanguage(language);
51
+ return mapping[full] || mapping[base] || this.fallbackLocale;
52
+ }
53
+ /**
54
+ * Format with locale-specific formatting
55
+ */
56
+ formatCurrency(amount, language, currency, options) {
57
+ const locale = this.getLocale(language, this.currencyConfig.localeMapping);
58
+ const currencyCode = currency || this.currencyConfig.defaultCurrency;
59
+ try {
60
+ return new Intl.NumberFormat(locale, {
61
+ style: "currency",
62
+ currency: currencyCode,
63
+ ...options,
64
+ }).format(amount);
65
+ }
66
+ catch (error) {
67
+ console.error("[i18n-formatters] Currency formatting error:", error);
68
+ return `${currencyCode} ${amount}`;
69
+ }
70
+ }
71
+ formatNumber(value, language, options) {
72
+ const locale = this.getLocale(language, this.numberConfig.localeMapping);
73
+ try {
74
+ return new Intl.NumberFormat(locale, options).format(value);
75
+ }
76
+ catch (error) {
77
+ console.error("[i18n-formatters] Number formatting error:", error);
78
+ return String(value);
79
+ }
80
+ }
81
+ formatPercent(value, language, options) {
82
+ const locale = this.getLocale(language, this.numberConfig.localeMapping);
83
+ try {
84
+ return new Intl.NumberFormat(locale, {
85
+ style: "percent",
86
+ ...options,
87
+ }).format(value);
88
+ }
89
+ catch (error) {
90
+ console.error("[i18n-formatters] Percent formatting error:", error);
91
+ return `${value * 100}%`;
92
+ }
93
+ }
94
+ formatDate(date, language, options) {
95
+ const locale = this.getLocale(language, this.dateConfig.localeMapping);
96
+ const dateObj = date instanceof Date ? date : new Date(date);
97
+ if (Number.isNaN(dateObj.getTime())) {
98
+ return new Date().toLocaleDateString(this.fallbackLocale);
99
+ }
100
+ try {
101
+ return new Intl.DateTimeFormat(locale, {
102
+ ...this.dateConfig.defaultFormat,
103
+ ...options,
104
+ }).format(dateObj);
105
+ }
106
+ catch (error) {
107
+ console.error("[i18n-formatters] Date formatting error:", error);
108
+ return dateObj.toLocaleDateString(this.fallbackLocale);
109
+ }
110
+ }
111
+ formatTime(date, language, options) {
112
+ const locale = this.getLocale(language, this.dateConfig.localeMapping);
113
+ const dateObj = date instanceof Date ? date : new Date(date);
114
+ if (Number.isNaN(dateObj.getTime())) {
115
+ return new Date().toLocaleTimeString(this.fallbackLocale);
116
+ }
117
+ try {
118
+ return new Intl.DateTimeFormat(locale, {
119
+ hour: "numeric",
120
+ minute: "numeric",
121
+ ...options,
122
+ }).format(dateObj);
123
+ }
124
+ catch (error) {
125
+ console.error("[i18n-formatters] Time formatting error:", error);
126
+ return dateObj.toLocaleTimeString(this.fallbackLocale);
127
+ }
128
+ }
129
+ formatDateTime(date, language, options) {
130
+ const locale = this.getLocale(language, this.dateConfig.localeMapping);
131
+ const dateObj = date instanceof Date ? date : new Date(date);
132
+ if (Number.isNaN(dateObj.getTime())) {
133
+ return new Date().toLocaleString(this.fallbackLocale);
134
+ }
135
+ try {
136
+ return new Intl.DateTimeFormat(locale, {
137
+ year: "numeric",
138
+ month: "long",
139
+ day: "numeric",
140
+ hour: "numeric",
141
+ minute: "numeric",
142
+ ...options,
143
+ }).format(dateObj);
144
+ }
145
+ catch (error) {
146
+ console.error("[i18n-formatters] DateTime formatting error:", error);
147
+ return dateObj.toLocaleString(this.fallbackLocale);
148
+ }
149
+ }
150
+ formatRelativeTime(value, unit, language, options) {
151
+ const locale = this.getLocale(language, this.dateConfig.localeMapping);
152
+ try {
153
+ return new Intl.RelativeTimeFormat(locale, {
154
+ numeric: "auto",
155
+ ...options,
156
+ }).format(value, unit);
157
+ }
158
+ catch (error) {
159
+ console.error("[i18n-formatters] Relative time formatting error:", error);
160
+ return `${value} ${unit}`;
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,111 @@
1
+ export interface UseFormattingReturn {
2
+ formatCurrency: (amount: number, currency?: string, options?: Intl.NumberFormatOptions) => string;
3
+ formatNumber: (value: number, options?: Intl.NumberFormatOptions) => string;
4
+ formatPercent: (value: number, options?: Intl.NumberFormatOptions) => string;
5
+ formatDate: (date: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
6
+ formatTime: (date: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
7
+ formatDateTime: (date: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
8
+ formatRelativeTime: (value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions) => string;
9
+ }
10
+ /**
11
+ * Custom hook that provides formatting functions based on current language
12
+ */
13
+ export declare const useFormatting: () => UseFormattingReturn;
14
+ /**
15
+ * Combined hook that provides both translation and formatting
16
+ */
17
+ export declare const useI18n: () => {
18
+ formatCurrency: (amount: number, currency?: string, options?: Intl.NumberFormatOptions) => string;
19
+ formatNumber: (value: number, options?: Intl.NumberFormatOptions) => string;
20
+ formatPercent: (value: number, options?: Intl.NumberFormatOptions) => string;
21
+ formatDate: (date: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
22
+ formatTime: (date: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
23
+ formatDateTime: (date: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
24
+ formatRelativeTime: (value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions) => string;
25
+ 0: import("i18next").TFunction<"translation", undefined>;
26
+ 1: import("i18next").i18n;
27
+ 2: boolean;
28
+ length: 3;
29
+ toString(): string;
30
+ toLocaleString(): string;
31
+ toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string;
32
+ pop(): boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined> | undefined;
33
+ push(...items: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]): number;
34
+ concat(...items: ConcatArray<boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>>[]): (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[];
35
+ concat(...items: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined> | ConcatArray<boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>>)[]): (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[];
36
+ join(separator?: string): string;
37
+ reverse(): (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[];
38
+ shift(): boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined> | undefined;
39
+ slice(start?: number, end?: number): (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[];
40
+ sort(compareFn?: ((a: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, b: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>) => number) | undefined): import("react-i18next").UseTranslationResponse<"translation", undefined>;
41
+ splice(start: number, deleteCount?: number): (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[];
42
+ splice(start: number, deleteCount: number, ...items: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]): (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[];
43
+ unshift(...items: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]): number;
44
+ indexOf(searchElement: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, fromIndex?: number): number;
45
+ lastIndexOf(searchElement: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, fromIndex?: number): number;
46
+ every<S extends boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>>(predicate: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => value is S, thisArg?: any): this is S[];
47
+ every(predicate: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => unknown, thisArg?: any): boolean;
48
+ some(predicate: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => unknown, thisArg?: any): boolean;
49
+ forEach(callbackfn: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => void, thisArg?: any): void;
50
+ map<U>(callbackfn: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => U, thisArg?: any): U[];
51
+ filter<S extends boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>>(predicate: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => value is S, thisArg?: any): S[];
52
+ filter(predicate: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => unknown, thisArg?: any): (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[];
53
+ reduce(callbackfn: (previousValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentIndex: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>): boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>;
54
+ reduce(callbackfn: (previousValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentIndex: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, initialValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>): boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>;
55
+ reduce<U>(callbackfn: (previousValue: U, currentValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentIndex: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => U, initialValue: U): U;
56
+ reduceRight(callbackfn: (previousValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentIndex: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>): boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>;
57
+ reduceRight(callbackfn: (previousValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentIndex: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, initialValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>): boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>;
58
+ reduceRight<U>(callbackfn: (previousValue: U, currentValue: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, currentIndex: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => U, initialValue: U): U;
59
+ find<S extends boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>>(predicate: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, obj: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => value is S, thisArg?: any): S | undefined;
60
+ find(predicate: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, obj: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => unknown, thisArg?: any): boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined> | undefined;
61
+ findIndex(predicate: (value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, obj: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => unknown, thisArg?: any): number;
62
+ fill(value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, start?: number, end?: number): import("react-i18next").UseTranslationResponse<"translation", undefined>;
63
+ copyWithin(target: number, start: number, end?: number): import("react-i18next").UseTranslationResponse<"translation", undefined>;
64
+ entries(): ArrayIterator<[number, boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>]>;
65
+ keys(): ArrayIterator<number>;
66
+ values(): ArrayIterator<boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>>;
67
+ includes(searchElement: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, fromIndex?: number): boolean;
68
+ flatMap<U, This = undefined>(callback: (this: This, value: boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>, index: number, array: (boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>)[]) => U | readonly U[], thisArg?: This | undefined): U[];
69
+ flat<A, D extends number = 1>(this: A, depth?: D | undefined): FlatArray<A, D>[];
70
+ [Symbol.iterator](): ArrayIterator<boolean | import("i18next").i18n | import("i18next").TFunction<"translation", undefined>>;
71
+ [Symbol.unscopables]: {
72
+ [x: number]: boolean | undefined;
73
+ length?: boolean | undefined;
74
+ toString?: boolean | undefined;
75
+ toLocaleString?: boolean | undefined;
76
+ pop?: boolean | undefined;
77
+ push?: boolean | undefined;
78
+ concat?: boolean | undefined;
79
+ join?: boolean | undefined;
80
+ reverse?: boolean | undefined;
81
+ shift?: boolean | undefined;
82
+ slice?: boolean | undefined;
83
+ sort?: boolean | undefined;
84
+ splice?: boolean | undefined;
85
+ unshift?: boolean | undefined;
86
+ indexOf?: boolean | undefined;
87
+ lastIndexOf?: boolean | undefined;
88
+ every?: boolean | undefined;
89
+ some?: boolean | undefined;
90
+ forEach?: boolean | undefined;
91
+ map?: boolean | undefined;
92
+ filter?: boolean | undefined;
93
+ reduce?: boolean | undefined;
94
+ reduceRight?: boolean | undefined;
95
+ find?: boolean | undefined;
96
+ findIndex?: boolean | undefined;
97
+ fill?: boolean | undefined;
98
+ copyWithin?: boolean | undefined;
99
+ entries?: boolean | undefined;
100
+ keys?: boolean | undefined;
101
+ values?: boolean | undefined;
102
+ includes?: boolean | undefined;
103
+ flatMap?: boolean | undefined;
104
+ flat?: boolean | undefined;
105
+ [Symbol.iterator]?: boolean | undefined;
106
+ readonly [Symbol.unscopables]?: boolean | undefined;
107
+ };
108
+ t: import("i18next").TFunction<"translation", undefined>;
109
+ i18n: import("i18next").i18n;
110
+ ready: boolean;
111
+ };
package/dist/hooks.js ADDED
@@ -0,0 +1,31 @@
1
+ import { useTranslation } from "react-i18next";
2
+ import { getFormatters } from "./config.js";
3
+ import { useMemo } from "react";
4
+ /**
5
+ * Custom hook that provides formatting functions based on current language
6
+ */
7
+ export const useFormatting = () => {
8
+ const { i18n } = useTranslation();
9
+ const formatters = getFormatters();
10
+ const currentLanguage = i18n.language;
11
+ return useMemo(() => ({
12
+ formatCurrency: (amount, currency, options) => formatters.formatCurrency(amount, currentLanguage, currency, options),
13
+ formatNumber: (value, options) => formatters.formatNumber(value, currentLanguage, options),
14
+ formatPercent: (value, options) => formatters.formatPercent(value, currentLanguage, options),
15
+ formatDate: (date, options) => formatters.formatDate(date, currentLanguage, options),
16
+ formatTime: (date, options) => formatters.formatTime(date, currentLanguage, options),
17
+ formatDateTime: (date, options) => formatters.formatDateTime(date, currentLanguage, options),
18
+ formatRelativeTime: (value, unit, options) => formatters.formatRelativeTime(value, unit, currentLanguage, options),
19
+ }), [currentLanguage, formatters]);
20
+ };
21
+ /**
22
+ * Combined hook that provides both translation and formatting
23
+ */
24
+ export const useI18n = () => {
25
+ const translation = useTranslation();
26
+ const formatting = useFormatting();
27
+ return {
28
+ ...translation,
29
+ ...formatting,
30
+ };
31
+ };
@@ -0,0 +1,8 @@
1
+ export { initializeI18n, getI18nInstance, getFormatters, defaultConfig, createI18nConfig, } from "./config.js";
2
+ export type { I18nConfig, I18nInitOptions, FormattersConfig, LocaleMapping, ParserPushConfig, NewKeyPayload, TranslationSnapshot, } from "./types.js";
3
+ export { I18nFormatters } from "./formatters.js";
4
+ export { useFormatting, useI18n } from "./hooks.js";
5
+ export { createI18nextParserConfig } from "./parser-config.js";
6
+ export { createTranslationSnapshot, collectNewTranslationKeys, handleNewTranslationKeys, } from "./parser-hooks.js";
7
+ export { useTranslation, Trans, Translation } from "react-i18next";
8
+ export { type TFunction } from "i18next";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { initializeI18n, getI18nInstance, getFormatters, defaultConfig, createI18nConfig, } from "./config.js";
2
+ export { I18nFormatters } from "./formatters.js";
3
+ export { useFormatting, useI18n } from "./hooks.js";
4
+ export { createI18nextParserConfig } from "./parser-config.js";
5
+ export { createTranslationSnapshot, collectNewTranslationKeys, handleNewTranslationKeys, } from "./parser-hooks.js";
6
+ // Re-export commonly used utilities from react-i18next
7
+ export { useTranslation, Trans, Translation } from "react-i18next";
@@ -0,0 +1,11 @@
1
+ import type { UserConfig } from "i18next-parser";
2
+ export interface ParserPresetOptions extends Partial<UserConfig> {
3
+ /** Limit glob set used by the parser. Defaults to src/*.{ts,tsx,js,jsx}. */
4
+ input?: string | string[];
5
+ /** Override parser defaults (indentation, separators, lexers etc.). */
6
+ overrides?: Partial<UserConfig>;
7
+ }
8
+ /**
9
+ * Create a shared parser config so every portal extracts translations the same way.
10
+ */
11
+ export declare const createI18nextParserConfig: (options?: ParserPresetOptions) => UserConfig;
@@ -0,0 +1,100 @@
1
+ const defaultInputGlobs = ["src/**/*.{ts,tsx,js,jsx}"];
2
+ const baseLexerFunctions = ["t", "i18next.t"];
3
+ const baseNamespaceFunctions = ["useTranslation", "useI18n"];
4
+ const jsxComponentFunctions = ["Trans", "Translation"];
5
+ const defaultLexers = {
6
+ js: [
7
+ {
8
+ lexer: "JavascriptLexer",
9
+ functions: baseLexerFunctions,
10
+ namespaceFunctions: baseNamespaceFunctions,
11
+ },
12
+ ],
13
+ ts: [
14
+ {
15
+ lexer: "JavascriptLexer",
16
+ functions: baseLexerFunctions,
17
+ namespaceFunctions: baseNamespaceFunctions,
18
+ },
19
+ ],
20
+ jsx: [
21
+ {
22
+ lexer: "JsxLexer",
23
+ functions: baseLexerFunctions,
24
+ namespaceFunctions: baseNamespaceFunctions,
25
+ componentFunctions: jsxComponentFunctions,
26
+ attr: "i18nKey",
27
+ },
28
+ ],
29
+ tsx: [
30
+ {
31
+ lexer: "JsxLexer",
32
+ functions: baseLexerFunctions,
33
+ namespaceFunctions: baseNamespaceFunctions,
34
+ componentFunctions: jsxComponentFunctions,
35
+ attr: "i18nKey",
36
+ },
37
+ ],
38
+ default: [
39
+ {
40
+ lexer: "JavascriptLexer",
41
+ functions: baseLexerFunctions,
42
+ namespaceFunctions: baseNamespaceFunctions,
43
+ },
44
+ ],
45
+ };
46
+ const parserDefaults = {
47
+ contextSeparator: "_",
48
+ createOldCatalogs: true,
49
+ defaultNamespace: "translation",
50
+ defaultValue: "",
51
+ indentation: 2,
52
+ keepRemoved: false,
53
+ keySeparator: ".",
54
+ lexers: defaultLexers,
55
+ lineEnding: "auto",
56
+ locales: ["en"],
57
+ namespaceSeparator: ":",
58
+ output: "locales/$LOCALE/$NAMESPACE.json",
59
+ pluralSeparator: "_",
60
+ sort: true,
61
+ verbose: false,
62
+ input: defaultInputGlobs,
63
+ };
64
+ const normalizeInput = (input) => {
65
+ if (!input) {
66
+ return defaultInputGlobs;
67
+ }
68
+ return Array.isArray(input) ? input : [input];
69
+ };
70
+ /**
71
+ * Create a shared parser config so every portal extracts translations the same way.
72
+ */
73
+ export const createI18nextParserConfig = (options = {}) => {
74
+ const { overrides, input, lexers, ...rest } = options;
75
+ const merged = {
76
+ ...parserDefaults,
77
+ ...rest,
78
+ input: normalizeInput(input ?? parserDefaults.input),
79
+ lexers: {
80
+ ...parserDefaults.lexers,
81
+ ...lexers,
82
+ },
83
+ };
84
+ if (overrides) {
85
+ merged.input = normalizeInput(overrides.input ?? merged.input);
86
+ merged.lexers = {
87
+ ...merged.lexers,
88
+ ...overrides.lexers,
89
+ };
90
+ return {
91
+ ...merged,
92
+ ...overrides,
93
+ lexers: {
94
+ ...merged.lexers,
95
+ ...overrides.lexers,
96
+ },
97
+ };
98
+ }
99
+ return merged;
100
+ };
@@ -0,0 +1,23 @@
1
+ import type { NewKeyPayload, ParserPushConfig, TranslationSnapshot } from "./types.js";
2
+ export interface CreateTranslationSnapshotOptions {
3
+ locale: string;
4
+ namespaces: Record<string, Record<string, unknown>>;
5
+ }
6
+ export interface CollectNewTranslationKeysOptions {
7
+ previous?: TranslationSnapshot | null;
8
+ next: TranslationSnapshot;
9
+ namespaces?: string[];
10
+ }
11
+ export interface HandleNewTranslationKeysOptions {
12
+ newKeys: NewKeyPayload[];
13
+ pushConfig: ParserPushConfig;
14
+ requestInit?: RequestInit;
15
+ logger?: Pick<Console, "log" | "warn" | "error">;
16
+ }
17
+ export interface HandleNewTranslationKeysResult {
18
+ pushed: number;
19
+ dryRun: boolean;
20
+ }
21
+ export declare const createTranslationSnapshot: (options: CreateTranslationSnapshotOptions) => TranslationSnapshot;
22
+ export declare const collectNewTranslationKeys: (options: CollectNewTranslationKeysOptions) => NewKeyPayload[];
23
+ export declare const handleNewTranslationKeys: (options: HandleNewTranslationKeysOptions) => Promise<HandleNewTranslationKeysResult>;
@@ -0,0 +1,125 @@
1
+ const formatValue = (value) => {
2
+ if (value === undefined || value === null) {
3
+ return "";
4
+ }
5
+ if (typeof value === "string") {
6
+ return value;
7
+ }
8
+ if (typeof value === "number" || typeof value === "boolean") {
9
+ return String(value);
10
+ }
11
+ return JSON.stringify(value);
12
+ };
13
+ const flattenCatalog = (node, prefix = "", acc = {}) => {
14
+ for (const [key, value] of Object.entries(node)) {
15
+ const nextKey = prefix ? `${prefix}.${key}` : key;
16
+ if (value && typeof value === "object" && !Array.isArray(value)) {
17
+ flattenCatalog(value, nextKey, acc);
18
+ }
19
+ else {
20
+ acc[nextKey] = formatValue(value);
21
+ }
22
+ }
23
+ return acc;
24
+ };
25
+ export const createTranslationSnapshot = (options) => {
26
+ const { locale, namespaces } = options;
27
+ const entries = {};
28
+ for (const [namespace, tree] of Object.entries(namespaces)) {
29
+ entries[namespace] = flattenCatalog(tree);
30
+ }
31
+ return { locale, namespaces: entries };
32
+ };
33
+ export const collectNewTranslationKeys = (options) => {
34
+ const { previous, next } = options;
35
+ const targetNamespaces = options.namespaces ?? Object.keys(next.namespaces);
36
+ const additions = [];
37
+ for (const namespace of targetNamespaces) {
38
+ const nextEntries = next.namespaces[namespace] ?? {};
39
+ const prevEntries = previous?.namespaces[namespace] ?? {};
40
+ for (const [key, defaultValue] of Object.entries(nextEntries)) {
41
+ if (Object.prototype.hasOwnProperty.call(prevEntries, key)) {
42
+ continue;
43
+ }
44
+ additions.push({
45
+ key,
46
+ namespace,
47
+ defaultValue,
48
+ locale: next.locale,
49
+ });
50
+ }
51
+ }
52
+ return additions.sort((a, b) => {
53
+ const namespaceCompare = a.namespace.localeCompare(b.namespace);
54
+ if (namespaceCompare !== 0) {
55
+ return namespaceCompare;
56
+ }
57
+ return a.key.localeCompare(b.key);
58
+ });
59
+ };
60
+ const headersToRecord = (headers) => {
61
+ if (!headers) {
62
+ return {};
63
+ }
64
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
65
+ const out = {};
66
+ headers.forEach((value, key) => {
67
+ out[key] = value;
68
+ });
69
+ return out;
70
+ }
71
+ if (Array.isArray(headers)) {
72
+ return headers.reduce((acc, [key, value]) => {
73
+ acc[key] = value;
74
+ return acc;
75
+ }, {});
76
+ }
77
+ return { ...headers };
78
+ };
79
+ export const handleNewTranslationKeys = async (options) => {
80
+ const { newKeys, pushConfig, requestInit, logger = console } = options;
81
+ const dryRun = pushConfig.dryRun || !pushConfig.pushUrl;
82
+ if (!newKeys.length) {
83
+ logger.log(`🗒️ No new translation keys for ${pushConfig.portalName}.`);
84
+ return { pushed: 0, dryRun };
85
+ }
86
+ const translations = {};
87
+ for (const keyEntry of newKeys) {
88
+ translations[keyEntry.key] = keyEntry.defaultValue;
89
+ }
90
+ const payload = {
91
+ data: {
92
+ translations,
93
+ },
94
+ };
95
+ if (dryRun) {
96
+ logger.log(`💡 Dry run for ${pushConfig.portalName}:\n` +
97
+ newKeys.map((key) => ` • ${key.namespace}:${key.key}`).join("\n"));
98
+ return { pushed: 0, dryRun: true };
99
+ }
100
+ const fetchImpl = pushConfig.fetchImpl ?? globalThis.fetch;
101
+ if (!fetchImpl) {
102
+ throw new Error("No fetch implementation available. Provide pushConfig.fetchImpl in non-Node 18 environments.");
103
+ }
104
+ const headerBag = {
105
+ "Content-Type": "application/json",
106
+ ...pushConfig.headers,
107
+ };
108
+ if (pushConfig.authorizationToken && !headerBag.Authorization) {
109
+ headerBag.Authorization = `Bearer ${pushConfig.authorizationToken}`;
110
+ }
111
+ Object.assign(headerBag, headersToRecord(requestInit?.headers));
112
+ const init = {
113
+ ...requestInit,
114
+ method: "POST",
115
+ body: JSON.stringify(payload),
116
+ headers: headerBag,
117
+ };
118
+ const response = await fetchImpl(pushConfig.pushUrl, init);
119
+ if (!response.ok) {
120
+ const errorBody = await response.text().catch(() => "");
121
+ throw new Error(`Failed to push new translation keys (${response.status}): ${errorBody}`);
122
+ }
123
+ logger.log(`✅ Pushed ${newKeys.length} keys for ${pushConfig.portalName}.`);
124
+ return { pushed: newKeys.length, dryRun: false };
125
+ };
@@ -0,0 +1,18 @@
1
+ import { LocalizationConfig } from "./types.js";
2
+ /** Minimal surface of the i18next instance the loader needs. Keeps this module
3
+ * decoupled from i18next so it is trivially unit-testable. */
4
+ export interface I18nLike {
5
+ addResourceBundle(lng: string, ns: string, resources: Record<string, unknown>, deep?: boolean, overwrite?: boolean): void;
6
+ }
7
+ /** Build the catalog URL for a language from the localization config. */
8
+ export declare function buildCatalogUrl(loc: LocalizationConfig, lng: string): string;
9
+ /** Unwrap the Ayinza `{ data: { translations } }` envelope. Tolerates a bare
10
+ * `{ translations }` or a flat map, and returns `{}` for anything else. */
11
+ export declare function extractTranslations(payload: unknown): Record<string, string>;
12
+ /**
13
+ * Create a function that fetches a language's catalog from localization-service
14
+ * and overlays it (deep-merge, remote wins) onto whatever i18next already holds
15
+ * for that language. De-duplicates by language; never throws; a failed load is
16
+ * not marked done, so a later `languageChanged` can retry.
17
+ */
18
+ export declare function createRemoteCatalogLoader(i18n: I18nLike, loc: LocalizationConfig, ns: string, fetchImpl?: typeof fetch): (lng: string) => Promise<void>;
@@ -0,0 +1,51 @@
1
+ const DEFAULT_PATH = "/l10n/translations/{{lng}}";
2
+ /** Build the catalog URL for a language from the localization config. */
3
+ export function buildCatalogUrl(loc, lng) {
4
+ const path = (loc.path ?? DEFAULT_PATH).replace("{{lng}}", encodeURIComponent(lng));
5
+ const base = loc.baseUrl.replace(/\/+$/, "");
6
+ const query = loc.category
7
+ ? `?category=${encodeURIComponent(loc.category)}`
8
+ : "";
9
+ return `${base}${path}${query}`;
10
+ }
11
+ /** Unwrap the Ayinza `{ data: { translations } }` envelope. Tolerates a bare
12
+ * `{ translations }` or a flat map, and returns `{}` for anything else. */
13
+ export function extractTranslations(payload) {
14
+ const p = payload;
15
+ const translations = p?.data?.translations ?? p?.translations;
16
+ return translations && typeof translations === "object"
17
+ ? translations
18
+ : {};
19
+ }
20
+ /**
21
+ * Create a function that fetches a language's catalog from localization-service
22
+ * and overlays it (deep-merge, remote wins) onto whatever i18next already holds
23
+ * for that language. De-duplicates by language; never throws; a failed load is
24
+ * not marked done, so a later `languageChanged` can retry.
25
+ */
26
+ export function createRemoteCatalogLoader(i18n, loc, ns, fetchImpl = fetch) {
27
+ const handled = new Set();
28
+ return async function load(lng) {
29
+ if (!lng || handled.has(lng)) {
30
+ return;
31
+ }
32
+ handled.add(lng);
33
+ try {
34
+ const res = await fetchImpl(buildCatalogUrl(loc, lng), loc.headers ? { headers: loc.headers } : undefined);
35
+ if (!res.ok) {
36
+ handled.delete(lng);
37
+ return;
38
+ }
39
+ const translations = extractTranslations(await res.json());
40
+ if (Object.keys(translations).length > 0) {
41
+ // deep=true, overwrite=true: remote values win over the bundled base;
42
+ // keys absent remotely keep their bundled-English fallback.
43
+ i18n.addResourceBundle(lng, ns, translations, true, true);
44
+ }
45
+ }
46
+ catch (err) {
47
+ handled.delete(lng);
48
+ console.warn(`[i18n] Failed to load remote catalog for ${lng}:`, err);
49
+ }
50
+ };
51
+ }