@ayinza_dev/i18n-config 1.4.1 → 1.5.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/dist/config.js CHANGED
@@ -57,6 +57,7 @@ const mergeFormatters = (base, override) => {
57
57
  ...override?.date,
58
58
  },
59
59
  fallbackLocale: override?.fallbackLocale ?? base?.fallbackLocale,
60
+ numberingSystem: override?.numberingSystem ?? base?.numberingSystem,
60
61
  };
61
62
  };
62
63
  function mergeConfig(base, override) {
@@ -4,9 +4,16 @@ export declare class I18nFormatters {
4
4
  private numberConfig;
5
5
  private dateConfig;
6
6
  private fallbackLocale;
7
+ private numberingSystem;
7
8
  private normalizeLanguage;
8
9
  constructor(config?: FormattersConfig);
9
10
  private getLocale;
11
+ /**
12
+ * Force a numbering system across every Intl formatter (incl. RelativeTime,
13
+ * which has no `numberingSystem` option) by appending the Unicode `-u-nu-`
14
+ * locale extension. No-op when disabled or already present.
15
+ */
16
+ private applyNumberingSystem;
10
17
  /**
11
18
  * Format with locale-specific formatting
12
19
  */
@@ -21,6 +21,10 @@ export class I18nFormatters {
21
21
  constructor(config = {}) {
22
22
  this.fallbackLocale =
23
23
  config.fallbackLocale || defaultLocaleMapping.en || "en-US";
24
+ // Default to Western (latn) digits so localized output keeps native words
25
+ // (month/weekday/currency labels) and RTL but renders 0-9. `??` (not `||`)
26
+ // so an explicit "" opts back into each locale's native numbering.
27
+ this.numberingSystem = config.numberingSystem ?? "latn";
24
28
  this.currencyConfig = {
25
29
  defaultCurrency: config.currency?.defaultCurrency || "USD",
26
30
  localeMapping: {
@@ -48,7 +52,19 @@ export class I18nFormatters {
48
52
  }
49
53
  getLocale(language, mapping) {
50
54
  const { full, base } = this.normalizeLanguage(language);
51
- return mapping[full] || mapping[base] || this.fallbackLocale;
55
+ const locale = mapping[full] || mapping[base] || this.fallbackLocale;
56
+ return this.applyNumberingSystem(locale);
57
+ }
58
+ /**
59
+ * Force a numbering system across every Intl formatter (incl. RelativeTime,
60
+ * which has no `numberingSystem` option) by appending the Unicode `-u-nu-`
61
+ * locale extension. No-op when disabled or already present.
62
+ */
63
+ applyNumberingSystem(locale) {
64
+ if (!this.numberingSystem || locale.includes("-u-nu-")) {
65
+ return locale;
66
+ }
67
+ return `${locale}-u-nu-${this.numberingSystem}`;
52
68
  }
53
69
  /**
54
70
  * Format with locale-specific formatting
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export type { I18nConfig, I18nInitOptions, FormattersConfig, LocaleMapping, Pars
3
3
  export { I18nFormatters } from "./formatters.js";
4
4
  export { useFormatting, useI18n } from "./hooks.js";
5
5
  export { createI18nextParserConfig } from "./parser-config.js";
6
- export { createTranslationSnapshot, collectNewTranslationKeys, handleNewTranslationKeys, } from "./parser-hooks.js";
6
+ export { createTranslationSnapshot, collectNewTranslationKeys, handleNewTranslationKeys, publishTranslationOverrides, } from "./parser-hooks.js";
7
+ export type { PublishTranslationOverridesOptions } from "./parser-hooks.js";
7
8
  export { useTranslation, Trans, Translation } from "react-i18next";
8
9
  export { type TFunction } from "i18next";
package/dist/index.js CHANGED
@@ -2,6 +2,6 @@ export { initializeI18n, getI18nInstance, getFormatters, defaultConfig, createI1
2
2
  export { I18nFormatters } from "./formatters.js";
3
3
  export { useFormatting, useI18n } from "./hooks.js";
4
4
  export { createI18nextParserConfig } from "./parser-config.js";
5
- export { createTranslationSnapshot, collectNewTranslationKeys, handleNewTranslationKeys, } from "./parser-hooks.js";
5
+ export { createTranslationSnapshot, collectNewTranslationKeys, handleNewTranslationKeys, publishTranslationOverrides, } from "./parser-hooks.js";
6
6
  // Re-export commonly used utilities from react-i18next
7
7
  export { useTranslation, Trans, Translation } from "react-i18next";
@@ -21,3 +21,24 @@ export interface HandleNewTranslationKeysResult {
21
21
  export declare const createTranslationSnapshot: (options: CreateTranslationSnapshotOptions) => TranslationSnapshot;
22
22
  export declare const collectNewTranslationKeys: (options: CollectNewTranslationKeysOptions) => NewKeyPayload[];
23
23
  export declare const handleNewTranslationKeys: (options: HandleNewTranslationKeysOptions) => Promise<HandleNewTranslationKeysResult>;
24
+ export interface PublishTranslationOverridesOptions {
25
+ /** Target locale whose values are being corrected, e.g. `ar-SA`. */
26
+ locale: string;
27
+ /** Flat `key -> corrected value` map. Only keys present are overridden. */
28
+ translations: Record<string, string>;
29
+ /**
30
+ * Push settings. `pushUrl` is the override collection endpoint (e.g.
31
+ * `${base}/l10n/admin/translations`); the locale is appended per request.
32
+ */
33
+ pushConfig: ParserPushConfig;
34
+ requestInit?: RequestInit;
35
+ logger?: Pick<Console, "log" | "warn" | "error">;
36
+ }
37
+ /**
38
+ * Publish curated translation overrides for a locale to the localization
39
+ * service's upsert endpoint (`PUT /admin/translations/{locale}`). Any Ayinza
40
+ * product uses this to correct its own catalog (e.g. replacing garbled machine
41
+ * translations); the service marks the rows human-authoritative so the MT
42
+ * sweep never re-manufactures them.
43
+ */
44
+ export declare const publishTranslationOverrides: (options: PublishTranslationOverridesOptions) => Promise<HandleNewTranslationKeysResult>;
@@ -76,31 +76,22 @@ const headersToRecord = (headers) => {
76
76
  }
77
77
  return { ...headers };
78
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
- }
79
+ /**
80
+ * Send a `{ data: { translations, category? } }` catalog payload to the
81
+ * localization-service. Shared by the source-key push (POST /translate/send)
82
+ * and the override publish (PUT /admin/translations/{locale}).
83
+ */
84
+ const sendCatalogPayload = async (options) => {
85
+ const { url, method, translations, pushConfig, requestInit } = options;
90
86
  const payload = {
91
87
  data: {
92
88
  translations,
93
- // Tag every pushed key with the producer's category when configured so
89
+ // Tag the payload with the producer's category when configured so
94
90
  // consumers can fetch a scoped catalog. Omitted entirely when unset to
95
91
  // preserve the un-categorised (shared) default.
96
92
  ...(pushConfig.category ? { category: pushConfig.category } : {}),
97
93
  },
98
94
  };
99
- if (dryRun) {
100
- logger.log(`💡 Dry run for ${pushConfig.portalName}:\n` +
101
- newKeys.map((key) => ` • ${key.namespace}:${key.key}`).join("\n"));
102
- return { pushed: 0, dryRun: true };
103
- }
104
95
  const fetchImpl = pushConfig.fetchImpl ?? globalThis.fetch;
105
96
  if (!fetchImpl) {
106
97
  throw new Error("No fetch implementation available. Provide pushConfig.fetchImpl in non-Node 18 environments.");
@@ -113,17 +104,74 @@ export const handleNewTranslationKeys = async (options) => {
113
104
  headerBag.Authorization = `Bearer ${pushConfig.authorizationToken}`;
114
105
  }
115
106
  Object.assign(headerBag, headersToRecord(requestInit?.headers));
116
- const init = {
107
+ const response = await fetchImpl(url, {
117
108
  ...requestInit,
118
- method: "POST",
109
+ method,
119
110
  body: JSON.stringify(payload),
120
111
  headers: headerBag,
121
- };
122
- const response = await fetchImpl(pushConfig.pushUrl, init);
112
+ });
123
113
  if (!response.ok) {
124
114
  const errorBody = await response.text().catch(() => "");
125
- throw new Error(`Failed to push new translation keys (${response.status}): ${errorBody}`);
115
+ throw new Error(`Failed to ${method} translations (${response.status}): ${errorBody}`);
116
+ }
117
+ };
118
+ export const handleNewTranslationKeys = async (options) => {
119
+ const { newKeys, pushConfig, requestInit, logger = console } = options;
120
+ const dryRun = pushConfig.dryRun || !pushConfig.pushUrl;
121
+ if (!newKeys.length) {
122
+ logger.log(`🗒️ No new translation keys for ${pushConfig.portalName}.`);
123
+ return { pushed: 0, dryRun };
124
+ }
125
+ const translations = {};
126
+ for (const keyEntry of newKeys) {
127
+ translations[keyEntry.key] = keyEntry.defaultValue;
126
128
  }
129
+ if (dryRun) {
130
+ logger.log(`💡 Dry run for ${pushConfig.portalName}:\n` +
131
+ newKeys.map((key) => ` • ${key.namespace}:${key.key}`).join("\n"));
132
+ return { pushed: 0, dryRun: true };
133
+ }
134
+ await sendCatalogPayload({
135
+ url: pushConfig.pushUrl,
136
+ method: "POST",
137
+ translations,
138
+ pushConfig,
139
+ requestInit,
140
+ });
127
141
  logger.log(`✅ Pushed ${newKeys.length} keys for ${pushConfig.portalName}.`);
128
142
  return { pushed: newKeys.length, dryRun: false };
129
143
  };
144
+ /**
145
+ * Publish curated translation overrides for a locale to the localization
146
+ * service's upsert endpoint (`PUT /admin/translations/{locale}`). Any Ayinza
147
+ * product uses this to correct its own catalog (e.g. replacing garbled machine
148
+ * translations); the service marks the rows human-authoritative so the MT
149
+ * sweep never re-manufactures them.
150
+ */
151
+ export const publishTranslationOverrides = async (options) => {
152
+ const { locale, translations, pushConfig, requestInit, logger = console } = options;
153
+ const count = Object.keys(translations).length;
154
+ const dryRun = pushConfig.dryRun || !pushConfig.pushUrl;
155
+ if (!count) {
156
+ logger.log(`🗒️ No translation overrides for ${pushConfig.portalName}.`);
157
+ return { pushed: 0, dryRun };
158
+ }
159
+ if (dryRun) {
160
+ logger.log(`💡 Dry run for ${pushConfig.portalName} (${locale}):\n` +
161
+ Object.keys(translations)
162
+ .map((key) => ` • ${key}`)
163
+ .join("\n"));
164
+ return { pushed: 0, dryRun: true };
165
+ }
166
+ const base = pushConfig.pushUrl.replace(/\/+$/, "");
167
+ const url = `${base}/${encodeURIComponent(locale)}`;
168
+ await sendCatalogPayload({
169
+ url,
170
+ method: "PUT",
171
+ translations,
172
+ pushConfig,
173
+ requestInit,
174
+ });
175
+ logger.log(`✅ Published ${count} ${locale} overrides for ${pushConfig.portalName}.`);
176
+ return { pushed: count, dryRun: false };
177
+ };
package/dist/types.d.ts CHANGED
@@ -21,6 +21,15 @@ export interface FormattersConfig {
21
21
  date?: DateConfig;
22
22
  /** Locale to fall back to when a language code is missing from mappings. */
23
23
  fallbackLocale?: string;
24
+ /**
25
+ * Unicode numbering system applied to all number/date/currency/percent/
26
+ * relative-time output via the locale's `-u-nu-` extension. Defaults to
27
+ * `"latn"` (Western 0-9 digits) so locales like `ar-SA` keep Arabic words
28
+ * (month/weekday names, currency labels) and RTL while rendering Western
29
+ * digits — the common convention for finance/tax UIs. Set to `"arab"` for
30
+ * Eastern Arabic numerals, or `""` to use each locale's native digits.
31
+ */
32
+ numberingSystem?: string;
24
33
  }
25
34
  export interface ReactConfig {
26
35
  useSuspense?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ayinza_dev/i18n-config",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",