smartloc 1.2.2 → 2.0.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.
Files changed (116) hide show
  1. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
  2. package/.vscode/launch.json +65 -0
  3. package/.vscode/settings.json +14 -0
  4. package/.vscode/tasks.json +27 -0
  5. package/README.md +225 -0
  6. package/build/README.md +225 -0
  7. package/build/package.json +60 -0
  8. package/bun.lock +310 -0
  9. package/index.js +1 -26
  10. package/package.json +17 -18
  11. package/samples/express/main.ts +37 -0
  12. package/samples/express/package.json +10 -0
  13. package/samples/graphql/main.ts +73 -0
  14. package/samples/graphql/package.json +15 -0
  15. package/src/adapters/adapter-base.ts +58 -0
  16. package/src/adapters/index.ts +31 -0
  17. package/src/adapters/json.ts +18 -0
  18. package/src/adapters/xliff.ts +30 -0
  19. package/src/cli/cli.ts +137 -0
  20. package/src/cli/collect.ts +196 -0
  21. package/src/cli/interfaces.ts +7 -0
  22. package/src/cli/reconciliate.ts +119 -0
  23. package/src/cli/utils.ts +36 -0
  24. package/src/core/default-locale.tsx +54 -0
  25. package/src/core/interfaces.ts +49 -0
  26. package/{core/json-utils.js → src/core/json-utils.ts} +45 -34
  27. package/src/core/literal.ts +66 -0
  28. package/src/core/load.ts +82 -0
  29. package/src/core/locale-list.ts +128 -0
  30. package/src/core/locale.tsx +75 -0
  31. package/src/core/smartloc.tsx +338 -0
  32. package/src/core/tag.ts +40 -0
  33. package/src/core/utils.tsx +24 -0
  34. package/src/express.ts +93 -0
  35. package/{graphql.js → src/graphql.ts} +80 -68
  36. package/src/index.ts +7 -0
  37. package/src/node/index.ts +1 -0
  38. package/src/node/load.ts +32 -0
  39. package/test/collect.spec.ts +45 -0
  40. package/test/reconciliation.spec.ts +132 -0
  41. package/test/serialization.spec.ts +162 -0
  42. package/test/translation.spec.ts +61 -0
  43. package/tsconfig.json +37 -0
  44. package/tslint.json +103 -0
  45. package/adapters/adapter-base.d.ts +0 -10
  46. package/adapters/adapter-base.js +0 -46
  47. package/adapters/adapter-base.js.map +0 -1
  48. package/adapters/index.d.ts +0 -3
  49. package/adapters/index.js +0 -33
  50. package/adapters/index.js.map +0 -1
  51. package/adapters/json.d.ts +0 -6
  52. package/adapters/json.js +0 -22
  53. package/adapters/json.js.map +0 -1
  54. package/adapters/xliff.d.ts +0 -6
  55. package/adapters/xliff.js +0 -32
  56. package/adapters/xliff.js.map +0 -1
  57. package/cli/cli.d.ts +0 -2
  58. package/cli/cli.js +0 -128
  59. package/cli/cli.js.map +0 -1
  60. package/cli/collect.d.ts +0 -12
  61. package/cli/collect.js +0 -151
  62. package/cli/collect.js.map +0 -1
  63. package/cli/interfaces.d.ts +0 -25
  64. package/cli/interfaces.js +0 -3
  65. package/cli/interfaces.js.map +0 -1
  66. package/cli/reconciliate.d.ts +0 -6
  67. package/cli/reconciliate.js +0 -108
  68. package/cli/reconciliate.js.map +0 -1
  69. package/cli/utils.d.ts +0 -7
  70. package/cli/utils.js +0 -54
  71. package/cli/utils.js.map +0 -1
  72. package/core/base-loc.d.ts +0 -3
  73. package/core/base-loc.js +0 -20
  74. package/core/base-loc.js.map +0 -1
  75. package/core/default-locale.d.ts +0 -8
  76. package/core/default-locale.js +0 -42
  77. package/core/default-locale.js.map +0 -1
  78. package/core/interfaces.d.ts +0 -32
  79. package/core/interfaces.js +0 -3
  80. package/core/interfaces.js.map +0 -1
  81. package/core/json-utils.d.ts +0 -19
  82. package/core/json-utils.js.map +0 -1
  83. package/core/literal.d.ts +0 -9
  84. package/core/literal.js +0 -52
  85. package/core/literal.js.map +0 -1
  86. package/core/locale-list.d.ts +0 -19
  87. package/core/locale-list.js +0 -84
  88. package/core/locale-list.js.map +0 -1
  89. package/core/locale.d.ts +0 -13
  90. package/core/locale.js +0 -34
  91. package/core/locale.js.map +0 -1
  92. package/core/locstr-array.d.ts +0 -10
  93. package/core/locstr-array.js +0 -48
  94. package/core/locstr-array.js.map +0 -1
  95. package/core/smartloc.d.ts +0 -64
  96. package/core/smartloc.js +0 -222
  97. package/core/smartloc.js.map +0 -1
  98. package/core/tag.d.ts +0 -5
  99. package/core/tag.js +0 -17
  100. package/core/tag.js.map +0 -1
  101. package/core/utils.d.ts +0 -1
  102. package/core/utils.js +0 -13
  103. package/core/utils.js.map +0 -1
  104. package/express.d.ts +0 -12
  105. package/express.js +0 -85
  106. package/express.js.map +0 -1
  107. package/graphql.d.ts +0 -12
  108. package/graphql.js.map +0 -1
  109. package/index.d.ts +0 -7
  110. package/index.js.map +0 -1
  111. package/node/index.d.ts +0 -1
  112. package/node/index.js +0 -14
  113. package/node/index.js.map +0 -1
  114. package/node/load.d.ts +0 -12
  115. package/node/load.js +0 -44
  116. package/node/load.js.map +0 -1
