@snack-uikit/locale 0.15.2-preview-69fcfa33.0 → 0.15.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## <small>0.15.2 (2025-10-06)</small>
7
+
8
+ * feat(PDS-2843): add interpolation to useLocale ([dfd0a2e](https://github.com/cloud-ru-tech/snack-uikit/commit/dfd0a2e))
9
+
10
+
11
+
12
+
13
+
6
14
  ## 0.15.1 (2025-05-28)
7
15
 
8
16
 
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { Dictionary, DottedTranslationKey, ExtendedDictionary, LocaleDictionary, LocaleLang, Locales, OverrideLocales } from '../../types';
2
+ import { Dictionary, DottedTranslationKey, ExtendedDictionary, InterpolationObject, LocaleDictionary, LocaleLang, Locales, OverrideLocales } from '../../types';
3
3
  export declare const DEFAULT_LANG = "en-GB";
4
4
  export type LocaleContextType<D extends Dictionary> = {
5
5
  lang: LocaleLang | string;
@@ -17,7 +17,7 @@ export type ContextOptions<D extends Dictionary> = {
17
17
  defaultLanguage?: LocaleLang;
18
18
  };
19
19
  type LocaleComponentName<D extends Dictionary> = keyof LocaleDictionary<D>;
20
- type GetLocaleText<D extends Dictionary, T extends keyof LocaleDictionary<D> | undefined = undefined> = (key: DottedTranslationKey<D, T>) => string;
20
+ type GetLocaleText<D extends Dictionary, T extends keyof LocaleDictionary<D> | undefined = undefined> = (key: DottedTranslationKey<D, T>, interpolation?: InterpolationObject) => string;
21
21
  export declare function createLocaleContext<D extends Dictionary>({ extendedDictionary, defaultLanguage, }: ContextOptions<D>): {
22
22
  LocaleContext: import("react").Context<LocaleContextType<D>>;
23
23
  LocaleProvider: ({ lang, fallbackLang, overrideLocales, children }: LocaleProviderProps<D>) => import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- 'use client';
3
2
 
4
3
  var __importDefault = void 0 && (void 0).__importDefault || function (mod) {
5
4
  return mod && mod.__esModule ? mod : {
@@ -16,6 +15,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
16
15
  const lodash_merge_1 = __importDefault(require("lodash.merge"));
17
16
  const react_1 = require("react");
18
17
  const locales_1 = require("../../locales");
18
+ const interpolateTranslation_1 = require("../../utils/interpolateTranslation");
19
19
  exports.DEFAULT_LANG = 'en-GB';
20
20
  function mergeLocaleWithExtension(extension) {
21
21
  return (0, lodash_merge_1.default)({}, locales_1.LOCALES, extension);
@@ -72,7 +72,7 @@ function createLocaleContext(_ref) {
72
72
  }
73
73
  return localesByLang[componentName] || {};
74
74
  }, [componentName, localesByLang]);
75
- const getLocaleText = (0, react_1.useCallback)(key => {
75
+ const getLocaleText = (0, react_1.useCallback)((key, interpolation) => {
76
76
  let translation = '';
77
77
  const complexKey = key.split('.');
78
78
  if (complexKey.length === 1) {
@@ -89,7 +89,7 @@ function createLocaleContext(_ref) {
89
89
  console.warn(`Snack-uikit: the '${key}' key is not found in the current locale '${lang}'.`);
90
90
  return key;
91
91
  }
92
- return translation;
92
+ return (0, interpolateTranslation_1.interpolateTranslation)(translation, interpolation);
93
93
  }, [lang, locales]);
94
94
  return {
95
95
  t: getLocaleText,
@@ -10,3 +10,4 @@ export type Locales<D extends Dictionary> = Record<LocaleLang, LocaleDictionary<
10
10
  export type OverrideLocales<D extends Dictionary> = PartialDeep<Record<LocaleLang, LocaleDictionary<D>>>;
11
11
  export type DottedTranslationKey<D extends Dictionary, C extends keyof LocaleDictionary<D> | undefined = undefined> = C extends keyof LocaleDictionary<D> ? PathsToProps<LocaleDictionary<D>[C], string> : PathsToProps<LocaleDictionary<D>, string>;
12
12
  export type ExtendedDictionary<D> = Record<LocaleLang, D>;
13
+ export type InterpolationObject = Record<string, string | number>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ const vitest_1 = require("vitest");
7
+ const interpolateTranslation_1 = require("../interpolateTranslation");
8
+ (0, vitest_1.describe)('interpolateTranslation', () => {
9
+ (0, vitest_1.it)('replaces a single placeholder', () => {
10
+ const result = (0, interpolateTranslation_1.interpolateTranslation)('Hello there {{who}}!', {
11
+ who: 'General Kenobi'
12
+ });
13
+ (0, vitest_1.expect)(result).toBe('Hello there General Kenobi!');
14
+ });
15
+ (0, vitest_1.it)('replaces numbers correctly', () => {
16
+ const result = (0, interpolateTranslation_1.interpolateTranslation)('{{zero}}, {{three}}', {
17
+ zero: 0,
18
+ three: 3
19
+ });
20
+ (0, vitest_1.expect)(result).toBe('0, 3');
21
+ });
22
+ (0, vitest_1.it)('keeps placeholder intact when value is an empty string or undefined', () => {
23
+ const result = (0, interpolateTranslation_1.interpolateTranslation)('{{value}}, "{{toBeUndefined}}" and /{{toBeEmptyString}}/"', {
24
+ toBeEmptyString: '',
25
+ value: 'value'
26
+ });
27
+ (0, vitest_1.expect)(result).toBe('value, "{{toBeUndefined}}" and /{{toBeEmptyString}}/"');
28
+ });
29
+ (0, vitest_1.it)('returns original text when there are no placeholders', () => {
30
+ const result = (0, interpolateTranslation_1.interpolateTranslation)('No placeholders here', {
31
+ nothing: 'to replace'
32
+ });
33
+ (0, vitest_1.expect)(result).toBe('No placeholders here');
34
+ });
35
+ });
@@ -0,0 +1,2 @@
1
+ import { InterpolationObject } from '../types';
2
+ export declare function interpolateTranslation(text: string, interpolation?: InterpolationObject): string;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.interpolateTranslation = interpolateTranslation;
7
+ function interpolateTranslation(text, interpolation) {
8
+ if (!interpolation || Object.keys(interpolation).length === 0) {
9
+ return text;
10
+ }
11
+ return text.replace(/{{(.*?)}}/g, (match, p1) => {
12
+ const value = interpolation[p1];
13
+ return value !== undefined && value !== '' ? String(value) : match;
14
+ });
15
+ }
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { Dictionary, DottedTranslationKey, ExtendedDictionary, LocaleDictionary, LocaleLang, Locales, OverrideLocales } from '../../types';
2
+ import { Dictionary, DottedTranslationKey, ExtendedDictionary, InterpolationObject, LocaleDictionary, LocaleLang, Locales, OverrideLocales } from '../../types';
3
3
  export declare const DEFAULT_LANG = "en-GB";
4
4
  export type LocaleContextType<D extends Dictionary> = {
5
5
  lang: LocaleLang | string;
@@ -17,7 +17,7 @@ export type ContextOptions<D extends Dictionary> = {
17
17
  defaultLanguage?: LocaleLang;
18
18
  };
19
19
  type LocaleComponentName<D extends Dictionary> = keyof LocaleDictionary<D>;
20
- type GetLocaleText<D extends Dictionary, T extends keyof LocaleDictionary<D> | undefined = undefined> = (key: DottedTranslationKey<D, T>) => string;
20
+ type GetLocaleText<D extends Dictionary, T extends keyof LocaleDictionary<D> | undefined = undefined> = (key: DottedTranslationKey<D, T>, interpolation?: InterpolationObject) => string;
21
21
  export declare function createLocaleContext<D extends Dictionary>({ extendedDictionary, defaultLanguage, }: ContextOptions<D>): {
22
22
  LocaleContext: import("react").Context<LocaleContextType<D>>;
23
23
  LocaleProvider: ({ lang, fallbackLang, overrideLocales, children }: LocaleProviderProps<D>) => import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,8 @@
1
- 'use client';
2
1
  import { jsx as _jsx } from "react/jsx-runtime";
3
2
  import merge from 'lodash.merge';
4
3
  import { createContext, useCallback, useContext, useMemo } from 'react';
5
4
  import { LOCALES } from '../../locales';
5
+ import { interpolateTranslation } from '../../utils/interpolateTranslation';
6
6
  export const DEFAULT_LANG = 'en-GB';
7
7
  function mergeLocaleWithExtension(extension) {
8
8
  return merge({}, LOCALES, extension);
@@ -39,7 +39,7 @@ export function createLocaleContext({ extendedDictionary, defaultLanguage = DEFA
39
39
  }
40
40
  return (localesByLang[componentName] || {});
41
41
  }, [componentName, localesByLang]);
42
- const getLocaleText = useCallback(key => {
42
+ const getLocaleText = useCallback((key, interpolation) => {
43
43
  let translation = '';
44
44
  const complexKey = key.split('.');
45
45
  if (complexKey.length === 1) {
@@ -57,7 +57,7 @@ export function createLocaleContext({ extendedDictionary, defaultLanguage = DEFA
57
57
  console.warn(`Snack-uikit: the '${key}' key is not found in the current locale '${lang}'.`);
58
58
  return key;
59
59
  }
60
- return translation;
60
+ return interpolateTranslation(translation, interpolation);
61
61
  }, [lang, locales]);
62
62
  return {
63
63
  t: getLocaleText,
@@ -10,3 +10,4 @@ export type Locales<D extends Dictionary> = Record<LocaleLang, LocaleDictionary<
10
10
  export type OverrideLocales<D extends Dictionary> = PartialDeep<Record<LocaleLang, LocaleDictionary<D>>>;
11
11
  export type DottedTranslationKey<D extends Dictionary, C extends keyof LocaleDictionary<D> | undefined = undefined> = C extends keyof LocaleDictionary<D> ? PathsToProps<LocaleDictionary<D>[C], string> : PathsToProps<LocaleDictionary<D>, string>;
12
12
  export type ExtendedDictionary<D> = Record<LocaleLang, D>;
13
+ export type InterpolationObject = Record<string, string | number>;
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { interpolateTranslation } from '../interpolateTranslation';
3
+ describe('interpolateTranslation', () => {
4
+ it('replaces a single placeholder', () => {
5
+ const result = interpolateTranslation('Hello there {{who}}!', { who: 'General Kenobi' });
6
+ expect(result).toBe('Hello there General Kenobi!');
7
+ });
8
+ it('replaces numbers correctly', () => {
9
+ const result = interpolateTranslation('{{zero}}, {{three}}', {
10
+ zero: 0,
11
+ three: 3,
12
+ });
13
+ expect(result).toBe('0, 3');
14
+ });
15
+ it('keeps placeholder intact when value is an empty string or undefined', () => {
16
+ const result = interpolateTranslation('{{value}}, "{{toBeUndefined}}" and /{{toBeEmptyString}}/"', {
17
+ toBeEmptyString: '',
18
+ value: 'value',
19
+ });
20
+ expect(result).toBe('value, "{{toBeUndefined}}" and /{{toBeEmptyString}}/"');
21
+ });
22
+ it('returns original text when there are no placeholders', () => {
23
+ const result = interpolateTranslation('No placeholders here', { nothing: 'to replace' });
24
+ expect(result).toBe('No placeholders here');
25
+ });
26
+ });
@@ -0,0 +1,2 @@
1
+ import { InterpolationObject } from '../types';
2
+ export declare function interpolateTranslation(text: string, interpolation?: InterpolationObject): string;
@@ -0,0 +1,9 @@
1
+ export function interpolateTranslation(text, interpolation) {
2
+ if (!interpolation || Object.keys(interpolation).length === 0) {
3
+ return text;
4
+ }
5
+ return text.replace(/{{(.*?)}}/g, (match, p1) => {
6
+ const value = interpolation[p1];
7
+ return value !== undefined && value !== '' ? String(value) : match;
8
+ });
9
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Locale",
7
- "version": "0.15.2-preview-69fcfa33.0",
7
+ "version": "0.15.2",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -41,5 +41,5 @@
41
41
  "devDependencies": {
42
42
  "@types/lodash.merge": "4.6.9"
43
43
  },
44
- "gitHead": "29427722aa93f2a800d130b6ab3db58fee5901c3"
44
+ "gitHead": "6cb0fa075cbf0ea8966672deb05dcbfa65e13326"
45
45
  }
@@ -1,4 +1,3 @@
1
- 'use client'
2
1
  import merge from 'lodash.merge';
3
2
  import { createContext, ReactNode, useCallback, useContext, useMemo } from 'react';
4
3
 
@@ -7,12 +6,14 @@ import {
7
6
  Dictionary,
8
7
  DottedTranslationKey,
9
8
  ExtendedDictionary,
9
+ InterpolationObject,
10
10
  LocaleDictionary,
11
11
  LocaleLang,
12
12
  Locales,
13
13
  OverrideLocales,
14
14
  TranslatedEntity,
15
15
  } from '../../types';
16
+ import { interpolateTranslation } from '../../utils/interpolateTranslation';
16
17
 
17
18
  export const DEFAULT_LANG = 'en-GB';
18
19
 
@@ -44,6 +45,7 @@ type LocaleComponentName<D extends Dictionary> = keyof LocaleDictionary<D>;
44
45
 
45
46
  type GetLocaleText<D extends Dictionary, T extends keyof LocaleDictionary<D> | undefined = undefined> = (
46
47
  key: DottedTranslationKey<D, T>,
48
+ interpolation?: InterpolationObject,
47
49
  ) => string;
48
50
 
49
51
  export function createLocaleContext<D extends Dictionary>({
@@ -104,7 +106,7 @@ export function createLocaleContext<D extends Dictionary>({
104
106
  }, [componentName, localesByLang]);
105
107
 
106
108
  const getLocaleText: GetLocaleText<D, C> = useCallback(
107
- key => {
109
+ (key, interpolation) => {
108
110
  let translation = '';
109
111
 
110
112
  const complexKey = key.split('.');
@@ -129,7 +131,7 @@ export function createLocaleContext<D extends Dictionary>({
129
131
  return key;
130
132
  }
131
133
 
132
- return translation;
134
+ return interpolateTranslation(translation, interpolation);
133
135
  },
134
136
  [lang, locales],
135
137
  );
package/src/types.ts CHANGED
@@ -23,3 +23,5 @@ export type DottedTranslationKey<
23
23
  : PathsToProps<LocaleDictionary<D>, string>;
24
24
 
25
25
  export type ExtendedDictionary<D> = Record<LocaleLang, D>;
26
+
27
+ export type InterpolationObject = Record<string, string | number>;
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { interpolateTranslation } from '../interpolateTranslation';
4
+
5
+ describe('interpolateTranslation', () => {
6
+ it('replaces a single placeholder', () => {
7
+ const result = interpolateTranslation('Hello there {{who}}!', { who: 'General Kenobi' });
8
+ expect(result).toBe('Hello there General Kenobi!');
9
+ });
10
+
11
+ it('replaces numbers correctly', () => {
12
+ const result = interpolateTranslation('{{zero}}, {{three}}', {
13
+ zero: 0,
14
+ three: 3,
15
+ });
16
+ expect(result).toBe('0, 3');
17
+ });
18
+
19
+ it('keeps placeholder intact when value is an empty string or undefined', () => {
20
+ const result = interpolateTranslation('{{value}}, "{{toBeUndefined}}" and /{{toBeEmptyString}}/"', {
21
+ toBeEmptyString: '',
22
+ value: 'value',
23
+ });
24
+ expect(result).toBe('value, "{{toBeUndefined}}" and /{{toBeEmptyString}}/"');
25
+ });
26
+
27
+ it('returns original text when there are no placeholders', () => {
28
+ const result = interpolateTranslation('No placeholders here', { nothing: 'to replace' });
29
+ expect(result).toBe('No placeholders here');
30
+ });
31
+ });
@@ -0,0 +1,12 @@
1
+ import { InterpolationObject } from '../types';
2
+
3
+ export function interpolateTranslation(text: string, interpolation?: InterpolationObject) {
4
+ if (!interpolation || Object.keys(interpolation).length === 0) {
5
+ return text;
6
+ }
7
+
8
+ return text.replace(/{{(.*?)}}/g, (match, p1) => {
9
+ const value = interpolation[p1];
10
+ return value !== undefined && value !== '' ? String(value) : match;
11
+ });
12
+ }