@shopify/ui-extensions-server-kit 0.0.0-experimental-20260902222805

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 (202) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +75 -0
  3. package/dist/ExtensionServerClient/ExtensionServerClient.d.ts +27 -0
  4. package/dist/ExtensionServerClient/ExtensionServerClient.test.d.ts +9 -0
  5. package/dist/ExtensionServerClient/index.d.ts +2 -0
  6. package/dist/ExtensionServerClient/server-types.d.ts +66 -0
  7. package/dist/ExtensionServerClient/types.d.ts +124 -0
  8. package/dist/_virtual/jsx-runtime.cjs.js +1 -0
  9. package/dist/_virtual/jsx-runtime.cjs2.js +1 -0
  10. package/dist/_virtual/jsx-runtime.es.js +5 -0
  11. package/dist/_virtual/jsx-runtime.es2.js +4 -0
  12. package/dist/_virtual/react-jsx-runtime.development.cjs.js +1 -0
  13. package/dist/_virtual/react-jsx-runtime.development.es.js +4 -0
  14. package/dist/_virtual/react-jsx-runtime.production.min.cjs.js +1 -0
  15. package/dist/_virtual/react-jsx-runtime.production.min.es.js +4 -0
  16. package/dist/context/ExtensionServerProvider.d.ts +3 -0
  17. package/dist/context/ExtensionServerProvider.test.d.ts +1 -0
  18. package/dist/context/constants.d.ts +2 -0
  19. package/dist/context/index.d.ts +3 -0
  20. package/dist/context/types.d.ts +12 -0
  21. package/dist/hooks/index.d.ts +3 -0
  22. package/dist/hooks/useExtensionServerContext.d.ts +1 -0
  23. package/dist/hooks/useExtensionServerState.d.ts +1 -0
  24. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
  25. package/dist/i18n.d.ts +68 -0
  26. package/dist/i18n.test.d.ts +1 -0
  27. package/dist/index.cjs.js +1 -0
  28. package/dist/index.cjs2.js +1 -0
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.es.js +46 -0
  31. package/dist/index.es2.js +8 -0
  32. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.cjs.js +22 -0
  33. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.es.js +604 -0
  34. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.cjs.js +9 -0
  35. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.es.js +28 -0
  36. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.cjs.js +1 -0
  37. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.es.js +10 -0
  38. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.cjs.js +1 -0
  39. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.es.js +136 -0
  40. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/types.cjs.js +1 -0
  41. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/types.es.js +4 -0
  42. package/dist/packages/ui-extensions-server-kit/src/context/ExtensionServerProvider.cjs.js +1 -0
  43. package/dist/packages/ui-extensions-server-kit/src/context/ExtensionServerProvider.es.js +30 -0
  44. package/dist/packages/ui-extensions-server-kit/src/context/constants.cjs.js +1 -0
  45. package/dist/packages/ui-extensions-server-kit/src/context/constants.es.js +13 -0
  46. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerContext.cjs.js +1 -0
  47. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerContext.es.js +7 -0
  48. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerState.cjs.js +1 -0
  49. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerState.es.js +9 -0
  50. package/dist/packages/ui-extensions-server-kit/src/hooks/useIsomorphicLayoutEffect.cjs.js +1 -0
  51. package/dist/packages/ui-extensions-server-kit/src/hooks/useIsomorphicLayoutEffect.es.js +5 -0
  52. package/dist/packages/ui-extensions-server-kit/src/i18n.cjs.js +1 -0
  53. package/dist/packages/ui-extensions-server-kit/src/i18n.es.js +60 -0
  54. package/dist/packages/ui-extensions-server-kit/src/state/actions/actions.cjs.js +1 -0
  55. package/dist/packages/ui-extensions-server-kit/src/state/actions/actions.es.js +37 -0
  56. package/dist/packages/ui-extensions-server-kit/src/state/reducers/constants.cjs.js +1 -0
  57. package/dist/packages/ui-extensions-server-kit/src/state/reducers/constants.es.js +7 -0
  58. package/dist/packages/ui-extensions-server-kit/src/state/reducers/extensionServerReducer.cjs.js +1 -0
  59. package/dist/packages/ui-extensions-server-kit/src/state/reducers/extensionServerReducer.es.js +57 -0
  60. package/dist/packages/ui-extensions-server-kit/src/testing/MockExtensionServerProvider.cjs.js +1 -0
  61. package/dist/packages/ui-extensions-server-kit/src/testing/MockExtensionServerProvider.es.js +25 -0
  62. package/dist/packages/ui-extensions-server-kit/src/testing/app.cjs.js +1 -0
  63. package/dist/packages/ui-extensions-server-kit/src/testing/app.es.js +16 -0
  64. package/dist/packages/ui-extensions-server-kit/src/testing/extensions.cjs.js +1 -0
  65. package/dist/packages/ui-extensions-server-kit/src/testing/extensions.es.js +66 -0
  66. package/dist/packages/ui-extensions-server-kit/src/types.cjs.js +1 -0
  67. package/dist/packages/ui-extensions-server-kit/src/types.es.js +4 -0
  68. package/dist/packages/ui-extensions-server-kit/src/utilities/assetToString.cjs.js +1 -0
  69. package/dist/packages/ui-extensions-server-kit/src/utilities/assetToString.es.js +7 -0
  70. package/dist/packages/ui-extensions-server-kit/src/utilities/isUIExtension.cjs.js +1 -0
  71. package/dist/packages/ui-extensions-server-kit/src/utilities/isUIExtension.es.js +7 -0
  72. package/dist/packages/ui-extensions-server-kit/src/utilities/isValidSurface.cjs.js +1 -0
  73. package/dist/packages/ui-extensions-server-kit/src/utilities/isValidSurface.es.js +7 -0
  74. package/dist/packages/ui-extensions-server-kit/src/utilities/noop.cjs.js +1 -0
  75. package/dist/packages/ui-extensions-server-kit/src/utilities/noop.es.js +5 -0
  76. package/dist/packages/ui-extensions-server-kit/src/utilities/replaceUpdated.cjs.js +1 -0
  77. package/dist/packages/ui-extensions-server-kit/src/utilities/replaceUpdated.es.js +14 -0
  78. package/dist/packages/ui-extensions-server-kit/src/utilities/set.cjs.js +1 -0
  79. package/dist/packages/ui-extensions-server-kit/src/utilities/set.es.js +18 -0
  80. package/dist/state/actions/actions.d.ts +6 -0
  81. package/dist/state/actions/index.d.ts +2 -0
  82. package/dist/state/actions/types.d.ts +21 -0
  83. package/dist/state/index.d.ts +2 -0
  84. package/dist/state/reducers/constants.d.ts +2 -0
  85. package/dist/state/reducers/extensionServerReducer.d.ts +3 -0
  86. package/dist/state/reducers/extensionServerReducer.test.d.ts +1 -0
  87. package/dist/state/reducers/index.d.ts +3 -0
  88. package/dist/state/reducers/types.d.ts +6 -0
  89. package/dist/testing/MockExtensionServerProvider.d.ts +8 -0
  90. package/dist/testing/app.d.ts +2 -0
  91. package/dist/testing/extensions.d.ts +6 -0
  92. package/dist/testing/index.d.ts +3 -0
  93. package/dist/types.d.ts +196 -0
  94. package/dist/utilities/assetToString.d.ts +2 -0
  95. package/dist/utilities/assetToString.test.d.ts +1 -0
  96. package/dist/utilities/index.d.ts +6 -0
  97. package/dist/utilities/isUIExtension.d.ts +1 -0
  98. package/dist/utilities/isValidSurface.d.ts +2 -0
  99. package/dist/utilities/noop.d.ts +1 -0
  100. package/dist/utilities/replaceUpdated.d.ts +1 -0
  101. package/dist/utilities/replaceUpdated.test.d.ts +1 -0
  102. package/dist/utilities/set.d.ts +4 -0
  103. package/dist/utilities/set.test.d.ts +1 -0
  104. package/index.d.ts +1 -0
  105. package/index.js +1 -0
  106. package/index.mjs +1 -0
  107. package/node_modules/@testing-library/react/LICENSE +20 -0
  108. package/node_modules/@testing-library/react/README.md +692 -0
  109. package/node_modules/@testing-library/react/dist/@testing-library/react.cjs.js +537 -0
  110. package/node_modules/@testing-library/react/dist/@testing-library/react.esm.js +497 -0
  111. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.cjs.js +501 -0
  112. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.esm.js +461 -0
  113. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.js +502 -0
  114. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.js.map +1 -0
  115. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.min.js +2 -0
  116. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.min.js.map +1 -0
  117. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.js +538 -0
  118. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.js.map +1 -0
  119. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.min.js +2 -0
  120. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.min.js.map +1 -0
  121. package/node_modules/@testing-library/react/dist/act-compat.js +80 -0
  122. package/node_modules/@testing-library/react/dist/config.js +33 -0
  123. package/node_modules/@testing-library/react/dist/fire-event.js +70 -0
  124. package/node_modules/@testing-library/react/dist/index.js +52 -0
  125. package/node_modules/@testing-library/react/dist/pure.js +356 -0
  126. package/node_modules/@testing-library/react/dont-cleanup-after-each.js +1 -0
  127. package/node_modules/@testing-library/react/package.json +116 -0
  128. package/node_modules/@testing-library/react/pure.d.ts +1 -0
  129. package/node_modules/@testing-library/react/pure.js +2 -0
  130. package/node_modules/@testing-library/react/types/index.d.ts +287 -0
  131. package/node_modules/@testing-library/react/types/pure.d.ts +1 -0
  132. package/node_modules/@types/react/LICENSE +21 -0
  133. package/node_modules/@types/react/README.md +15 -0
  134. package/node_modules/@types/react/canary.d.ts +166 -0
  135. package/node_modules/@types/react/experimental.d.ts +127 -0
  136. package/node_modules/@types/react/global.d.ts +160 -0
  137. package/node_modules/@types/react/index.d.ts +4543 -0
  138. package/node_modules/@types/react/jsx-dev-runtime.d.ts +45 -0
  139. package/node_modules/@types/react/jsx-runtime.d.ts +36 -0
  140. package/node_modules/@types/react/package.json +206 -0
  141. package/node_modules/@types/react/ts5.0/canary.d.ts +166 -0
  142. package/node_modules/@types/react/ts5.0/experimental.d.ts +127 -0
  143. package/node_modules/@types/react/ts5.0/global.d.ts +160 -0
  144. package/node_modules/@types/react/ts5.0/index.d.ts +4530 -0
  145. package/node_modules/@types/react/ts5.0/jsx-dev-runtime.d.ts +44 -0
  146. package/node_modules/@types/react/ts5.0/jsx-runtime.d.ts +35 -0
  147. package/node_modules/@vitejs/plugin-react/LICENSE +21 -0
  148. package/node_modules/@vitejs/plugin-react/README.md +159 -0
  149. package/node_modules/@vitejs/plugin-react/dist/index.d.ts +75 -0
  150. package/node_modules/@vitejs/plugin-react/dist/index.js +401 -0
  151. package/node_modules/@vitejs/plugin-react/dist/refresh-runtime.js +663 -0
  152. package/node_modules/@vitejs/plugin-react/package.json +77 -0
  153. package/node_modules/@vitejs/plugin-react/types/preamble.d.ts +1 -0
  154. package/package.json +64 -0
  155. package/project.json +40 -0
  156. package/scripts/create-entry-files.ts +44 -0
  157. package/src/ExtensionServerClient/ExtensionServerClient.test.ts +1238 -0
  158. package/src/ExtensionServerClient/ExtensionServerClient.ts +320 -0
  159. package/src/ExtensionServerClient/index.ts +2 -0
  160. package/src/ExtensionServerClient/server-types.ts +65 -0
  161. package/src/ExtensionServerClient/types.ts +159 -0
  162. package/src/context/ExtensionServerProvider.test.tsx +318 -0
  163. package/src/context/ExtensionServerProvider.tsx +48 -0
  164. package/src/context/constants.ts +16 -0
  165. package/src/context/index.ts +3 -0
  166. package/src/context/types.ts +14 -0
  167. package/src/hooks/index.ts +3 -0
  168. package/src/hooks/useExtensionServerContext.ts +4 -0
  169. package/src/hooks/useExtensionServerState.ts +6 -0
  170. package/src/hooks/useIsomorphicLayoutEffect.ts +6 -0
  171. package/src/i18n.test.ts +417 -0
  172. package/src/i18n.ts +205 -0
  173. package/src/index.ts +7 -0
  174. package/src/state/actions/actions.ts +36 -0
  175. package/src/state/actions/index.ts +2 -0
  176. package/src/state/actions/types.ts +26 -0
  177. package/src/state/index.ts +2 -0
  178. package/src/state/reducers/constants.ts +6 -0
  179. package/src/state/reducers/extensionServerReducer.test.ts +165 -0
  180. package/src/state/reducers/extensionServerReducer.ts +87 -0
  181. package/src/state/reducers/index.ts +3 -0
  182. package/src/state/reducers/types.ts +7 -0
  183. package/src/testing/MockExtensionServerProvider.tsx +36 -0
  184. package/src/testing/app.ts +15 -0
  185. package/src/testing/extensions.ts +71 -0
  186. package/src/testing/index.ts +3 -0
  187. package/src/types.ts +204 -0
  188. package/src/utilities/assetToString.test.ts +16 -0
  189. package/src/utilities/assetToString.ts +7 -0
  190. package/src/utilities/index.ts +6 -0
  191. package/src/utilities/isUIExtension.ts +8 -0
  192. package/src/utilities/isValidSurface.ts +6 -0
  193. package/src/utilities/noop.ts +1 -0
  194. package/src/utilities/replaceUpdated.test.ts +26 -0
  195. package/src/utilities/replaceUpdated.ts +15 -0
  196. package/src/utilities/set.test.ts +19 -0
  197. package/src/utilities/set.ts +35 -0
  198. package/testing.d.ts +1 -0
  199. package/testing.js +1 -0
  200. package/testing.mjs +1 -0
  201. package/tests/setup.ts +3 -0
  202. package/vite.config.mts +57 -0
