@chipmobilesdk/rn-i18n 0.1.2 → 0.2.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/CHANGELOG.md +168 -57
- package/README.md +277 -218
- package/package.json +49 -49
- package/skills/sdk-rn-i18n/SKILL.md +65 -62
- package/src/config/catalog.ts +25 -0
- package/src/config/diagnostics.ts +68 -0
- package/src/config/errors.ts +28 -0
- package/src/config/types.ts +150 -152
- package/src/config/validateConfig.ts +161 -174
- package/src/format/currency.ts +34 -27
- package/src/format/date.ts +32 -26
- package/src/format/invalid.ts +22 -16
- package/src/format/number.ts +30 -25
- package/src/format/units.ts +139 -151
- package/src/i18n.ts +231 -222
- package/src/index.ts +58 -50
- package/src/locale/activeLocale.ts +42 -45
- package/src/locale/deviceLocale.ts +50 -54
- package/src/locale/keyValueStorage.ts +27 -0
- package/src/locale/persistence.ts +59 -37
- package/src/react/I18nProvider.tsx +31 -35
- package/src/react/hooks.ts +66 -61
- package/src/tooling/cli.ts +76 -78
- package/src/tooling/generateKeys.ts +62 -68
- package/src/tooling/index.ts +27 -32
- package/src/tooling/report.ts +109 -117
- package/src/tooling/runner.ts +111 -126
- package/src/tooling/scan.ts +193 -210
- package/src/tooling/sync.ts +193 -198
- package/src/tooling/types.ts +119 -119
- package/src/translation/interpolate.ts +23 -23
- package/src/translation/plurals.ts +62 -69
- package/src/translation/resolver.ts +161 -146
- package/src/translation/runtimeKeys.ts +27 -27
|
@@ -1,174 +1,161 @@
|
|
|
1
|
-
import type { I18nConfig, LocaleConfig } from './types';
|
|
2
|
-
import { I18nConfigError } from './types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Validates an I18nConfig before any localized content is served (FR-003,
|
|
6
|
-
* FR-046). Collects every violation and throws a single I18nConfigError so
|
|
7
|
-
* apps see the complete picture instead of fixing issues one at a time.
|
|
8
|
-
*/
|
|
9
|
-
export function validateConfig(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
issues.push(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (entry
|
|
117
|
-
issues.push(`Locale "${code}"
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
states.set(code, 'done');
|
|
165
|
-
return false;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
for (const locale of locales) {
|
|
169
|
-
if (visit(locale.code, [])) {
|
|
170
|
-
// Report the first cycle only; one clear cycle message is actionable.
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
1
|
+
import type { I18nConfig, LocaleConfig } from './types';
|
|
2
|
+
import { I18nConfigError } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates an I18nConfig before any localized content is served (FR-003,
|
|
6
|
+
* FR-046). Collects every violation and throws a single I18nConfigError so
|
|
7
|
+
* apps see the complete picture instead of fixing issues one at a time.
|
|
8
|
+
*/
|
|
9
|
+
export function validateConfig(config: I18nConfig, knownUnits: ReadonlySet<string>): void {
|
|
10
|
+
const issues: string[] = [];
|
|
11
|
+
const locales = Array.isArray(config.locales) ? config.locales : [];
|
|
12
|
+
|
|
13
|
+
if (locales.length === 0) {
|
|
14
|
+
issues.push('At least one supported locale must be declared.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const codes = new Set<string>();
|
|
18
|
+
for (const locale of locales) {
|
|
19
|
+
if (codes.has(locale.code)) {
|
|
20
|
+
issues.push(`Duplicate locale code "${locale.code}".`);
|
|
21
|
+
}
|
|
22
|
+
codes.add(locale.code);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const locale of locales) {
|
|
26
|
+
validateLocale(locale, codes, knownUnits, issues);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
detectFallbackCycles(locales, codes, issues);
|
|
30
|
+
|
|
31
|
+
if (locales.length > 0 && !codes.has(config.primaryLocale)) {
|
|
32
|
+
issues.push(`Primary locale "${config.primaryLocale}" is not a supported locale.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const code of codes) {
|
|
36
|
+
const resource = config.resources?.[code];
|
|
37
|
+
if (resource === undefined || resource === null) {
|
|
38
|
+
issues.push(`Locale "${code}" has no translation resource.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (config.persistLocale !== false && !config.storage) {
|
|
43
|
+
issues.push(
|
|
44
|
+
'Locale persistence is enabled but no storage adapter was injected. ' +
|
|
45
|
+
'Provide `storage` or set `persistLocale: false`.',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const runtimeKeys = new Set<string>();
|
|
50
|
+
for (const entry of config.runtimeEntries ?? []) {
|
|
51
|
+
if (!entry.key) {
|
|
52
|
+
issues.push('Runtime-only entries must declare a non-empty key.');
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (runtimeKeys.has(entry.key)) {
|
|
56
|
+
issues.push(`Duplicate runtime-only entry "${entry.key}".`);
|
|
57
|
+
}
|
|
58
|
+
runtimeKeys.add(entry.key);
|
|
59
|
+
if (typeof entry.defaultText !== 'string') {
|
|
60
|
+
issues.push(`Runtime-only entry "${entry.key}" must declare default text.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (issues.length > 0) {
|
|
65
|
+
throw new I18nConfigError(issues);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateLocale(
|
|
70
|
+
locale: LocaleConfig,
|
|
71
|
+
codes: ReadonlySet<string>,
|
|
72
|
+
knownUnits: ReadonlySet<string>,
|
|
73
|
+
issues: string[],
|
|
74
|
+
): void {
|
|
75
|
+
const code = locale.code;
|
|
76
|
+
if (!code) {
|
|
77
|
+
issues.push('Every locale must declare a non-empty code.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!locale.displayName) {
|
|
81
|
+
issues.push(`Locale "${code}" must declare a display name.`);
|
|
82
|
+
}
|
|
83
|
+
if (locale.direction !== 'ltr' && locale.direction !== 'rtl') {
|
|
84
|
+
issues.push(`Locale "${code}" must declare direction "ltr" or "rtl".`);
|
|
85
|
+
}
|
|
86
|
+
if (!locale.defaultCurrency) {
|
|
87
|
+
issues.push(`Locale "${code}" must declare a default currency.`);
|
|
88
|
+
} else if (!/^[A-Z]{3}$/.test(locale.defaultCurrency)) {
|
|
89
|
+
// An ISO 4217 code; anything else would fail every currency format (I18N-08).
|
|
90
|
+
issues.push(
|
|
91
|
+
`Locale "${code}" default currency "${locale.defaultCurrency}" is not a three-letter ISO 4217 code.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!knownUnits.has(locale.defaultUnit)) {
|
|
95
|
+
issues.push(`Locale "${code}" declares unknown default unit "${locale.defaultUnit}".`);
|
|
96
|
+
}
|
|
97
|
+
if (typeof locale.defaultDateFormat !== 'object' || !locale.defaultDateFormat) {
|
|
98
|
+
issues.push(`Locale "${code}" must declare a default date format.`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof locale.defaultNumberFormat !== 'object' || !locale.defaultNumberFormat) {
|
|
101
|
+
issues.push(`Locale "${code}" must declare a default number format.`);
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(locale.fallbackChain)) {
|
|
104
|
+
issues.push(`Locale "${code}" must declare a fallback chain array.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const seen = new Set<string>();
|
|
108
|
+
for (const entry of locale.fallbackChain) {
|
|
109
|
+
if (entry === code) {
|
|
110
|
+
issues.push(`Locale "${code}" must not include itself in its fallback chain.`);
|
|
111
|
+
}
|
|
112
|
+
if (seen.has(entry)) {
|
|
113
|
+
issues.push(`Locale "${code}" lists duplicate fallback-chain entry "${entry}".`);
|
|
114
|
+
}
|
|
115
|
+
seen.add(entry);
|
|
116
|
+
if (!codes.has(entry)) {
|
|
117
|
+
issues.push(`Locale "${code}" references unsupported fallback locale "${entry}".`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function detectFallbackCycles(
|
|
123
|
+
locales: readonly LocaleConfig[],
|
|
124
|
+
codes: ReadonlySet<string>,
|
|
125
|
+
issues: string[],
|
|
126
|
+
): void {
|
|
127
|
+
const chains = new Map(
|
|
128
|
+
locales.map(locale => [
|
|
129
|
+
locale.code,
|
|
130
|
+
(locale.fallbackChain ?? []).filter(entry => codes.has(entry)),
|
|
131
|
+
]),
|
|
132
|
+
);
|
|
133
|
+
const states = new Map<string, 'visiting' | 'done'>();
|
|
134
|
+
|
|
135
|
+
const visit = (code: string, path: string[]): boolean => {
|
|
136
|
+
const state = states.get(code);
|
|
137
|
+
if (state === 'done') {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (state === 'visiting') {
|
|
141
|
+
issues.push(`Fallback cycle detected: ${[...path, code].join(' -> ')}.`);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
states.set(code, 'visiting');
|
|
145
|
+
for (const next of chains.get(code) ?? []) {
|
|
146
|
+
if (visit(next, [...path, code])) {
|
|
147
|
+
states.set(code, 'done');
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
states.set(code, 'done');
|
|
152
|
+
return false;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
for (const locale of locales) {
|
|
156
|
+
if (visit(locale.code, [])) {
|
|
157
|
+
// Report the first cycle only; one clear cycle message is actionable.
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/format/currency.ts
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
1
|
+
import { getNumberFormat } from '@chipmobilesdk/rn-core';
|
|
2
|
+
import type { InvalidFormatPolicy } from '../config/types';
|
|
3
|
+
import { applyInvalidFormatPolicy } from './invalid';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Locale-aware currency formatting. The locale's configured default currency
|
|
7
|
+
* applies unless the caller passes an explicit currency override (FR-021).
|
|
8
|
+
*/
|
|
9
|
+
export function formatCurrencyValue(
|
|
10
|
+
value: number,
|
|
11
|
+
localeCode: string,
|
|
12
|
+
defaultCurrency: string,
|
|
13
|
+
currencyOverride: string | undefined,
|
|
14
|
+
options: Intl.NumberFormatOptions | undefined,
|
|
15
|
+
invalidFormat: InvalidFormatPolicy,
|
|
16
|
+
): string {
|
|
17
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
18
|
+
return applyInvalidFormatPolicy(
|
|
19
|
+
invalidFormat,
|
|
20
|
+
`Cannot format invalid currency value "${String(value)}" for locale "${localeCode}".`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const currency = currencyOverride ?? defaultCurrency;
|
|
24
|
+
// rn-core's cached formatter (I18N-03). An unknown currency code (`usd1`) used
|
|
25
|
+
// to throw out of render; it is a format problem now (I18N-08).
|
|
26
|
+
try {
|
|
27
|
+
return getNumberFormat(localeCode, { ...options, style: 'currency', currency }).format(value);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return applyInvalidFormatPolicy(
|
|
30
|
+
invalidFormat,
|
|
31
|
+
`Cannot format currency "${currency}" for locale "${localeCode}": ${String(error)}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/format/date.ts
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
import { getDateTimeFormat } from '@chipmobilesdk/rn-core';
|
|
2
|
+
import type { InvalidFormatPolicy } from '../config/types';
|
|
3
|
+
import { applyInvalidFormatPolicy } from './invalid';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Locale-aware date formatting through the Hermes built-in
|
|
7
|
+
* Intl.DateTimeFormat. The locale's configured default date format applies
|
|
8
|
+
* unless the caller passes explicit overrides (FR-020). Options are limited
|
|
9
|
+
* to component options supported by Hermes on both platforms.
|
|
10
|
+
*/
|
|
11
|
+
export function formatDateValue(
|
|
12
|
+
value: Date | number,
|
|
13
|
+
localeCode: string,
|
|
14
|
+
defaults: Intl.DateTimeFormatOptions,
|
|
15
|
+
override: Intl.DateTimeFormatOptions | undefined,
|
|
16
|
+
invalidFormat: InvalidFormatPolicy,
|
|
17
|
+
): string {
|
|
18
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
19
|
+
if (Number.isNaN(date.getTime())) {
|
|
20
|
+
return applyInvalidFormatPolicy(
|
|
21
|
+
invalidFormat,
|
|
22
|
+
`Cannot format invalid date value "${String(value)}" for locale "${localeCode}".`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const options = override ?? defaults;
|
|
26
|
+
// rn-core's cached formatter (I18N-03); a bad option is a format problem (I18N-08).
|
|
27
|
+
try {
|
|
28
|
+
return getDateTimeFormat(localeCode, options).format(date);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return applyInvalidFormatPolicy(invalidFormat, `Cannot format a date: ${String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/format/invalid.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import { defaultReporter, type I18nDiagnosticHandler } from '../config/diagnostics';
|
|
2
|
+
import type { InvalidFormatPolicy } from '../config/types';
|
|
3
|
+
|
|
4
|
+
/** The policy as an i18n instance resolves it: with the instance's reporter. */
|
|
5
|
+
export type ResolvedInvalidFormatPolicy = InvalidFormatPolicy & {
|
|
6
|
+
readonly report?: I18nDiagnosticHandler | undefined;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Applies the configured invalid-format behavior: return the placeholder, or
|
|
11
|
+
* throw a clear formatting error in `error` mode (FR-037, FR-045).
|
|
12
|
+
*/
|
|
13
|
+
export function applyInvalidFormatPolicy(
|
|
14
|
+
policy: ResolvedInvalidFormatPolicy,
|
|
15
|
+
message: string,
|
|
16
|
+
): string {
|
|
17
|
+
(policy.report ?? defaultReporter)({ code: 'invalid_format', message });
|
|
18
|
+
if (policy.mode === 'error') {
|
|
19
|
+
throw new RangeError(`[rn-i18n] ${message}`);
|
|
20
|
+
}
|
|
21
|
+
return policy.text;
|
|
22
|
+
}
|
package/src/format/number.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
1
|
+
import { getNumberFormat } from '@chipmobilesdk/rn-core';
|
|
2
|
+
import type { InvalidFormatPolicy } from '../config/types';
|
|
3
|
+
import { applyInvalidFormatPolicy } from './invalid';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Locale-aware number formatting through the Hermes built-in
|
|
7
|
+
* Intl.NumberFormat. The locale default options apply unless the caller
|
|
8
|
+
* passes explicit overrides (FR-019).
|
|
9
|
+
*/
|
|
10
|
+
export function formatNumberValue(
|
|
11
|
+
value: number,
|
|
12
|
+
localeCode: string,
|
|
13
|
+
defaults: Intl.NumberFormatOptions,
|
|
14
|
+
override: Intl.NumberFormatOptions | undefined,
|
|
15
|
+
invalidFormat: InvalidFormatPolicy,
|
|
16
|
+
): string {
|
|
17
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
18
|
+
return applyInvalidFormatPolicy(
|
|
19
|
+
invalidFormat,
|
|
20
|
+
`Cannot format invalid number value "${String(value)}" for locale "${localeCode}".`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
// rn-core's cached formatter (I18N-03); a bad option is a format problem, not a
|
|
24
|
+
// crash in render (I18N-08).
|
|
25
|
+
try {
|
|
26
|
+
return getNumberFormat(localeCode, { ...defaults, ...override }).format(value);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return applyInvalidFormatPolicy(invalidFormat, `Cannot format a number: ${String(error)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|