@chipmobilesdk/rn-i18n 0.2.0 → 0.4.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 (201) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +48 -17
  3. package/lib/commonjs/config/catalog.js +29 -0
  4. package/lib/commonjs/config/catalog.js.map +1 -0
  5. package/lib/commonjs/config/diagnostics.js +56 -0
  6. package/lib/commonjs/config/diagnostics.js.map +1 -0
  7. package/lib/commonjs/config/errors.js +35 -0
  8. package/lib/commonjs/config/errors.js.map +1 -0
  9. package/lib/commonjs/config/types.js +13 -0
  10. package/lib/commonjs/config/types.js.map +1 -0
  11. package/lib/commonjs/config/validateConfig.js +134 -0
  12. package/lib/commonjs/config/validateConfig.js.map +1 -0
  13. package/lib/commonjs/format/currency.js +35 -0
  14. package/lib/commonjs/format/currency.js.map +1 -0
  15. package/lib/commonjs/format/date.js +36 -0
  16. package/lib/commonjs/format/date.js.map +1 -0
  17. package/lib/commonjs/format/invalid.js +24 -0
  18. package/lib/commonjs/format/invalid.js.map +1 -0
  19. package/lib/commonjs/format/number.js +29 -0
  20. package/lib/commonjs/format/number.js.map +1 -0
  21. package/lib/commonjs/format/units.js +184 -0
  22. package/lib/commonjs/format/units.js.map +1 -0
  23. package/lib/commonjs/i18n.js +234 -0
  24. package/lib/commonjs/i18n.js.map +1 -0
  25. package/lib/commonjs/index.js +93 -0
  26. package/lib/commonjs/index.js.map +1 -0
  27. package/lib/commonjs/locale/activeLocale.js +37 -0
  28. package/lib/commonjs/locale/activeLocale.js.map +1 -0
  29. package/lib/commonjs/locale/deviceLocale.js +52 -0
  30. package/lib/commonjs/locale/deviceLocale.js.map +1 -0
  31. package/lib/commonjs/locale/keyValueStorage.js +29 -0
  32. package/lib/commonjs/locale/keyValueStorage.js.map +1 -0
  33. package/lib/commonjs/locale/persistence.js +74 -0
  34. package/lib/commonjs/locale/persistence.js.map +1 -0
  35. package/lib/commonjs/package.json +1 -0
  36. package/lib/commonjs/react/I18nProvider.js +36 -0
  37. package/lib/commonjs/react/I18nProvider.js.map +1 -0
  38. package/lib/commonjs/react/hooks.js +47 -0
  39. package/lib/commonjs/react/hooks.js.map +1 -0
  40. package/lib/commonjs/translation/interpolate.js +24 -0
  41. package/lib/commonjs/translation/interpolate.js.map +1 -0
  42. package/lib/commonjs/translation/plurals.js +122 -0
  43. package/lib/commonjs/translation/plurals.js.map +1 -0
  44. package/lib/commonjs/translation/resolver.js +124 -0
  45. package/lib/commonjs/translation/resolver.js.map +1 -0
  46. package/lib/commonjs/translation/runtimeKeys.js +55 -0
  47. package/lib/commonjs/translation/runtimeKeys.js.map +1 -0
  48. package/lib/module/config/catalog.js +25 -0
  49. package/lib/module/config/catalog.js.map +1 -0
  50. package/lib/module/config/diagnostics.js +51 -0
  51. package/lib/module/config/diagnostics.js.map +1 -0
  52. package/lib/module/config/errors.js +30 -0
  53. package/lib/module/config/errors.js.map +1 -0
  54. package/lib/module/config/types.js +26 -0
  55. package/lib/module/config/types.js.map +1 -0
  56. package/lib/module/config/validateConfig.js +131 -0
  57. package/lib/module/config/validateConfig.js.map +1 -0
  58. package/lib/module/format/currency.js +32 -0
  59. package/lib/module/format/currency.js.map +1 -0
  60. package/lib/module/format/date.js +33 -0
  61. package/lib/module/format/date.js.map +1 -0
  62. package/lib/module/format/invalid.js +21 -0
  63. package/lib/module/format/invalid.js.map +1 -0
  64. package/lib/module/format/number.js +26 -0
  65. package/lib/module/format/number.js.map +1 -0
  66. package/lib/module/format/units.js +171 -0
  67. package/lib/module/format/units.js.map +1 -0
  68. package/lib/module/i18n.js +230 -0
  69. package/lib/module/i18n.js.map +1 -0
  70. package/lib/module/index.js +23 -0
  71. package/lib/module/index.js.map +1 -0
  72. package/lib/module/locale/activeLocale.js +33 -0
  73. package/lib/module/locale/activeLocale.js.map +1 -0
  74. package/lib/module/locale/deviceLocale.js +46 -0
  75. package/lib/module/locale/deviceLocale.js.map +1 -0
  76. package/lib/module/locale/keyValueStorage.js +25 -0
  77. package/lib/module/locale/keyValueStorage.js.map +1 -0
  78. package/lib/module/locale/persistence.js +66 -0
  79. package/lib/module/locale/persistence.js.map +1 -0
  80. package/lib/module/package.json +1 -0
  81. package/lib/module/react/I18nProvider.js +29 -0
  82. package/lib/module/react/I18nProvider.js.map +1 -0
  83. package/lib/module/react/hooks.js +42 -0
  84. package/lib/module/react/hooks.js.map +1 -0
  85. package/lib/module/translation/interpolate.js +20 -0
  86. package/lib/module/translation/interpolate.js.map +1 -0
  87. package/lib/module/translation/plurals.js +114 -0
  88. package/lib/module/translation/plurals.js.map +1 -0
  89. package/lib/module/translation/resolver.js +118 -0
  90. package/lib/module/translation/resolver.js.map +1 -0
  91. package/lib/module/translation/runtimeKeys.js +50 -0
  92. package/lib/module/translation/runtimeKeys.js.map +1 -0
  93. package/lib/typescript/commonjs/config/catalog.d.ts +18 -0
  94. package/lib/typescript/commonjs/config/catalog.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/config/diagnostics.d.ts +21 -0
  96. package/lib/typescript/commonjs/config/diagnostics.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/config/errors.d.ts +16 -0
  98. package/lib/typescript/commonjs/config/errors.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/config/types.d.ts +127 -0
  100. package/lib/typescript/commonjs/config/types.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/config/validateConfig.d.ts +8 -0
  102. package/lib/typescript/commonjs/config/validateConfig.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/format/currency.d.ts +12 -0
  104. package/lib/typescript/commonjs/format/currency.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/format/date.d.ts +14 -0
  106. package/lib/typescript/commonjs/format/date.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/format/invalid.d.ts +12 -0
  108. package/lib/typescript/commonjs/format/invalid.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/format/number.d.ts +8 -0
  110. package/lib/typescript/commonjs/format/number.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/format/units.d.ts +23 -0
  112. package/lib/typescript/commonjs/format/units.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/i18n.d.ts +46 -0
  114. package/lib/typescript/commonjs/i18n.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/index.d.ts +27 -0
  116. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/locale/activeLocale.d.ts +17 -0
  118. package/lib/typescript/commonjs/locale/activeLocale.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/locale/deviceLocale.d.ts +19 -0
  120. package/lib/typescript/commonjs/locale/deviceLocale.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/locale/keyValueStorage.d.ts +13 -0
  122. package/lib/typescript/commonjs/locale/keyValueStorage.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/locale/persistence.d.ts +24 -0
  124. package/lib/typescript/commonjs/locale/persistence.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/package.json +1 -0
  126. package/lib/typescript/commonjs/react/I18nProvider.d.ts +16 -0
  127. package/lib/typescript/commonjs/react/I18nProvider.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/react/hooks.d.ts +21 -0
  129. package/lib/typescript/commonjs/react/hooks.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/translation/interpolate.d.ts +8 -0
  131. package/lib/typescript/commonjs/translation/interpolate.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/translation/plurals.d.ts +13 -0
  133. package/lib/typescript/commonjs/translation/plurals.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/translation/resolver.d.ts +24 -0
  135. package/lib/typescript/commonjs/translation/resolver.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/translation/runtimeKeys.d.ts +16 -0
  137. package/lib/typescript/commonjs/translation/runtimeKeys.d.ts.map +1 -0
  138. package/lib/typescript/module/config/catalog.d.ts +18 -0
  139. package/lib/typescript/module/config/catalog.d.ts.map +1 -0
  140. package/lib/typescript/module/config/diagnostics.d.ts +21 -0
  141. package/lib/typescript/module/config/diagnostics.d.ts.map +1 -0
  142. package/lib/typescript/module/config/errors.d.ts +16 -0
  143. package/lib/typescript/module/config/errors.d.ts.map +1 -0
  144. package/lib/typescript/module/config/types.d.ts +127 -0
  145. package/lib/typescript/module/config/types.d.ts.map +1 -0
  146. package/lib/typescript/module/config/validateConfig.d.ts +8 -0
  147. package/lib/typescript/module/config/validateConfig.d.ts.map +1 -0
  148. package/lib/typescript/module/format/currency.d.ts +12 -0
  149. package/lib/typescript/module/format/currency.d.ts.map +1 -0
  150. package/lib/typescript/module/format/date.d.ts +14 -0
  151. package/lib/typescript/module/format/date.d.ts.map +1 -0
  152. package/lib/typescript/module/format/invalid.d.ts +12 -0
  153. package/lib/typescript/module/format/invalid.d.ts.map +1 -0
  154. package/lib/typescript/module/format/number.d.ts +8 -0
  155. package/lib/typescript/module/format/number.d.ts.map +1 -0
  156. package/lib/typescript/module/format/units.d.ts +23 -0
  157. package/lib/typescript/module/format/units.d.ts.map +1 -0
  158. package/lib/typescript/module/i18n.d.ts +46 -0
  159. package/lib/typescript/module/i18n.d.ts.map +1 -0
  160. package/lib/typescript/module/index.d.ts +27 -0
  161. package/lib/typescript/module/index.d.ts.map +1 -0
  162. package/lib/typescript/module/locale/activeLocale.d.ts +17 -0
  163. package/lib/typescript/module/locale/activeLocale.d.ts.map +1 -0
  164. package/lib/typescript/module/locale/deviceLocale.d.ts +19 -0
  165. package/lib/typescript/module/locale/deviceLocale.d.ts.map +1 -0
  166. package/lib/typescript/module/locale/keyValueStorage.d.ts +13 -0
  167. package/lib/typescript/module/locale/keyValueStorage.d.ts.map +1 -0
  168. package/lib/typescript/module/locale/persistence.d.ts +24 -0
  169. package/lib/typescript/module/locale/persistence.d.ts.map +1 -0
  170. package/lib/typescript/module/package.json +1 -0
  171. package/lib/typescript/module/react/I18nProvider.d.ts +16 -0
  172. package/lib/typescript/module/react/I18nProvider.d.ts.map +1 -0
  173. package/lib/typescript/module/react/hooks.d.ts +21 -0
  174. package/lib/typescript/module/react/hooks.d.ts.map +1 -0
  175. package/lib/typescript/module/translation/interpolate.d.ts +8 -0
  176. package/lib/typescript/module/translation/interpolate.d.ts.map +1 -0
  177. package/lib/typescript/module/translation/plurals.d.ts +13 -0
  178. package/lib/typescript/module/translation/plurals.d.ts.map +1 -0
  179. package/lib/typescript/module/translation/resolver.d.ts +24 -0
  180. package/lib/typescript/module/translation/resolver.d.ts.map +1 -0
  181. package/lib/typescript/module/translation/runtimeKeys.d.ts +16 -0
  182. package/lib/typescript/module/translation/runtimeKeys.d.ts.map +1 -0
  183. package/package.json +41 -18
  184. package/skills/sdk-rn-i18n/SKILL.md +27 -0
  185. package/src/config/diagnostics.ts +2 -0
  186. package/src/format/currency.ts +5 -0
  187. package/src/format/date.ts +10 -4
  188. package/src/i18n.ts +93 -9
  189. package/src/index.ts +5 -3
  190. package/src/locale/activeLocale.ts +10 -0
  191. package/src/locale/persistence.ts +19 -0
  192. package/src/translation/plurals.ts +74 -5
  193. package/src/translation/runtimeKeys.ts +28 -1
  194. package/src/tooling/cli.ts +0 -76
  195. package/src/tooling/generateKeys.ts +0 -62
  196. package/src/tooling/index.ts +0 -27
  197. package/src/tooling/report.ts +0 -109
  198. package/src/tooling/runner.ts +0 -111
  199. package/src/tooling/scan.ts +0 -193
  200. package/src/tooling/sync.ts +0 -193
  201. package/src/tooling/types.ts +0 -119