@@ -0,0 +1,54 @@
1
+ import { ILocaleDef, LocLiteral, ILiteralLocalizer } from './interfaces';
2
+ import { LiteralLocalizer } from './literal';
3
+ import { getLocale } from './locale-list';
4
+ import { asFragment, autoGenerateId, getLocaleCode } from './utils';
5
+ import React from 'react';
6
+
7
+ const emptyStringAutoId = autoGenerateId([]);
8
+ export class DefaultLocale implements ILocaleDef {
9
+
10
+ readonly literals: ILiteralLocalizer;
11
+ readonly code: string | undefined;
12
+
13
+ constructor(readonly id: string) {
14
+ this.literals = new LiteralLocalizer(this);
15
+ this.code = getLocaleCode(id);
16
+ }
17
+
18
+ localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: false): string;
19
+ localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: true): React.ReactNode;
20
+ localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: boolean): string | React.ReactNode {
21
+ const existing = getLocale(this.id);
22
+ if (existing) {
23
+ const exist = existing.localize(id, count, parts, placeholders, react);
24
+ if (exist) {
25
+ return exist;
26
+ }
27
+ }
28
+
29
+ if (!parts || !placeholders) {
30
+ // special case for empty string, which could not be translated.
31
+ if (id === emptyStringAutoId) {
32
+ return '';
33
+ }
34
+ throw new Error(`The smartloc string "${id}" cannot be translated to your default locale. It is likely that is has been deserialized. In order to support deserialized strings, you MUST load your default locale as a classic locale. nb: To generate your default locale translation file through smartloc CLI, use --generateDefault option`)
35
+ }
36
+
37
+ // fallback to the in-code translation
38
+ const result: React.ReactNode[] = [];
39
+
40
+ // interleave the literals with the placeholders
41
+ for (let i = 0; i < placeholders.length; i++) {
42
+ result.push(parts[i]);
43
+ result.push(this.literals.localize(placeholders[i], react));
44
+ }
45
+
46
+ // add the last literal
47
+ result.push(parts[parts.length - 1]);
48
+ if (react) {
49
+ // return a fragment having result as children
50
+ return asFragment(result);
51
+ }
52
+ return result.join('');
53
+ }
54
+ }
@@ -0,0 +1,49 @@
1
+ export type LocStr = InternalLocStr & React.ReactElement;
2
+
3
+ export interface InternalLocStr {
4
+ /**
5
+ * Localizes this string. To be used within a @see withLocales scope to have an actual translation.
6
+ */
7
+ toString(locale?: string | ILocaleDef): string;
8
+ toJSON(): any;
9
+
10
+ /** Apply a transformation to the final translated text */
11
+ transform(transformer: (x: string) => string): LocStr;
12
+
13
+ /** React hook equivalent of .toString(), but which will updated when local changes */
14
+ useAsString(): string;
15
+ }
16
+
17
+ export type LocLiteral = string | number | Date | LocStr | React.ReactNode;
18
+ export interface ILiteralLocalizer {
19
+ localize(data: LocLiteral, react: false): string;
20
+ localize(data: LocLiteral, react: true): React.ReactNode;
21
+ localize(data: LocLiteral, react: boolean): string | React.ReactNode;
22
+ }
23
+
24
+ export interface ILocaleDef {
25
+ readonly id: string;
26
+ readonly code: string | undefined;
27
+ readonly literals: ILiteralLocalizer;
28
+ localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: false): string | null;
29
+ localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: true): React.ReactNode | null;
30
+ localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: boolean): string | React.ReactNode | null;
31
+ }
32
+
33
+ export type TranslationTarget = string | {
34
+ singular: string | null;
35
+ plural: string;
36
+ }
37
+
38
+ export type TranslationOf<T> = T extends LocStr ? string
39
+ : T extends Object ? { [K in keyof T]: TranslationOf<T[K]> }
40
+ : T extends (infer E)[] ? TranslationOf<E>[]
41
+ : T;
42
+
43
+
44
+ export type StorableOf<T> = T extends LocStr ? Stored
45
+ : T extends Object ? { [K in keyof T]: StorableOf<T[K]> }
46
+ : T extends (infer E)[] ? StorableOf<E>[]
47
+ : T;
48
+
49
+ type Stored = string | { [key: string]: string } | { i18n: string; data: any[]; count?: number } | LocStr; // LocStr is there in case nonDescriptive is 'skip'
@@ -1,46 +1,48 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.translateInContext = exports.translateObject = exports.toJsonStorable = exports.toLocalizable = exports.jsonParseLocalized = void 0;
7
- const smartloc_1 = require("./smartloc");
8
- const moment_1 = __importDefault(require("moment"));
9
- const literal_1 = require("./literal");
1
+ import { singleLoc, multiLoc, withSerializationContext, withLocales, SerializationContextOptions, smartLoc } from './smartloc';
2
+ import { isLocStr } from './literal';
3
+ import type { TranslationOf, StorableOf } from './interfaces';
4
+
10
5
  /**
11
6
  * Parse a JSON string where localized strings have been serialized in their non translated form
12
7
  */