@@ -0,0 +1,417 @@
1
+ import {flattenDevExtensionTranslations, resolveDevExtensionLocale, getFlattenedLocalization} from './i18n'
2
+
3
+ /**
4
+ * These tests were adapted from https://github.com/Shopify/checkout-web/blob/master/app/utilities/development-mode/tests/i18n-utilities.test.tsx
5
+ * */
6
+
7
+ describe('resolveDevExtensionLocale()', () => {
8
+ test('returns the user locale if it is provided as a supported locale', () => {
9
+ const userLocale = 'fr-FR'
10
+
11
+ const localization = {
12
+ defaultLocale: 'en',
13
+ lastUpdated: Date.now(),
14
+ translations: {
15
+ en: {
16
+ greetings: {
17
+ hello: 'Hello',
18
+ },
19
+ },
20
+ de: {
21
+ greetings: {
22
+ hello: 'Hallo',
23
+ },
24
+ },
25
+ 'fr-FR': {
26
+ greetings: {
27
+ hello: 'Bonjour',
28
+ },
29
+ },
30
+ },
31
+ }
32
+
33
+ const extensionLocale = resolveDevExtensionLocale(localization, {user: userLocale})
34
+
35
+ expect(extensionLocale).toBe(userLocale)
36
+ })
37
+
38
+ test('returns the non-regional user locale if there is no exact match with user locale', async () => {
39
+ const userLocale = 'de-DE'
40
+
41
+ const localization = {
42
+ defaultLocale: 'en',
43
+ lastUpdated: Date.now(),
44
+ translations: {
45
+ en: {
46
+ greetings: {
47
+ hello: 'Hello',
48
+ },
49
+ },
50
+ de: {
51
+ greetings: {
52
+ hello: 'Hallo',
53
+ },
54
+ },
55
+ 'fr-FR': {
56
+ greetings: {
57
+ hello: 'Bonjour',
58
+ },
59
+ },
60
+ },
61
+ }
62
+
63
+ const extensionLocale = resolveDevExtensionLocale(localization, {user: userLocale})
64
+
65
+ expect(extensionLocale).toBe(userLocale.split('-')[0])
66
+ })
67
+
68
+ test("returns the shop locale if it is provided as a supported locale and there isn't a match for the user locale", () => {
69
+ const shopLocale = 'fr-FR'
70
+ const userLocale = 'de-DE'
71
+
72
+ const localization = {
73
+ defaultLocale: 'en',
74
+ lastUpdated: Date.now(),
75
+ translations: {
76
+ en: {
77
+ greetings: {
78
+ hello: 'Hello',
79
+ },
80
+ },
81
+ 'fr-FR': {
82
+ greetings: {
83
+ hello: 'Bonjour',
84
+ },
85
+ },
86
+ },
87
+ }
88
+
89
+ const extensionLocale = resolveDevExtensionLocale(localization, {user: userLocale, shop: shopLocale})
90
+
91
+ expect(extensionLocale).toBe(shopLocale)
92
+ })
93
+
94
+ test("returns the non-regional shop locale if it is provided as a supported locale and there isn't a match for the user locale", async () => {
95
+ const userLocale = 'ja-JP'
96
+ const shopLocale = 'de-DE'
97
+
98
+ const localization = {
99
+ defaultLocale: 'en',
100
+ lastUpdated: Date.now(),
101
+ translations: {
102
+ en: {
103
+ greetings: {
104
+ hello: 'Hello',
105
+ },
106
+ },
107
+ de: {
108
+ greetings: {
109
+ hello: 'Hallo',
110
+ },
111
+ },
112
+ },
113
+ }
114
+
115
+ const extensionLocale = resolveDevExtensionLocale(localization, {user: userLocale, shop: shopLocale})
116
+
117
+ expect(extensionLocale).toBe(shopLocale.split('-')[0])
118
+ })
119
+
120
+ test('returns the default translation locale when no full or partial match can be for neither user nor shop locale made', async () => {
121
+ const userLocale = 'en-US'
122
+ const shopLocale = 'ja'
123
+ const defaultLocale = 'fi'
124
+
125
+ const localization = {
126
+ defaultLocale,
127
+ lastUpdated: Date.now(),
128
+ translations: {
129
+ 'en-CA': {
130
+ greetings: {
131
+ hello: 'Hello',
132
+ },
133
+ },
134
+ de: {
135
+ greetings: {
136
+ hello: 'Hallo',
137
+ },
138
+ },
139
+ fi: {
140
+ greetings: {
141
+ hello: 'Hei',
142
+ },
143
+ },
144
+ },
145
+ }
146
+
147
+ const extensionLocale = resolveDevExtensionLocale(localization, {user: userLocale, shop: shopLocale})
148
+
149
+ expect(extensionLocale).toBe(defaultLocale)
150
+ })
151
+ })
152
+
153
+ describe('flattenDevExtensionTranslations()', () => {
154
+ test('returns flattened translation keys using a single locale', () => {
155
+ const resolvedLocale = 'en'
156
+
157
+ const localization = {
158
+ defaultLocale: 'en',
159
+ lastUpdated: Date.now(),
160
+ translations: {
161
+ en: {
162
+ greetings: {
163
+ hello: 'Hello',
164
+ },
165
+ },
166
+ },
167
+ }
168
+
169
+ const flattenedTranslations = flattenDevExtensionTranslations(localization, resolvedLocale)
170
+
171
+ expect(flattenedTranslations).toStrictEqual({
172
+ 'greetings.hello': 'Hello',
173
+ })
174
+ })
175
+
176
+ test('returns flattened and merged translations with fallbacks for missing keys from the default locale', () => {
177
+ const resolvedLocale = 'en-GB'
178
+
179
+ const localization = {
180
+ defaultLocale: 'en',
181
+ lastUpdated: Date.now(),
182
+ translations: {
183
+ en: {
184
+ greetings: {
185
+ hello: 'Hello',
186
+ goodbye: 'Goodbye',
187
+ },
188
+ },
189
+ 'en-GB': {
190
+ greetings: {
191
+ hello: 'Hello, UK',
192
+ },
193
+ },
194
+ fr: {
195
+ greetings: {
196
+ hello: 'Bonjour',
197
+ },
198
+ },
199
+ },
200
+ }
201
+
202
+ const flattenedTranslations = flattenDevExtensionTranslations(localization, resolvedLocale)
203
+
204
+ expect(flattenedTranslations).toStrictEqual({
205
+ 'greetings.hello': 'Hello, UK',
206
+ 'greetings.goodbye': 'Goodbye',
207
+ })
208
+ })
209
+
210
+ test('returns flattened and merged translations based on exact match for locale translation', () => {
211
+ const resolvedLocale = 'fr-CA'
212
+
213
+ const localization = {
214
+ defaultLocale: 'en',
215
+ lastUpdated: Date.now(),
216
+ translations: {
217
+ en: {
218
+ greetings: {
219
+ hello: 'Hello',
220
+ goodbye: 'Goodbye',
221
+ },
222
+ },
223
+ fr: {
224
+ greetings: {
225
+ hello: 'Bonjour',
226
+ goodbye: 'Au revoir',
227
+ },
228
+ },
229
+ 'fr-CA': {
230
+ greetings: {
231
+ hello: 'Bonjour, Canada',
232
+ goodbye: 'Au revoir, Canada',
233
+ },
234
+ },
235
+ },
236
+ }
237
+
238
+ const flattenedTranslations = flattenDevExtensionTranslations(localization, resolvedLocale)
239
+
240
+ expect(flattenedTranslations).toStrictEqual({
241
+ 'greetings.hello': 'Bonjour, Canada',
242
+ 'greetings.goodbye': 'Au revoir, Canada',
243
+ })
244
+ })
245
+
246
+ test('returns flattened and merged translations based on non-regional match for locale translation', () => {
247
+ const resolvedLocale = 'fr-CA'
248
+
249
+ const localization = {
250
+ defaultLocale: 'en',
251
+ lastUpdated: Date.now(),
252
+ translations: {
253
+ en: {
254
+ greetings: {
255
+ hello: 'Hello',
256
+ goodbye: 'Goodbye',
257
+ sorry: 'Sorry',
258
+ },
259
+ },
260
+ fr: {
261
+ greetings: {
262
+ hello: 'Bonjour',
263
+ goodbye: 'Au revoir',
264
+ sorry: 'Désolé',
265
+ },
266
+ },
267
+ },
268
+ }
269
+
270
+ const flattenedTranslations = flattenDevExtensionTranslations(localization, resolvedLocale)
271
+
272
+ expect(flattenedTranslations).toStrictEqual({
273
+ 'greetings.hello': 'Bonjour',
274
+ 'greetings.goodbye': 'Au revoir',
275
+ 'greetings.sorry': 'Désolé',
276
+ })
277
+ })
278
+
279
+ test('returns the flattened default locale where no locale match is possible (full or non-regional)', () => {
280
+ const resolvedLocale = 'de'
281
+
282
+ const localization = {
283
+ defaultLocale: 'en',
284
+ lastUpdated: Date.now(),
285
+ translations: {
286
+ en: {
287
+ greetings: {
288
+ hello: 'Hello',
289
+ goodbye: 'Goodbye',
290
+ sorry: 'Sorry',
291
+ },
292
+ },
293
+ fr: {
294
+ greetings: {
295
+ hello: 'Bonjour',
296
+ goodbye: 'Au revoir',
297
+ sorry: 'Désolé',
298
+ },
299
+ },
300
+ 'de-DE': {
301
+ greetings: {
302
+ hello: 'Hallo',
303
+ goodbye: 'Auf Wiedersehen',
304
+ sorry: ' Entschuldigung',
305
+ },
306
+ },
307
+ },
308
+ }
309
+
310
+ const flattenedTranslations = flattenDevExtensionTranslations(localization, resolvedLocale)
311
+
312
+ expect(flattenedTranslations).toStrictEqual({
313
+ 'greetings.hello': 'Hello',
314
+ 'greetings.goodbye': 'Goodbye',
315
+ 'greetings.sorry': 'Sorry',
316
+ })
317
+ })
318
+
319
+ test('returns flattened translations with pluralizations', () => {
320
+ const resolvedLocale = 'en'
321
+
322
+ const localization = {
323
+ defaultLocale: 'en',
324
+ lastUpdated: Date.now(),
325
+ translations: {
326
+ en: {
327
+ pluralExample: {
328
+ one: 'You have {{count}} apple',
329
+ other: 'You have {{count}} apples',
330
+ },
331
+ example: {
332
+ of: {
333
+ nestedTranslation: 'This is a nested translation only in the default locale',
334
+ anotherNestedTranslation: 'This is another nested translation defined in the default locale',
335
+ },
336
+ },
337
+ },
338
+ },
339
+ }
340
+
341
+ const flattenedTranslations = flattenDevExtensionTranslations(localization, resolvedLocale)
342
+
343
+ expect(flattenedTranslations).toStrictEqual({
344
+ 'pluralExample.one': 'You have {{count}} apple',
345
+ 'pluralExample.other': 'You have {{count}} apples',
346
+ 'example.of.nestedTranslation': 'This is a nested translation only in the default locale',
347
+ 'example.of.anotherNestedTranslation': 'This is another nested translation defined in the default locale',
348
+ })
349
+ })
350
+ })
351
+
352
+ describe('getFlattenedLocalization()', () => {
353
+ test('returns flattened translations string and resolved extension locale when locales options are provided', () => {
354
+ const userLocale = 'en'
355
+
356
+ const localization = {
357
+ defaultLocale: 'en',
358
+ lastUpdated: Date.now(),
359
+ translations: {
360
+ en: {
361
+ greetings: {
362
+ hello: 'Hello',
363
+ },
364
+ },
365
+ },
366
+ }
367
+
368
+ const flattenedLocalization = getFlattenedLocalization(localization, {user: userLocale})
369
+
370
+ expect(flattenedLocalization).toStrictEqual({
371
+ extensionLocale: userLocale,
372
+ translations: '{"greetings.hello":"Hello"}',
373
+ lastUpdated: localization.lastUpdated,
374
+ })
375
+ })
376
+
377
+ test('return undefined when locales options are not provided', () => {
378
+ const userLocale = 'en'
379
+
380
+ const localization = {
381
+ defaultLocale: 'en',
382
+ lastUpdated: Date.now(),
383
+ translations: {
384
+ en: {
385
+ greetings: {
386
+ hello: 'Hello',
387
+ },
388
+ },
389
+ },
390
+ }
391
+
392
+ const flattenedLocalization = getFlattenedLocalization(localization)
393
+
394
+ expect(flattenedLocalization).toBeUndefined()
395
+ })
396
+
397
+ test('return undefined when localization is null', () => {
398
+ const userLocale = 'en'
399
+ const flattenedLocalization = getFlattenedLocalization(null, {user: userLocale})
400
+
401
+ expect(flattenedLocalization).toBeUndefined()
402
+ })
403
+
404
+ test('return flattened localization if localization is already flattened', () => {
405
+ const userLocale = 'en'
406
+
407
+ const localization = {
408
+ extensionLocale: userLocale,
409
+ lastUpdated: Date.now(),
410
+ translations: '{"greetings.hello":"Hello"}',
411
+ }
412
+
413
+ const flattenedLocalization = getFlattenedLocalization(localization, {user: userLocale})
414
+
415
+ expect(flattenedLocalization).toStrictEqual(localization)
416
+ })
417
+ })
package/src/i18n.ts ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * These translation utilities were adapted from https://github.com/Shopify/checkout-web/blob/master/app/utilities/development-mode/i18n-utilities.tsx
3
+ * */
4
+
5
+ type Translation = string | {[key: string]: Translation}
6
+
7
+ export interface Localization {
8
+ defaultLocale: string
9
+ translations: Record<string, Record<string, Translation>>
10
+ lastUpdated: number
11
+ }
12
+
13
+ export interface FlattenedLocalization {
14
+ extensionLocale: string
15
+ translations: string
16
+ lastUpdated: number
17
+ }
18
+
19
+ export interface LocalesOptions {
20
+ user: string
21
+ shop?: string
22
+ }
23
+
24
+ interface TranslationDictionary {
25
+ [key: string]: string | TranslationDictionary
26
+ }
27
+
28
+ /**
29
+ * This is a flattened dictionary of extension translations for the active locale.
30
+ *
31
+ * Here are some examples for the 'en' locale.
32
+ *
33
+ * Nested keys are dot separated. Consider this translation definition:
34
+ *
35
+ * ```
36
+ * {
37
+ * "greetings": {
38
+ * "hello": "Hello {{user}}"
39
+ * }
40
+ * }
41
+ * ```
42
+ *
43
+ * It would be represented in this flattened dictionary as:
44
+ * ```
45
+ * {
46
+ * "greetings.hello": "Hello {{user}}"
47
+ * }
48
+ * ```
49
+ * Pluralized translations will also be flattened. Consider this definition:
50
+ * ```
51
+ * {
52
+ * "loyaltyPointsRemaining": {
53
+ * "one": "You have {{count}} loyalty point",
54
+ * "other": "You have {{count}} loyalty points",
55
+ * }
56
+ * }
57
+ * ```
58
+ * It would be represented in this flattened dictionary as:
59
+ * ```
60
+ * {
61
+ * "loyaltyPointsRemaining.one": "You have {{count}} loyalty point",
62
+ * "loyaltyPointsRemaining.other": "You have {{count}} loyalty points",
63
+ * }
64
+ * ```
65
+ */
66
+ type ExtensionTranslationMap = Record<string, string>
67
+
68
+ export const TRANSLATED_KEYS = ['localization', 'name', 'description']
69
+ /**
70
+ * From a nested dictionary like the following :
71
+ *
72
+ * ```typescript
73
+ * const dictionary = {
74
+ * Foo: {
75
+ * Bar: {
76
+ * fooBar: 'something'
77
+ * }
78
+ * }
79
+ * }
80
+ *
81
+ * Returns a map containing this pair : {'Foo.Bar.fooBar': 'something'}
82
+ * ```
83
+ */
84
+ function dictionaryToFlatMap(dictionary: TranslationDictionary) {
85
+ const map = new Map<string, string>()
86
+
87
+ traverseDictionary(dictionary, (key, value) => map.set(key, value))
88
+
89
+ return map
90
+ }
91
+
92
+ function traverseDictionary(
93
+ dictionary: TranslationDictionary,
94
+ callback: (key: string, value: string) => void,
95
+ keyPrefix?: string,
96
+ ) {
97
+ Object.keys(dictionary).forEach((key: string) => {
98
+ const value = dictionary[key]
99
+ const translationKey = keyPrefix ? `${keyPrefix}.${key}` : key
100
+
101
+ if (value == null) {
102
+ return
103
+ }
104
+
105
+ if (typeof value === 'string') {
106
+ callback(translationKey, value)
107
+ } else {
108
+ traverseDictionary(value, callback, translationKey)
109
+ }
110
+ })
111
+ }
112
+
113
+ export function flattenDevExtensionTranslations(localization: Localization, locale: string): ExtensionTranslationMap {
114
+ const defaultTranslations = getFlatMap(localization.translations[localization.defaultLocale])
115
+
116
+ const nonRegionalLocale = getNonRegionalLocale(locale)
117
+ const nonRegionalTranslations = getFlatMap(localization.translations[nonRegionalLocale])
118
+
119
+ const userLocaleTranslations = getFlatMap(localization.translations[locale])
120
+
121
+ return convertMapToExtensionTranslationMap(
122
+ new Map([...defaultTranslations, ...nonRegionalTranslations, ...userLocaleTranslations]),
123
+ )
124
+ }
125
+
126
+ function getFlatMap(translationDictionary: TranslationDictionary | undefined) {
127
+ if (translationDictionary) {
128
+ return dictionaryToFlatMap(translationDictionary)
129
+ }
130
+ return new Map<string, string>()
131
+ }
132
+
133
+ export function resolveDevExtensionLocale(
134
+ localization: Localization,
135
+ locales: {
136
+ user: string
137
+ shop?: string
138
+ },
139
+ ): string {
140
+ const extensionLocales = new Set(Object.keys(localization.translations))
141
+
142
+ // 0. If no translations, use the user's locale
143
+ if (extensionLocales.size === 0) return locales.user
144
+
145
+ // 1. Attempt to match the user's locale exactly
146
+ if (extensionLocales.has(locales.user)) return locales.user
147
+
148
+ // 2. Attempt to match the non-regional part of the user's locale (i.e. 'fr-CA' becomes 'fr')
149
+ const nonRegionalUserLocale = getNonRegionalLocale(locales.user)
150
+ if (extensionLocales.has(nonRegionalUserLocale)) return nonRegionalUserLocale
151
+
152
+ // 3. Attempt to match the shop's default locale exactly
153
+ if (locales.shop && extensionLocales.has(locales.shop)) return locales.shop
154
+
155
+ // 4. Attempt to match the non-regional part of the shop's default locale (i.e. 'fr-CA' becomes 'fr')
156
+ const nonRegionalShopLocale = locales.shop && getNonRegionalLocale(locales.shop)
157
+ if (nonRegionalShopLocale && extensionLocales.has(nonRegionalShopLocale)) return nonRegionalShopLocale
158
+
159
+ // 5. Finally, return the default locale as a fallback
160
+ return localization.defaultLocale
161
+ }
162
+
163
+ function convertMapToExtensionTranslationMap(map: Map<string, string>): ExtensionTranslationMap {
164
+ const extTransMap: ExtensionTranslationMap = {}
165
+ for (const [key, value] of map) {
166
+ extTransMap[key] = value
167
+ }
168
+ return extTransMap
169
+ }
170
+
171
+ function getNonRegionalLocale(locale: string): string {
172
+ return locale.split('-')[0]
173
+ }
174
+
175
+ export function getFlattenedLocalization(
176
+ localization?: FlattenedLocalization | Localization | null,
177
+ locales?: LocalesOptions,
178
+ ) {
179
+ if (!localization || !locales) {
180
+ return
181
+ }
182
+
183
+ if (isFlattenedTranslations(localization)) {
184
+ return localization
185
+ }
186
+
187
+ const extensionLocale = resolveDevExtensionLocale(localization, locales)
188
+ const translations = JSON.stringify(flattenDevExtensionTranslations(localization, extensionLocale))
189
+
190
+ const flattenedLocalization: FlattenedLocalization = {
191
+ extensionLocale,
192
+ translations,
193
+ lastUpdated: localization.lastUpdated,
194
+ }
195
+ return flattenedLocalization
196
+ }
197
+
198
+ export function isFlattenedTranslations(
199
+ localization: Localization | FlattenedLocalization,
200
+ ): localization is FlattenedLocalization {
201
+ return (
202
+ typeof localization.translations === 'string' &&
203
+ Object.prototype.hasOwnProperty.call(localization, 'extensionLocale')
204
+ )
205
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './context'
2
+ export * from './ExtensionServerClient'
3
+ export * from './hooks'
4
+ export * from './state'
5
+ export * from './types'
6
+ export * from './utilities'
7
+ export * from './i18n'
@@ -0,0 +1,36 @@
1
+ import type {ConnectedAction, UpdateAction, RefreshAction, FocusAction, UnfocusAction} from './types'
2
+
3
+ export function createConnectedAction(payload: ConnectedAction['payload']): ConnectedAction {
4
+ return {
5
+ type: 'connected',
6
+ payload,
7
+ }
8
+ }
9
+
10
+ export function createUpdateAction(payload: UpdateAction['payload']): UpdateAction {
11
+ return {
12
+ type: 'update',
13
+ payload,
14
+ }
15
+ }
16
+
17
+ export function createRefreshAction(payload: RefreshAction['payload']): RefreshAction {
18
+ return {
19
+ type: 'refresh',
20
+ payload,
21
+ }
22
+ }
23
+
24
+ export function createFocusAction(payload: FocusAction['payload']): FocusAction {
25
+ return {
26
+ type: 'focus',
27
+ payload,
28
+ }
29
+ }
30
+
31
+ export function createUnfocusAction(payload: UnfocusAction['payload']): UnfocusAction {
32
+ return {
33
+ type: 'unfocus',
34
+ payload,
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ export * from './actions'
2
+ export * from './types'
@@ -0,0 +1,26 @@
1
+ export interface ConnectedAction {
2
+ type: 'connected'
3
+ payload: ExtensionServer.InboundEvents['connected']
4
+ }
5
+
6
+ export interface UpdateAction {
7
+ type: 'update'
8
+ payload: ExtensionServer.InboundEvents['update']
9
+ }
10
+
11
+ export interface RefreshAction {
12
+ type: 'refresh'
13
+ payload: ExtensionServer.InboundEvents['refresh']
14
+ }
15
+
16
+ export interface FocusAction {
17
+ type: 'focus'
18
+ payload: ExtensionServer.InboundEvents['focus']
19
+ }
20
+
21
+ export interface UnfocusAction {
22
+ type: 'unfocus'
23
+ payload: ExtensionServer.InboundEvents['unfocus']
24
+ }
25
+
26
+ export type ExtensionServerActions = ConnectedAction | UpdateAction | RefreshAction | FocusAction | UnfocusAction
@@ -0,0 +1,2 @@
1
+ export * from './actions'
2
+ export * from './reducers'
@@ -0,0 +1,6 @@
1
+ import type {ExtensionServerState} from './types'
2
+
3
+ export const INITIAL_STATE: ExtensionServerState = {
4
+ extensions: [],
5
+ store: '',
6
+ }