package/CHANGELOG.md CHANGED
@@ -1,5 +1,178 @@
1
1
  # @chipmobilesdk/rn-i18n
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Every peer range has an upper bound, and no package declares a peer it does not use. ([#21](https://github.com/operationd8979/chip-mobile-sdk/pull/21))
8
+
9
+ A peer range is now a caret, a tilde or an explicit `<`, except for `react` and
10
+ `react-native`: an app brings exactly one of each, so a floor is the only useful thing
11
+ to say about them. Every other peer is a library the app installs beside us, and an
12
+ open `>=` was a promise about versions nobody had run — mmkv 3→4 lost entitlements and
13
+ pending purchases exactly that way.
14
+
15
+ **Four of the new ceilings exclude a version that is already published**, so an app
16
+ that took the open range at its word may now see a peer warning. The new bound is the
17
+ major this release is tested against, which is the thing the old range failed to say:
18
+
19
+ | Peer | Was | Now | Published above the ceiling |
20
+ |---|---|---|---|
21
+ | `react-native-app-auth` (rn-auth) | `>=8` | `^8.0.0` | 9.0.0 |
22
+ | `react-native-keychain` (rn-auth) | `>=10` | `^10.0.0` | 11.0.0 |
23
+ | `@op-engineering/op-sqlite` (rn-local-db) | `>=17.1.0` | `^17.1.0` | 18.x, 19.x |
24
+ | `@react-native-firebase/messaging` (rn-notification) | `>=26.0.0` | `^26.0.0` | 27.x |
25
+
26
+ The others — `react-native-vision-camera` `^5.1.1`, `react-native-svg` `^15.15.0`,
27
+ `react-native-unistyles` `^3.2.0`, `@dr.pogodin/react-native-fs` `^2.39.2`,
28
+ `react-native-quick-crypto` `^1.0.0`, `react-native-notify-kit` `^10.5.0`,
29
+ `react-native-localize` `^3.7.0`, `react-native-mmkv` `^4.0.0` — exclude nothing that
30
+ exists today. rn-payment's mmkv range was already bounded (`>=4.0.0 <5`) and is now
31
+ written as the caret that means the same thing.
32
+
33
+ Peers nothing imports are gone. `rn-theme`, `rn-local-db` and `rn-notification` asked
34
+ for `react` without having a component or a hook between them; `rn-auth`, `rn-ai` and
35
+ `rn-i18n` asked for `react-native` without touching a React Native API (rn-auth reaches
36
+ both native modules through `lazyPeer`, rn-i18n formats on `Intl`); `rn-notification`
37
+ asked for `@react-native-firebase/app`, which messaging declares itself. `rn-ai` no
38
+ longer peers on `@chipmobilesdk/rn-auth` at all — it asks for a `TokenProvider`, a
39
+ two-method interface rn-auth's `auth` satisfies, so an app with its own token source
40
+ installs nothing extra. An app that enables push or signs in through rn-auth installs
41
+ the same packages as before; it is the manifest that stopped overstating.
42
+
43
+ `react-native-localize` is declared optional, which is what the code already did: the
44
+ time-zone reader goes through `lazyPeer` and falls back to `Intl` without it. The
45
+ `peerDependenciesMeta` entries that said `optional: false` are gone — that is npm's
46
+ default, and writing it down only invited the question of which peers were undecided.
47
+
48
+ `npm run check:manifests` now fails on an unbounded peer range, a peer nothing in
49
+ `src/` imports, and a `peerDependenciesMeta` entry for either, so the policy holds
50
+ without anyone remembering it.
51
+
52
+ - The extraction CLI is a package of its own, and it runs from npm. ([#21](https://github.com/operationd8979/chip-mobile-sdk/pull/21))
53
+
54
+ `@chipmobilesdk/rn-i18n-tooling` is new: it builds to JavaScript, declares the Node it
55
+ needs (`>=22.13.0`) and exposes a `chip-i18n` binary.
56
+
57
+ ```sh
58
+ npm i -D @chipmobilesdk/rn-i18n-tooling
59
+ npx chip-i18n sync # was: node --experimental-strip-types \
60
+ # node_modules/@chipmobilesdk/rn-i18n/src/tooling/cli.ts
61
+ ```
62
+
63
+ The tooling used to ship inside the runtime tarball as TypeScript source behind a
64
+ `./tooling` subpath, so the command the README documented worked in this monorepo and
65
+ nowhere else — a consumer had no `bin`, and `node` could not execute the `.ts`
66
+ (I18N-01, DEP-02). A test now installs the package into a throwaway app's
67
+ `node_modules` from exactly what `files` lists and runs the binary there, so "works
68
+ when published" is checked rather than asserted.
69
+
70
+ `@chipmobilesdk/rn-i18n` therefore has one entrypoint and no subpaths, and its tarball
71
+ no longer carries `src/tooling`. **Migration:** an app that imported
72
+ `@chipmobilesdk/rn-i18n/tooling` for the programmatic API installs the new package and
73
+ imports `@chipmobilesdk/rn-i18n-tooling` instead; the members are unchanged
74
+ (`runSync`, `scanSource`, `syncLocaleFiles`, `buildReport`, `renderReport`,
75
+ `generateKeysFile`, `renderKeysModule`, `validateSyncConfig`, `listSourceFiles`,
76
+ `globToRegExp`). An app that only ran the CLI replaces its script with `chip-i18n sync`.
77
+
78
+ Three things the move fixed while the code was in hand:
79
+
80
+ - **The scan starts where the glob does.** `listSourceFiles` walked the project root and
81
+ threw away almost everything it read; it now walks each glob's own base directory
82
+ (`src/screens/**/*.tsx` → `src/screens`), one walk per distinct base.
83
+ - **A match offset becomes a line number by binary search** over the file's line
84
+ starts, computed once. Counting newlines per match made a file with many keys
85
+ quadratic in its own length, and the ambiguous-usage snippet re-split the whole file
86
+ for every hit.
87
+ - **A sync with nothing to do writes nothing.** Each locale file and the generated keys
88
+ file are compared with what would be written — with line endings normalised, so a
89
+ Windows checkout's CRLF is not "a change" — and left alone when they match. Rewriting
90
+ identical bytes churned every mtime, which woke watchers and showed work in
91
+ `git status` that had not happened. The report says `Locale files: unchanged, nothing
92
+ written`.
93
+
94
+ `chip-i18n` also takes a command rather than only flags: `sync` is the one command,
95
+ `--help` prints usage, an unknown command exits 2, and a bare invocation still means
96
+ `sync` for a script that predates the name.
97
+
98
+ - Every package now ships built JavaScript with declaration files, instead of asking the
99
+ app to compile its TypeScript. ([#22](https://github.com/operationd8979/chip-mobile-sdk/pull/22))
100
+
101
+ Each tarball carries `lib/commonjs`, `lib/module` and `lib/typescript` (built with
102
+ `react-native-builder-bob`), and `src/` alongside them. What that changes for a
103
+ consumer:
104
+
105
+ - **Nothing to add to `transformIgnorePatterns`.** `require` resolves to the CommonJS
106
+ build, so an app's Jest loads this package without being told to transform it. That
107
+ was the one thing every consuming app had to know about us (DEP-04).
108
+ - **Typechecking reads our `.d.ts`** rather than compiling our source under our
109
+ `tsconfig`. A consumer's stricter — or looser — settings no longer meet our code.
110
+ - **`import` resolves to ESM**, so a bundler can tree-shake what it does not use.
111
+
112
+ No exported name changed, and no subpath moved: `.`, `./testing`, `./push`,
113
+ `./advanced`, `./app-auth`, `./keychain` and rn-core's twelve subpaths resolve exactly
114
+ as before, each now with its own declarations for both module systems. publint and
115
+ `attw --profile node16` pass on all twelve packages, which they could not before.
116
+
117
+ Inside the monorepo nothing changed either, and that is deliberate: every subpath also
118
+ declares a private `chipmobilesdk-source` condition pointing at its TypeScript, and
119
+ Metro, Jest and `tsc` here ask for that condition first. The demo app bundles 371
120
+ modules straight from `packages/*/src`, so editing a package still changes the app with
121
+ no build step, and coverage is still measured on the source. The condition is named for
122
+ this organization rather than using the conventional `source` because third-party
123
+ packages declare that one too — `@dr.pogodin/react-native-fs` does — and asking for it
124
+ pulled their sources into our stricter compile.
125
+
126
+ `npm run build:packages` builds everything, `prepack` rebuilds before any `npm pack`,
127
+ and `lib/` is git-ignored: the published bytes are produced by the release, never
128
+ committed. `npm run check:manifests` now fails a package whose `main`, `module`,
129
+ `types`, `source` condition, `files`, build script or `prepack` do not match this shape.
130
+
131
+ ### Patch Changes
132
+
133
+ - Each package's public API and its weight are now recorded in the repository, so a change
134
+ to either is reviewed rather than discovered. ([#23](https://github.com/operationd8979/chip-mobile-sdk/pull/23))
135
+
136
+ `packages/<pkg>/etc/*.api.md` is an api-extractor report per entrypoint — twenty-seven
137
+ of them, generated from the declarations the package publishes and committed. A renamed
138
+ type, a widened union, one more optional field: all of it shows up in a pull request's
139
+ diff. `npm run check:api` fails when a report is out of date and `npm run api:update`
140
+ rewrites them.
141
+
142
+ A report that moves now needs a **minor or major** changeset for that package.
143
+ `check:changesets` enforces it, because the report is generated from what the package
144
+ publishes, and a patch release that moves the public surface is how a consumer's build
145
+ breaks on `npm update`. A report added for the first time is a baseline, not a change.
146
+ `./testing` deliberately has no report: test doubles are not a surface a release is
147
+ gated on.
148
+
149
+ `npm run check:size` holds every entrypoint to a budget in `.size-limit.json`, measured
150
+ on the ESM build with peers and rn-core treated as external — they are what an app
151
+ installs beside us, not our weight. Each budget started at today's measurement plus five
152
+ per cent, so growth becomes a decision someone makes instead of a drift nobody sees. The
153
+ current figures run from 500 B (`rn-core/lazy`) to 19 kB (rn-notification).
154
+
155
+ No behaviour or API changed in this release; the entry is here so the changelog records
156
+ when the reports and the budgets began.
157
+ - Updated dependencies ([#22](https://github.com/operationd8979/chip-mobile-sdk/pull/22), [#23](https://github.com/operationd8979/chip-mobile-sdk/pull/23)):
158
+ - @chipmobilesdk/rn-core@0.2.0
159
+
160
+ ## 0.3.0
161
+
162
+ ### Minor Changes
163
+
164
+ - `registerRuntimeEntries` notifies subscribers, so copy a lazily loaded feature registers after its screen mounted appears instead of leaving the key on screen until something else re-rendered. Entries registered at runtime get the same validation as configured ones: a missing key or a non-string default throws `I18nConfigError` there rather than surfacing at render time. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
165
+
166
+ `init()` runs once however many times it is called — StrictMode mounts an effect twice — and loses to the user: a `setLocale` made while the persisted read was in flight is no longer overwritten by the stored value. Setting the locale that is already active changes nothing and re-renders nothing.
167
+
168
+ `i18n.useDeviceLocale()` forgets the stored choice and detects again, which is the "System language" option a settings screen offers; the storage adapter's `removeItem`, required by the contract and called by nothing, is what it uses.
169
+
170
+ Plural rules cover a compact CLDR subset instead of three languages: English and its neighbours, Korean, Chinese, Thai, Indonesian and Malay (no singular), French and Portuguese (0 is singular), Russian, Ukrainian, Polish, Czech and Slovak (`few`/`many`), and Arabic (every category). A language outside the table still falls back to "1 is singular", but now reports a `plural_rule_default` diagnostic once, rather than shipping wrong plurals in silence.
171
+
172
+ `formatDate` **merges** its options over the locale's default date format, as `formatNumber` always has: `{ month: 'long' }` keeps the year and the day. Pass `formatDate(value, options, { replaceDefaults: true })` for the old behaviour of replacing them. `formatCurrency` deliberately does not apply the locale's default number options, because a currency's own minor units must win.
173
+
174
+ The generated keys file escapes a key holding an apostrophe or a backslash, which used to produce a file that did not parse and failed `tsc` for the whole app.
175
+
3
176
  ## 0.2.0
4
177
 
5
178
  ### Minor Changes
package/README.md CHANGED
@@ -15,24 +15,27 @@ formatting, and a Node CLI that extracts and synchronizes translation keys.
15
15
  injected by the consuming app.
16
16
  - **Public package.** Published under the `@chipmobilesdk` npm organization.
17
17
  - **Version**: see [CHANGELOG](CHANGELOG.md)
18
- - **Entrypoints**: `@chipmobilesdk/rn-i18n` and the Node-only
19
- `@chipmobilesdk/rn-i18n/tooling`
18
+ - **Entrypoints**: `@chipmobilesdk/rn-i18n`, one entrypoint and no subpaths. The
19
+ Node-only extraction and synchronization tooling is a separate package,
20
+ [`@chipmobilesdk/rn-i18n-tooling`](../rn-i18n-tooling/README.md)
20
21
 
21
22
  ## Installation
22
23
 
23
- This package ships TypeScript sources and is consumed through the workspace or
24
- the public npm registry:
24
+ The tarball carries built JavaScript (CommonJS and ESM) with declaration files, and
25
+ the TypeScript it was built from. An app resolves the build, so there is nothing to add
26
+ to `transformIgnorePatterns` and typechecking reads the shipped `.d.ts`:
25
27
 
26
28
  ```bash
27
29
  npm install @chipmobilesdk/rn-i18n
28
30
  ```
29
31
 
30
- Host frameworks are peer dependencies:
32
+ React is the one peer dependency. There is no `react-native` peer: the formatters
33
+ run on `Intl` and persistence is an injected port, so nothing here reaches for a
34
+ React Native API — the extraction CLI runs on plain Node for the same reason.
31
35
 
32
36
  ```jsonc
33
37
  // peerDependencies
34
- "react": ">=19",
35
- "react-native": ">=0.85"
38
+ "react": ">=19"
36
39
  ```
37
40
 
38
41
  ## Quick start
@@ -115,8 +118,9 @@ t('cart.items', { count }, { count }); // plural variant by count
115
118
 
116
119
  Public contract types also include the configuration, locale, translation,
117
120
  formatting, unit, provider/adapter, and hook result types exported by
118
- `src/index.ts`. Do not deep-import package internals. Node-only extraction and
119
- synchronization APIs must be imported from `@chipmobilesdk/rn-i18n/tooling`.
121
+ `src/index.ts`. Do not deep-import package internals. The Node-only extraction and
122
+ synchronization APIs are in `@chipmobilesdk/rn-i18n-tooling`, a devDependency of the
123
+ app rather than a subpath of this package.
120
124
 
121
125
  ### Storage adapter (required for persistence)
122
126
 
@@ -174,8 +178,10 @@ supported Hermes environments. Built-ins cover `en` (one/other), `vi`/`ja`
174
178
 
175
179
  ## Extraction & synchronization CLI
176
180
 
177
- Node-only, exposed via `@chipmobilesdk/rn-i18n/tooling` and a CLI. Add an app
178
- config (`i18n-sync.config.json`) and a script:
181
+ The CLI is its own package — `npm i -D @chipmobilesdk/rn-i18n-tooling` — which builds
182
+ to JavaScript and exposes a `chip-i18n` binary. It used to ship inside this tarball as
183
+ TypeScript under `./tooling`, where the documented command could not run outside this
184
+ monorepo (I18N-01, DEP-02). Add an app config (`i18n-sync.config.json`) and a script:
179
185
 
180
186
  ```jsonc
181
187
  {
@@ -192,15 +198,20 @@ config (`i18n-sync.config.json`) and a script:
192
198
  ```jsonc
193
199
  // package.json
194
200
  "scripts": {
195
- "i18n:sync": "node --experimental-strip-types node_modules/@chipmobilesdk/rn-i18n/src/tooling/cli.ts --config i18n-sync.config.json"
201
+ "i18n:sync": "chip-i18n sync"
196
202
  }
197
203
  ```
198
204
 
199
- Running it scans the source tree, then per locale: creates missing files, adds
200
- missing keys (with the placeholder policy), preserves existing translations,
201
- removes stale keys, protects declared runtime-only keys, regenerates the typed
202
- key contract, and prints a report. Exit codes: `0` success/warnings, `1`
203
- blocking findings (conflicts, malformed locale files), `2` invalid config.
205
+ Or without a script: `npx chip-i18n sync`. `--config` names another file; it defaults
206
+ to `i18n-sync.config.json`, resolved from the working directory.
207
+
208
+ Running it scans the source tree — starting at each glob's own base directory, not the
209
+ project root — then per locale: creates missing files, adds missing keys (with the
210
+ placeholder policy), preserves existing translations, removes stale keys, protects
211
+ declared runtime-only keys, regenerates the typed key contract, and prints a report. A
212
+ run that finds nothing to change writes no file and says so, so it is safe in a watch
213
+ loop or a pre-commit hook. Exit codes: `0` success/warnings, `1` blocking findings
214
+ (conflicts, malformed locale files), `2` invalid config or usage.
204
215
 
205
216
  Bind `t()` to the generated `AppI18nKey` so `tsc --noEmit` reports invalid or
206
217
  stale key usage before release.
@@ -231,6 +242,14 @@ createI18n({
231
242
 
232
243
  ## Formatting
233
244
 
245
+ Options **merge** over the active locale's defaults, for every formatter:
246
+ `formatDate(value, { month: 'long' })` keeps the locale's year and day, exactly as
247
+ `formatNumber(value, { maximumFractionDigits: 0 })` keeps the rest of the locale's number
248
+ options. Pass `formatDate(value, options, { replaceDefaults: true })` to use only what you
249
+ give it. `formatCurrency` deliberately does not take the locale's default number options:
250
+ a currency carries its own minor units (yen has none), and generic number defaults would
251
+ override them.
252
+
234
253
  `formatNumber`, `formatCurrency`, `formatDate` and `formatUnit` reuse one cached
235
254
  `Intl` formatter per locale and options. A value or option they cannot format (a
236
255
  `NaN`, an unknown currency code) is handled by the `invalidFormat` policy: the
@@ -238,6 +257,18 @@ placeholder text, or a thrown error in `error` mode, and an `invalid_format`
238
257
  diagnostic either way. It never throws from inside `Intl` during a render. A
239
258
  locale's `defaultCurrency` must be a three-letter ISO 4217 code, checked at startup.
240
259
 
260
+ ## Switching, and following the device
261
+
262
+ ```ts
263
+ await i18n.setLocale('vi-VN'); // the user chose a language
264
+ await i18n.useDeviceLocale(); // "System language": forget the choice, detect again
265
+ ```
266
+
267
+ `init()` resolves the locale once — persisted, then device, then the primary fallback —
268
+ and is safe to call again (it returns the same promise). A `setLocale` during that read
269
+ wins: the stored value never overwrites a choice the user has just made. Setting the
270
+ locale already active changes nothing and re-renders nothing.
271
+
241
272
  ## Diagnostics
242
273
 
243
274
  Missing translations and variables, fallbacks, invalid formats, persistence
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.runtimeEntriesFromCatalog = runtimeEntriesFromCatalog;
7
+ /**
8
+ * Runtime entries for a package's message catalog (I18N-13), so an app registers
9
+ * an SDK's copy in one line and translates it like its own:
10
+ *
11
+ * ```ts
12
+ * createI18n({ ...config, runtimeEntries: runtimeEntriesFromCatalog(paymentMessages.user) });
13
+ * ```
14
+ *
15
+ * Catalogs mark variables `{name}`; rn-i18n's are `{{name}}`, so they are
16
+ * converted. `policy: 'add'` lets the sync CLI write missing keys into each locale
17
+ * file with the default text.
18
+ */
19
+ function runtimeEntriesFromCatalog(catalog, options = {}) {
20
+ return Object.entries(catalog).map(([key, text]) => ({
21
+ key,
22
+ defaultText: text.replace(/(?<!\{)\{(\w+)\}(?!\})/g, '{{$1}}'),
23
+ policy: options.policy ?? 'report',
24
+ ...(options.description === undefined ? {} : {
25
+ description: options.description
26
+ })
27
+ }));
28
+ }
29
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["runtimeEntriesFromCatalog","catalog","options","Object","entries","map","key","text","defaultText","replace","policy","description","undefined"],"sourceRoot":"../../../src","sources":["config/catalog.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASA,yBAAyBA,CACxCC,OAAyC,EACzCC,OAA8E,GAAG,CAAC,CAAC,EACvD;EAC5B,OAAOC,MAAM,CAACC,OAAO,CAACH,OAAO,CAAC,CAACI,GAAG,CAAC,CAAC,CAACC,GAAG,EAAEC,IAAI,CAAC,MAAM;IACpDD,GAAG;IACHE,WAAW,EAAED,IAAI,CAACE,OAAO,CAAC,yBAAyB,EAAE,QAAQ,CAAC;IAC9DC,MAAM,EAAER,OAAO,CAACQ,MAAM,IAAI,QAAQ;IAClC,IAAIR,OAAO,CAACS,WAAW,KAAKC,SAAS,GAAG,CAAC,CAAC,GAAG;MAAED,WAAW,EAAET,OAAO,CAACS;IAAY,CAAC;EAClF,CAAC,CAAC,CAAC;AACJ","ignoreList":[]}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createReporter = createReporter;
7
+ exports.defaultReporter = void 0;
8
+ exports.describeError = describeError;
9
+ var _rnCore = require("@chipmobilesdk/rn-core");
10
+ /** What rn-i18n reports. Stable: an app may branch on these. */
11
+
12
+ const SEVERITY = {
13
+ missing_translation: 'warn',
14
+ fallback_used: 'info',
15
+ missing_variable: 'warn',
16
+ invalid_value: 'warn',
17
+ invalid_format: 'warn',
18
+ locale_unsupported: 'warn',
19
+ plural_rule_default: 'warn',
20
+ locale_read_failed: 'warn',
21
+ locale_write_failed: 'warn',
22
+ listener_threw: 'error'
23
+ };
24
+ const channel = (0, _rnCore.createDiagnosticChannel)('rn-i18n');
25
+
26
+ /**
27
+ * One place every diagnostic goes through (I18N-11): the app's SDK sinks always
28
+ * (`installSdkSink` from rn-core), then `onDiagnostic` when the app gave one,
29
+ * otherwise `console.warn` in development builds and tests only.
30
+ */
31
+ function createReporter(onDiagnostic) {
32
+ return diagnostic => {
33
+ channel.emit(diagnostic.code, SEVERITY[diagnostic.code], {
34
+ ...(diagnostic.key === undefined ? {} : {
35
+ key: diagnostic.key
36
+ }),
37
+ ...(diagnostic.locale === undefined ? {} : {
38
+ locale: diagnostic.locale
39
+ })
40
+ });
41
+ if (onDiagnostic) {
42
+ onDiagnostic(diagnostic);
43
+ } else if (typeof __DEV__ === 'undefined' || __DEV__) {
44
+ console.warn(`[rn-i18n] ${diagnostic.message}`);
45
+ }
46
+ };
47
+ }
48
+
49
+ /** For the few call sites that run before or outside an i18n instance. */
50
+ const defaultReporter = exports.defaultReporter = createReporter();
51
+
52
+ /** Foreign error text, shaped before it reaches a message. */
53
+ function describeError(error) {
54
+ return (0, _rnCore.redactPatterns)(error instanceof Error ? error.message : String(error));
55
+ }
56
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_rnCore","require","SEVERITY","missing_translation","fallback_used","missing_variable","invalid_value","invalid_format","locale_unsupported","plural_rule_default","locale_read_failed","locale_write_failed","listener_threw","channel","createDiagnosticChannel","createReporter","onDiagnostic","diagnostic","emit","code","key","undefined","locale","__DEV__","console","warn","message","defaultReporter","exports","describeError","error","redactPatterns","Error","String"],"sourceRoot":"../../../src","sources":["config/diagnostics.ts"],"mappings":";;;;;;;;AAAA,IAAAA,OAAA,GAAAC,OAAA;AAMA;;AAuBA,MAAMC,QAAwD,GAAG;EAChEC,mBAAmB,EAAE,MAAM;EAC3BC,aAAa,EAAE,MAAM;EACrBC,gBAAgB,EAAE,MAAM;EACxBC,aAAa,EAAE,MAAM;EACrBC,cAAc,EAAE,MAAM;EACtBC,kBAAkB,EAAE,MAAM;EAC1BC,mBAAmB,EAAE,MAAM;EAC3BC,kBAAkB,EAAE,MAAM;EAC1BC,mBAAmB,EAAE,MAAM;EAC3BC,cAAc,EAAE;AACjB,CAAC;AAED,MAAMC,OAAO,GAAG,IAAAC,+BAAuB,EAAqB,SAAS,CAAC;;AAEtE;AACA;AACA;AACA;AACA;AACO,SAASC,cAAcA,CAACC,YAAoC,EAAyB;EAC3F,OAAOC,UAAU,IAAI;IACpBJ,OAAO,CAACK,IAAI,CAACD,UAAU,CAACE,IAAI,EAAEjB,QAAQ,CAACe,UAAU,CAACE,IAAI,CAAC,EAAE;MACxD,IAAIF,UAAU,CAACG,GAAG,KAAKC,SAAS,GAAG,CAAC,CAAC,GAAG;QAAED,GAAG,EAAEH,UAAU,CAACG;MAAI,CAAC,CAAC;MAChE,IAAIH,UAAU,CAACK,MAAM,KAAKD,SAAS,GAAG,CAAC,CAAC,GAAG;QAAEC,MAAM,EAAEL,UAAU,CAACK;MAAO,CAAC;IACzE,CAAC,CAAC;IACF,IAAIN,YAAY,EAAE;MACjBA,YAAY,CAACC,UAAU,CAAC;IACzB,CAAC,MAAM,IAAI,OAAOM,OAAO,KAAK,WAAW,IAAIA,OAAO,EAAE;MACrDC,OAAO,CAACC,IAAI,CAAC,aAAaR,UAAU,CAACS,OAAO,EAAE,CAAC;IAChD;EACD,CAAC;AACF;;AAEA;AACO,MAAMC,eAAe,GAAAC,OAAA,CAAAD,eAAA,GAAGZ,cAAc,CAAC,CAAC;;AAE/C;AACO,SAASc,aAAaA,CAACC,KAAc,EAAU;EACrD,OAAO,IAAAC,sBAAc,EAACD,KAAK,YAAYE,KAAK,GAAGF,KAAK,CAACJ,OAAO,GAAGO,MAAM,CAACH,KAAK,CAAC,CAAC;AAC9E","ignoreList":[]}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.UnitConversionError = exports.I18nConfigError = void 0;
7
+ var _rnCore = require("@chipmobilesdk/rn-core");
8
+ /** The package's error codes. */
9
+
10
+ /**
11
+ * Configuration error listing every detected violation. An `SdkError`
12
+ * (`@chipmobilesdk/rn-core`) with `pkg: 'rn-i18n'`.
13
+ */
14
+ class I18nConfigError extends _rnCore.SdkError {
15
+ constructor(issues) {
16
+ super('rn-i18n', 'invalid_config', {
17
+ message: `Invalid i18n configuration:\n- ${issues.join('\n- ')}`
18
+ });
19
+ this.name = 'I18nConfigError';
20
+ this.issues = issues;
21
+ }
22
+ }
23
+
24
+ /** A unit conversion the package cannot perform, such as across dimensions. */
25
+ exports.I18nConfigError = I18nConfigError;
26
+ class UnitConversionError extends _rnCore.SdkError {
27
+ constructor(message) {
28
+ super('rn-i18n', 'unit_conversion', {
29
+ message
30
+ });
31
+ this.name = 'UnitConversionError';
32
+ }
33
+ }
34
+ exports.UnitConversionError = UnitConversionError;
35
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_rnCore","require","I18nConfigError","SdkError","constructor","issues","message","join","name","exports","UnitConversionError"],"sourceRoot":"../../../src","sources":["config/errors.ts"],"mappings":";;;;;;AAAA,IAAAA,OAAA,GAAAC,OAAA;AAEA;;AAGA;AACA;AACA;AACA;AACO,MAAMC,eAAe,SAASC,gBAAQ,CAAgB;EAG5DC,WAAWA,CAACC,MAAyB,EAAE;IACtC,KAAK,CAAC,SAAS,EAAE,gBAAgB,EAAE;MAClCC,OAAO,EAAE,kCAAkCD,MAAM,CAACE,IAAI,CAAC,MAAM,CAAC;IAC/D,CAAC,CAAC;IACF,IAAI,CAACC,IAAI,GAAG,iBAAiB;IAC7B,IAAI,CAACH,MAAM,GAAGA,MAAM;EACrB;AACD;;AAEA;AAAAI,OAAA,CAAAP,eAAA,GAAAA,eAAA;AACO,MAAMQ,mBAAmB,SAASP,gBAAQ,CAAgB;EAChEC,WAAWA,CAACE,OAAe,EAAE;IAC5B,KAAK,CAAC,SAAS,EAAE,iBAAiB,EAAE;MAAEA;IAAQ,CAAC,CAAC;IAChD,IAAI,CAACE,IAAI,GAAG,qBAAqB;EAClC;AACD;AAACC,OAAA,CAAAC,mBAAA,GAAAA,mBAAA","ignoreList":[]}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "I18nConfigError", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _errors.I18nConfigError;
10
+ }
11
+ });
12
+ var _errors = require("./errors.js");
13
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_errors","require"],"sourceRoot":"../../../src","sources":["config/types.ts"],"mappings":";;;;;;;;;;;AAqJA,IAAAA,OAAA,GAAAC,OAAA","ignoreList":[]}
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.validateConfig = validateConfig;
7
+ var _types = require("./types.js");
8
+ /**
9
+ * Validates an I18nConfig before any localized content is served (FR-003,
10
+ * FR-046). Collects every violation and throws a single I18nConfigError so
11
+ * apps see the complete picture instead of fixing issues one at a time.
12
+ */
13
+ function validateConfig(config, knownUnits) {
14
+ const issues = [];
15
+ const locales = Array.isArray(config.locales) ? config.locales : [];
16
+ if (locales.length === 0) {
17
+ issues.push('At least one supported locale must be declared.');
18
+ }
19
+ const codes = new Set();
20
+ for (const locale of locales) {
21
+ if (codes.has(locale.code)) {
22
+ issues.push(`Duplicate locale code "${locale.code}".`);
23
+ }
24
+ codes.add(locale.code);
25
+ }
26
+ for (const locale of locales) {
27
+ validateLocale(locale, codes, knownUnits, issues);
28
+ }
29
+ detectFallbackCycles(locales, codes, issues);
30
+ if (locales.length > 0 && !codes.has(config.primaryLocale)) {
31
+ issues.push(`Primary locale "${config.primaryLocale}" is not a supported locale.`);
32
+ }
33
+ for (const code of codes) {
34
+ const resource = config.resources?.[code];
35
+ if (resource === undefined || resource === null) {
36
+ issues.push(`Locale "${code}" has no translation resource.`);
37
+ }
38
+ }
39
+ if (config.persistLocale !== false && !config.storage) {
40
+ issues.push('Locale persistence is enabled but no storage adapter was injected. ' + 'Provide `storage` or set `persistLocale: false`.');
41
+ }
42
+ const runtimeKeys = new Set();
43
+ for (const entry of config.runtimeEntries ?? []) {
44
+ if (!entry.key) {
45
+ issues.push('Runtime-only entries must declare a non-empty key.');
46
+ continue;
47
+ }
48
+ if (runtimeKeys.has(entry.key)) {
49
+ issues.push(`Duplicate runtime-only entry "${entry.key}".`);
50
+ }
51
+ runtimeKeys.add(entry.key);
52
+ if (typeof entry.defaultText !== 'string') {
53
+ issues.push(`Runtime-only entry "${entry.key}" must declare default text.`);
54
+ }
55
+ }
56
+ if (issues.length > 0) {
57
+ throw new _types.I18nConfigError(issues);
58
+ }
59
+ }
60
+ function validateLocale(locale, codes, knownUnits, issues) {
61
+ const code = locale.code;
62
+ if (!code) {
63
+ issues.push('Every locale must declare a non-empty code.');
64
+ return;
65
+ }
66
+ if (!locale.displayName) {
67
+ issues.push(`Locale "${code}" must declare a display name.`);
68
+ }
69
+ if (locale.direction !== 'ltr' && locale.direction !== 'rtl') {
70
+ issues.push(`Locale "${code}" must declare direction "ltr" or "rtl".`);
71
+ }
72
+ if (!locale.defaultCurrency) {
73
+ issues.push(`Locale "${code}" must declare a default currency.`);
74
+ } else if (!/^[A-Z]{3}$/.test(locale.defaultCurrency)) {
75
+ // An ISO 4217 code; anything else would fail every currency format (I18N-08).
76
+ issues.push(`Locale "${code}" default currency "${locale.defaultCurrency}" is not a three-letter ISO 4217 code.`);
77
+ }
78
+ if (!knownUnits.has(locale.defaultUnit)) {
79
+ issues.push(`Locale "${code}" declares unknown default unit "${locale.defaultUnit}".`);
80
+ }
81
+ if (typeof locale.defaultDateFormat !== 'object' || !locale.defaultDateFormat) {
82
+ issues.push(`Locale "${code}" must declare a default date format.`);
83
+ }
84
+ if (typeof locale.defaultNumberFormat !== 'object' || !locale.defaultNumberFormat) {
85
+ issues.push(`Locale "${code}" must declare a default number format.`);
86
+ }
87
+ if (!Array.isArray(locale.fallbackChain)) {
88
+ issues.push(`Locale "${code}" must declare a fallback chain array.`);
89
+ return;
90
+ }
91
+ const seen = new Set();
92
+ for (const entry of locale.fallbackChain) {
93
+ if (entry === code) {
94
+ issues.push(`Locale "${code}" must not include itself in its fallback chain.`);
95
+ }
96
+ if (seen.has(entry)) {
97
+ issues.push(`Locale "${code}" lists duplicate fallback-chain entry "${entry}".`);
98
+ }
99
+ seen.add(entry);
100
+ if (!codes.has(entry)) {
101
+ issues.push(`Locale "${code}" references unsupported fallback locale "${entry}".`);
102
+ }
103
+ }
104
+ }
105
+ function detectFallbackCycles(locales, codes, issues) {
106
+ const chains = new Map(locales.map(locale => [locale.code, (locale.fallbackChain ?? []).filter(entry => codes.has(entry))]));
107
+ const states = new Map();
108
+ const visit = (code, path) => {
109
+ const state = states.get(code);
110
+ if (state === 'done') {
111
+ return false;
112
+ }
113
+ if (state === 'visiting') {
114
+ issues.push(`Fallback cycle detected: ${[...path, code].join(' -> ')}.`);
115
+ return true;
116
+ }
117
+ states.set(code, 'visiting');
118
+ for (const next of chains.get(code) ?? []) {
119
+ if (visit(next, [...path, code])) {
120
+ states.set(code, 'done');
121
+ return true;
122
+ }
123
+ }
124
+ states.set(code, 'done');
125
+ return false;
126
+ };
127
+ for (const locale of locales) {
128
+ if (visit(locale.code, [])) {
129
+ // Report the first cycle only; one clear cycle message is actionable.
130
+ return;
131
+ }
132
+ }
133
+ }
134
+ //# sourceMappingURL=validateConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_types","require","validateConfig","config","knownUnits","issues","locales","Array","isArray","length","push","codes","Set","locale","has","code","add","validateLocale","detectFallbackCycles","primaryLocale","resource","resources","undefined","persistLocale","storage","runtimeKeys","entry","runtimeEntries","key","defaultText","I18nConfigError","displayName","direction","defaultCurrency","test","defaultUnit","defaultDateFormat","defaultNumberFormat","fallbackChain","seen","chains","Map","map","filter","states","visit","path","state","get","join","set","next"],"sourceRoot":"../../../src","sources":["config/validateConfig.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.formatCurrencyValue = formatCurrencyValue;
7
+ var _rnCore = require("@chipmobilesdk/rn-core");
8
+ var _invalid = require("./invalid.js");
9
+ /**
10
+ * Locale-aware currency formatting. The locale's configured default currency
11
+ * applies unless the caller passes an explicit currency override (FR-021).
12
+ *
13
+ * `locale.defaultNumberFormat` is deliberately **not** applied here: a currency
14
+ * carries its own minor units (yen has none, dinar has three), and the locale's
15
+ * generic number options would override them — ¥1,234.56 instead of ¥1,235.
16
+ * Pass the options you want for a currency explicitly (I18N-09).
17
+ */
18
+ function formatCurrencyValue(value, localeCode, defaultCurrency, currencyOverride, options, invalidFormat) {
19
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
20
+ return (0, _invalid.applyInvalidFormatPolicy)(invalidFormat, `Cannot format invalid currency value "${String(value)}" for locale "${localeCode}".`);
21
+ }
22
+ const currency = currencyOverride ?? defaultCurrency;
23
+ // rn-core's cached formatter (I18N-03). An unknown currency code (`usd1`) used
24
+ // to throw out of render; it is a format problem now (I18N-08).
25
+ try {
26
+ return (0, _rnCore.getNumberFormat)(localeCode, {
27
+ ...options,
28
+ style: 'currency',
29
+ currency
30
+ }).format(value);
31
+ } catch (error) {
32
+ return (0, _invalid.applyInvalidFormatPolicy)(invalidFormat, `Cannot format currency "${currency}" for locale "${localeCode}": ${String(error)}`);
33
+ }
34
+ }
35
+ //# sourceMappingURL=currency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_rnCore","require","_invalid","formatCurrencyValue","value","localeCode","defaultCurrency","currencyOverride","options","invalidFormat","Number","isFinite","applyInvalidFormatPolicy","String","currency","getNumberFormat","style","format","error"],"sourceRoot":"../../../src","sources":["format/currency.ts"],"mappings":";;;;;;AAAA,IAAAA,OAAA,GAAAC,OAAA;AAEA,IAAAC,QAAA,GAAAD,OAAA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASE,mBAAmBA,CAClCC,KAAa,EACbC,UAAkB,EAClBC,eAAuB,EACvBC,gBAAoC,EACpCC,OAA6C,EAC7CC,aAAkC,EACzB;EACT,IAAI,OAAOL,KAAK,KAAK,QAAQ,IAAI,CAACM,MAAM,CAACC,QAAQ,CAACP,KAAK,CAAC,EAAE;IACzD,OAAO,IAAAQ,iCAAwB,EAC9BH,aAAa,EACb,yCAAyCI,MAAM,CAACT,KAAK,CAAC,iBAAiBC,UAAU,IAClF,CAAC;EACF;EACA,MAAMS,QAAQ,GAAGP,gBAAgB,IAAID,eAAe;EACpD;EACA;EACA,IAAI;IACH,OAAO,IAAAS,uBAAe,EAACV,UAAU,EAAE;MAAE,GAAGG,OAAO;MAAEQ,KAAK,EAAE,UAAU;MAAEF;IAAS,CAAC,CAAC,CAACG,MAAM,CAACb,KAAK,CAAC;EAC9F,CAAC,CAAC,OAAOc,KAAK,EAAE;IACf,OAAO,IAAAN,iCAAwB,EAC9BH,aAAa,EACb,2BAA2BK,QAAQ,iBAAiBT,UAAU,MAAMQ,MAAM,CAACK,KAAK,CAAC,EAClF,CAAC;EACF;AACD","ignoreList":[]}