13
- function jsonParseLocalized(value) {
8
+ export function jsonParseLocalized(value: string) {
14
9
  const parsed = JSON.parse(value);
15
10
  return _toLocalizable(parsed, 50);
16
11
  }
17
- exports.jsonParseLocalized = jsonParseLocalized;
12
+
18
13
  /**
19
14
  * Transforms an object that has been deserialized from a non translated from to an object that is translatable
20
15
  */
21
- function toLocalizable(value) {
16
+ export function toLocalizable(value: any) {
22
17
  return _toLocalizable(value, 50);
23
18
  }
24
- exports.toLocalizable = toLocalizable;
25
- function _toLocalizable(value, depth) {
19
+ function _toLocalizable(value: any, depth: number): any {
26
20
  if (depth < 0) {
27
21
  throw new Error('This object is either too nested, or has cycles');
28
22
  }
29
23
  if (!value) {
30
24
  return value;
31
25
  }
26
+
32
27
  if (typeof value === 'string') {
33
28
  if (value.startsWith('i18n/single:')) {
34
- return new smartloc_1.SingleLoc(value.substr('i18n/single:'.length));
29
+ return singleLoc(value.substr('i18n/single:'.length));
35
30
  }
36
31
  if (value.startsWith('i18n/id:')) {
37
- return new smartloc_1.SmartLoc(value.substr('i18n/id:'.length), null, null);
32
+ return smartLoc({
33
+ id: value.substr('i18n/id:'.length),
34
+ count: undefined,
35
+ literals: null,
36
+ placeholders: null
37
+ });
38
38
  }
39
39
  return value;
40
40
  }
41
+
41
42
  if (typeof value !== 'object' || value instanceof Date) {
42
43
  return value;
43
44
  }
45
+
44
46
  // === handle arrays
45
47
  if (value instanceof Array) {
46
48
  const retArray = [];
@@ -58,18 +60,26 @@ function _toLocalizable(value, depth) {
58
60
  if (!keys.length) {
59
61
  return value;
60
62
  }
63
+
61
64
  // === handle smartloc with args
62
65
  if (keys.length === 2 && keys.includes('i18n') && keys.includes('data') && typeof value.i18n === 'string' && value.data instanceof Array) {
63
- return new smartloc_1.SmartLoc(value.i18n, null, value.data);
64
- }
66
+ return smartLoc({
67
+ id: value.i18n,
68
+ literals: null,
69
+ placeholders: value.data,
70
+ count: value.count
71
+ });
72
+ }
73
+
65
74
  // === handle multi
66
- const ret = {};
75
+ const ret: Record<string, string> = {};
67
76
  if (!keys.some(x => !x.startsWith('i18n:'))) {
68
77
  for (const k of keys) {
69
78
  ret[k.substr('i18n:'.length)] = value[k];
70
79
  }
71
- return new smartloc_1.MultiLoc(ret);
80
+ return multiLoc(ret);
72
81
  }
82
+
73
83
  // === handle plain object
74
84
  let changed = false;
75
85
  for (const k of keys) {
@@ -80,28 +90,29 @@ function _toLocalizable(value, depth) {
80
90
  ? ret
81
91
  : value;
82
92
  }
93
+
83
94
  /**
84
95
  * Transforms an object so it can be stored with localized strings in their non translated form
85
96
  * @returns Either a new copy of your object, or your object (if had no smarloc strings in it)
86
97
  */
87
- function toJsonStorable(value, options) {
88
- return smartloc_1.withSerializationContext(() => _toJsonStorable(value, 50), options);
98
+ export function toJsonStorable<T>(value: T, options?: SerializationContextOptions): StorableOf<T> {
99
+ return withSerializationContext(() => _toJsonStorable(value, 50), options);
89
100
  }
90
- exports.toJsonStorable = toJsonStorable;
101
+
91
102
  /** Translates an object to the given language */
92
- function translateObject(locales, object) {
103
+ export function translateObject<T>(locales: string | string[], object: T): TranslationOf<T> {
93
104
  if (typeof locales === 'string') {
94
105
  locales = [locales];
95
106
  }
96
- return smartloc_1.withLocales(locales, () => _toJsonStorable(object, 50));
107
+ return withLocales(locales, () => _toJsonStorable(object, 50));
97
108
  }
98
- exports.translateObject = translateObject;
109
+
99
110
  /** Translates an object according to the current context (call this when wrapped in withSerializationContext() or withLocales()) */
100
- function translateInContext(object) {
111
+ export function translateInContext(object: any) {
101
112
  return _toJsonStorable(object, 50);
102
113
  }
103
- exports.translateInContext = translateInContext;
104
- function _toJsonStorable(v, depth) {
114
+
115
+ function _toJsonStorable(v: any, depth: number) {
105
116
  if (depth < 0) {
106
117
  throw new Error('This object is either too nested, or has cycles');
107
118
  }
@@ -111,11 +122,11 @@ function _toJsonStorable(v, depth) {
111
122
  if (typeof v !== 'object') {
112
123
  return v;
113
124
  }
114
- if (moment_1.default.isMoment(v) || moment_1.default.isDuration(v)) {
125
+ if (v instanceof Date) {
115
126
  return v;
116
127
  }
117
128
  if (v instanceof Array) {
118
- let ret;
129
+ let ret: any[] | undefined = undefined;
119
130
  for (let i = 0; i < v.length; i++) {
120
131
  const p = _toJsonStorable(v[i], depth - 1);
121
132
  if (p !== v[i] && !ret) {
@@ -130,10 +141,11 @@ function _toJsonStorable(v, depth) {
130
141
  }
131
142
  return ret || v;
132
143
  }
133
- if (literal_1.isLocStr(v)) {
144
+ if (isLocStr(v)) {
134
145
  return v.toJSON();
135
146
  }
136
- const obj = {};
147
+
148
+ const obj: Record<string, any> = {};
137
149
  let diff = false;
138
150
  for (const [k, p] of Object.entries(v)) {
139
151
  const val = _toJsonStorable(p, depth - 1);
@@ -148,4 +160,3 @@ function _toJsonStorable(v, depth) {
148
160
  Object.setPrototypeOf(obj, Object.getPrototypeOf(v));
149
161
  return obj;
150
162
  }
151
- //# sourceMappingURL=json-utils.js.map
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { LocLiteral, LocStr, ILocaleDef, ILiteralLocalizer } from './interfaces';
3
+
4
+ const locTag = Symbol('_localizable');
5
+ export function isLocStr(data: any): data is LocStr {
6
+ return data && typeof data === 'object' && data[locTag];
7
+ }
8
+
9
+ export function setIsLoc(data: any) {
10
+ data[locTag] = true;
11
+ Object.freeze(data);
12
+ }
13
+
14
+ export class LiteralLocalizer implements ILiteralLocalizer {
15
+ private numbers: Intl.NumberFormat;
16
+ private dateTimeFormat: Intl.DateTimeFormat;
17
+ private dateFormat: Intl.DateTimeFormat;
18
+
19
+ constructor(private locale: ILocaleDef) {
20
+ this.numbers = new Intl.NumberFormat(locale.id);
21
+ // Format for dates with time (equivalent to moment's 'LLL')
22
+ this.dateTimeFormat = new Intl.DateTimeFormat(locale.id, {
23
+ year: 'numeric',
24
+ month: 'long',
25
+ day: 'numeric',
26
+ hour: 'numeric',
27
+ minute: 'numeric'
28
+ });
29
+ // Format for dates without time (equivalent to moment's 'LL')
30
+ this.dateFormat = new Intl.DateTimeFormat(locale.id, {
31
+ year: 'numeric',
32
+ month: 'long',
33
+ day: 'numeric'
34
+ });
35
+ }
36
+
37
+ localize(data: LocLiteral, react: false): string;
38
+ localize(data: LocLiteral, react: true): React.ReactNode;
39
+ localize(data: LocLiteral, react: boolean): string | React.ReactNode {
40
+ if (typeof data === 'string') {
41
+ return data;
42
+ }
43
+ if (typeof data === 'number') {
44
+ return this.numbers.format(data);
45
+ }
46
+ if (data instanceof Date) {
47
+ // Check if the date has a time component (non-zero hours or minutes)
48
+ if (data.getHours() || data.getMinutes()) {
49
+ return this.dateTimeFormat.format(data);
50
+ }
51
+ return this.dateFormat.format(data);
52
+ }
53
+ if (isLocStr(data)) {
54
+ return data.toString(this.locale);
55
+ }
56
+ if (react) {
57
+ return data as React.ReactNode;
58
+ } else {
59
+ if (isLocStr(data)) {
60
+ return data.toString(this.locale);
61
+ }
62
+ console.error(new Error('Cannot use React elements in LocStr elements when not used in a React renderer'));
63
+ return '';
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,82 @@
1
+ import { addLocale } from './locale-list';
2
+ import type { TranslationTarget } from './interfaces';
3
+
4
+ /**
5
+ * Loads a locale generated from CLI
6
+ * @param filePath The file path to load
7
+ * @param merge If true, then the translations will be merged with already existing translations.
8
+ */
9
+ export function loadJsonLocale(obj: GroupedTranslation, merge?: boolean): void
10
+ export function loadJsonLocale(url: string, merge?: boolean): Promise<void>
11
+ export function loadJsonLocale(urlOrObj: string | GroupedTranslation, merge?: boolean) {
12
+ if (typeof urlOrObj === 'string') {
13
+ return fetch(urlOrObj).then(async response => {
14
+ if (!response.ok) {
15
+ throw new Error(`Failed to fetch ${urlOrObj}: ${response.statusText}`);
16
+ }
17
+ const text = await response.json();
18
+ if (typeof text !== 'object') {
19
+ throw new Error(`Expecting ${urlOrObj} to be a JSON object`);
20
+ }
21
+ return loadJsonLocale(text, merge);
22
+ });
23
+ }
24
+
25
+ const transformed = convertLocale(ungroupTranslation(urlOrObj));
26
+ addLocale(urlOrObj.targetLanguage, transformed, merge);
27
+ }
28
+
29
+
30
+ export interface CollectItem {
31
+ source?: TranslationTarget;
32
+ target?: TranslationTarget | null;
33
+ dirty?: boolean;
34
+ }
35
+
36
+ export interface CollectResult {
37
+ [id: string]: CollectItem;
38
+ }
39
+
40
+ export interface Translation {
41
+ resources: CollectResult;
42
+ sourceLanguage?: string;
43
+ targetLanguage: string;
44
+ }
45
+
46
+
47
+ export interface GroupedTranslation {
48
+ resources: { [namespace: string]: CollectResult };
49
+ sourceLanguage?: string;
50
+ targetLanguage: string;
51
+ }
52
+
53
+
54
+ export function ungroupTranslation(translation: GroupedTranslation): Translation {
55
+ const result: Translation = {
56
+ targetLanguage: translation.targetLanguage,
57
+ sourceLanguage: translation.sourceLanguage,
58
+ resources: {},
59
+ };
60
+ for (const [namespace, data] of Object.entries(translation.resources)) {
61
+ for (const [id, val] of Object.entries(data ?? {})) {
62
+ if (namespace === '$default') {
63
+ result.resources[id] = val;
64
+ } else {
65
+ result.resources[`${namespace}.${id}`] = val;
66
+ }
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+
73
+
74
+ export function convertLocale(locale: Translation): { [key: string]: TranslationTarget } {
75
+ const ret: { [key: string]: TranslationTarget } = {};
76
+ for (const [k, v] of Object.entries(locale.resources)) {
77
+ if (v.target) {
78
+ ret[k] = v.target;
79
+ }
80
+ }
81
+ return ret;
82
+ }
@@ -0,0 +1,128 @@
1
+ import { ILocaleDef, TranslationTarget } from './interfaces';
2
+ import { DefaultLocale } from './default-locale';
3
+ import { Locale } from './locale';
4
+
5
+ let locales: { [key: string]: Locale } = {};
6
+
7
+ let defaultLocale: ILocaleDef | null = null;
8
+ let currentLocale: ILocaleDef | null = null;
9
+
10
+ /** Defines the locale used in code */
11
+ export function setDefaultLocale(localeId: string) {
12
+ defaultLocale = new DefaultLocale(localeId);
13
+ currentLocale = defaultLocale;
14
+ }
15
+
16
+ export function changeLocale(locale: ILocaleDef | string, throwIfNotFound = true) {
17
+ const loc = typeof locale === 'string' ? getLocale(locale) : locale;
18
+ if (!loc) {
19
+ if (!throwIfNotFound) {
20
+ return 'not found';
21
+ }
22
+ throw new Error(`Locale '${locale}' not found. Please load it first`);
23
+ }
24
+ if (currentLocale === loc) {
25
+ return 'unchanged';
26
+ }
27
+ currentLocale = loc;
28
+ subs.forEach(sub => sub());
29
+ return 'changed';
30
+ }
31
+
32
+ const subs: Set<() => void> = new Set();
33
+ export function subscribeToLocaleChange(callback: () => void) {
34
+ subs.add(callback);
35
+ return () => { subs.delete(callback); };
36
+ }
37
+ export function getCurrentLocale(noDefault?: false): ILocaleDef;
38
+ export function getCurrentLocale(noDefault: true): ILocaleDef | null;
39
+ export function getCurrentLocale(noDefault = false) {
40
+ if (currentLocale || noDefault) {
41
+ return currentLocale ?? null;
42
+ }
43
+ return getDefaultLocale();
44
+ }
45
+
46
+ export function getDefaultLocale() {
47
+ if (!defaultLocale) {
48
+ throw new Error('You must specify default locale via setDefaultLocale() before using localization');
49
+ }
50
+ return defaultLocale;
51
+ }
52
+
53
+ /** Resets all locales */
54
+ export function clearLocales() {
55
+ locales = {};
56
+ }
57
+
58
+ /** Remove a locale */
59
+ export function removeLocale(id: string) {
60
+ id = id.toLowerCase();
61
+ const existing = locales[id];
62
+ if (!existing) {
63
+ return;
64
+ }
65
+ delete locales[id];
66
+ const bl = baseLocale(id);
67
+ if (bl && locales[bl] === existing) {
68
+ delete locales[bl];
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Adds translations
74
+ * @param id Locale ID (ex: 'en', or 'en-US')
75
+ * @param localeDef Translations (string id as key, formatted string as values - ex : {myId: 'Translated {0} text {1}'})
76
+ * @param merge If true, then the translations will be merged with already existing translations.
77
+ */
78
+ export function addLocale(id: string, localeDef: { [id: string]: TranslationTarget }, merge?: boolean) {
79
+ id = id.toLowerCase();
80
+ let loc = locales[id];
81
+ if (loc) {
82
+ if (!merge) {
83
+ throw new Error(`There already is a locale '${id}' defined. Set the 'merge' argument to 'true' if you want to add translations`);
84
+ }
85
+ loc.localeDef = {
86
+ ...loc.localeDef,
87
+ ...localeDef,
88
+ };
89
+ return;
90
+ }
91
+ loc = locales[id] = new Locale(id, localeDef);
92
+ const bl = baseLocale(id);
93
+ if (bl && !locales[bl]) {
94
+ locales[bl] = loc;
95
+ }
96
+ }
97
+
98
+ export function getLocale(id: string): ILocaleDef | null {
99
+ if (!id) {
100
+ return null;
101
+ }
102
+ const found = locales[id];
103
+ if (found) {
104
+ return found;
105
+ }
106
+ // if given "en-US", then try to downgrade to a non specific language.
107
+ const bl = baseLocale(id);
108
+ return bl && locales[bl] || null;
109
+ }
110
+
111
+ function baseLocale(l: string) {
112
+ const m = /^([a-zA-Z]+)\-[a-zA-Z]+$/.exec(l);
113
+ return m?.[1] || null;
114
+ }
115
+
116
+ export function* listLocales() {
117
+ yield* Object.keys(locales);
118
+ if (defaultLocale) {
119
+ yield defaultLocale.id;
120
+ }
121
+ }
122
+
123
+ export function* listLocaleDefs(): IterableIterator<ILocaleDef> {
124
+ yield* Object.values(locales);
125
+ if (defaultLocale) {
126
+ yield defaultLocale;
127
+ }
128
+ }
@@ -0,0 +1,75 @@
1
+ import { ILocaleDef, ILiteralLocalizer, LocLiteral, TranslationTarget } from './interfaces';
2
+ import { LiteralLocalizer } from './literal';
3
+ import React from 'react';
4
+ import { asFragment, getLocaleCode } from './utils';
5
+
6
+ export class Locale implements ILocaleDef {
7
+ readonly literals: ILiteralLocalizer;
8
+ readonly code: string | undefined;
9
+
10
+ constructor(readonly id: string, public localeDef: { [id: string]: TranslationTarget }) {
11
+ this.literals = new LiteralLocalizer(this);
12
+ this.code = getLocaleCode(id);
13
+ }
14
+
15
+ localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react?: false): string | null;
16
+ localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: true): React.ReactNode | null;
17
+ localize(id: string, count: number | null, _parts: TemplateStringsArray, placeholders: LocLiteral[], react?: boolean): string | React.ReactNode | null {
18
+ const templateObj = this.localeDef[id];
19
+ if (!templateObj) {
20
+ return null;
21
+ }
22
+ let template: string | undefined = undefined;
23
+ if (typeof templateObj === 'string') {
24
+ template = templateObj;
25
+ } else {
26
+ if (count === 1 && templateObj.singular) {
27
+ template = templateObj.singular;
28
+ } else {
29
+ template = templateObj.plural;
30
+ }
31
+ }
32
+ if (!react) {
33
+ // same, but faster
34
+ const literals = placeholders?.map(x => this.literals.localize(x, false));
35
+ return format(template, literals);
36
+ }
37
+
38
+
39
+ const ret: React.ReactNode[] = [];
40
+ // match all {N} in the template and replace them with the corresponding literal
41
+ const matches = template.matchAll(/\{(\d+)\}/g);
42
+ let currentIndex = 0;
43
+ for (const match of matches) {
44
+ const fromLast = template.substring(currentIndex, match.index);
45
+ if (fromLast) {
46
+ ret.push(fromLast);
47
+ }
48
+ const i = parseInt(match[1]);
49
+ const p = placeholders[i];
50
+ if (p) {
51
+ ret.push(this.literals.localize(p, true));
52
+ } else {
53
+ console.warn(`Placeholder {${i}} in template of ${id} for locale ${this.id} does not exist`);
54
+ }
55
+ currentIndex = match.index + match[0].length;
56
+ }
57
+ const fromLast = template.substring(currentIndex);
58
+ if (fromLast) {
59
+ ret.push(fromLast);
60
+ }
61
+ return asFragment(ret);
62
+ }
63
+ }
64
+
65
+ function format(str: string, args: string[]) {
66
+ if (!args || !args.length) {
67
+ return str;
68
+ }
69
+ return str.replace(/{(\d+)}/g, function (match, number) {
70
+ return typeof args[number] !== 'undefined'
71
+ ? args[number]
72
+ : match
73
+ ;
74
+ });
75
+ }