@prosopo/locale 3.4.3 → 3.6.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 (158) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +15 -13
  2. package/.turbo/turbo-build$colon$tsc.log +8 -5
  3. package/.turbo/turbo-build.log +17 -15
  4. package/CHANGELOG.md +74 -0
  5. package/dist/cjs/i18SharedOptions.cjs +2 -2
  6. package/dist/cjs/i18nBackend.cjs +1 -1
  7. package/dist/cjs/i18nFrontend.cjs +167 -53
  8. package/dist/cjs/i18nMiddleware.cjs +5 -2
  9. package/dist/cjs/index.cjs +5 -4
  10. package/dist/cjs/locales/en/translation.cjs +1 -0
  11. package/dist/cjs/translationKey.cjs +2 -3
  12. package/dist/cjs/translations.cjs +4 -5
  13. package/dist/cjs/translator.cjs +29 -0
  14. package/dist/i18SharedOptions.d.ts +2 -2
  15. package/dist/i18SharedOptions.js +3 -3
  16. package/dist/i18SharedOptions.js.map +1 -1
  17. package/dist/i18nBackend.js +1 -1
  18. package/dist/i18nFrontend.d.ts +2 -2
  19. package/dist/i18nFrontend.d.ts.map +1 -1
  20. package/dist/i18nFrontend.js +167 -47
  21. package/dist/i18nFrontend.js.map +1 -1
  22. package/dist/i18nMiddleware.d.ts.map +1 -1
  23. package/dist/i18nMiddleware.js +3 -1
  24. package/dist/i18nMiddleware.js.map +1 -1
  25. package/dist/index.d.ts +6 -4
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +4 -4
  28. package/dist/index.js.map +1 -1
  29. package/dist/loadI18next.d.ts +2 -3
  30. package/dist/loadI18next.d.ts.map +1 -1
  31. package/dist/loadI18next.js.map +1 -1
  32. package/dist/locales/ar/translation.json +1 -0
  33. package/dist/locales/az/translation.json +1 -0
  34. package/dist/locales/cs/translation.json +1 -0
  35. package/dist/locales/de/translation.json +1 -0
  36. package/dist/locales/el/translation.json +1 -0
  37. package/dist/locales/en/translation.js +1 -0
  38. package/dist/locales/en/translation.json +1 -0
  39. package/dist/locales/es/translation.json +1 -0
  40. package/dist/locales/fi/translation.json +1 -0
  41. package/dist/locales/fr/translation.json +1 -0
  42. package/dist/locales/hi/translation.json +1 -0
  43. package/dist/locales/hu/translation.json +1 -0
  44. package/dist/locales/id/translation.json +1 -0
  45. package/dist/locales/it/translation.json +1 -0
  46. package/dist/locales/ja/translation.json +1 -0
  47. package/dist/locales/jv/translation.json +1 -0
  48. package/dist/locales/ko/translation.json +1 -0
  49. package/dist/locales/ml/translation.json +1 -0
  50. package/dist/locales/ms/translation.json +1 -0
  51. package/dist/locales/nl/translation.json +1 -0
  52. package/dist/locales/no/translation.json +1 -0
  53. package/dist/locales/pl/translation.json +1 -0
  54. package/dist/locales/pt/translation.json +1 -0
  55. package/dist/locales/pt-BR/translation.json +1 -0
  56. package/dist/locales/ro/translation.json +1 -0
  57. package/dist/locales/ru/translation.json +1 -0
  58. package/dist/locales/sr/translation.json +1 -0
  59. package/dist/locales/sv/translation.json +1 -0
  60. package/dist/locales/th/translation.json +1 -0
  61. package/dist/locales/tr/translation.json +1 -0
  62. package/dist/locales/uk/translation.json +1 -0
  63. package/dist/locales/vi/translation.json +1 -0
  64. package/dist/locales/zh-CN/translation.json +1 -0
  65. package/dist/tests/i18nFrontend.unit.test.js +218 -173
  66. package/dist/tests/i18nFrontend.unit.test.js.map +1 -1
  67. package/dist/tests/i18nMiddleware.unit.test.js +11 -2
  68. package/dist/tests/i18nMiddleware.unit.test.js.map +1 -1
  69. package/dist/tests/loadI18next.unit.test.js.map +1 -1
  70. package/dist/tests/locale.test-d.js +27 -20
  71. package/dist/tests/locale.test-d.js.map +1 -1
  72. package/dist/tests/translationKey.unit.test.js +11 -33
  73. package/dist/tests/translationKey.unit.test.js.map +1 -1
  74. package/dist/tests/translations.unit.test.js +12 -22
  75. package/dist/tests/translations.unit.test.js.map +1 -1
  76. package/dist/tests/translator.unit.test.d.ts +2 -0
  77. package/dist/tests/translator.unit.test.d.ts.map +1 -0
  78. package/dist/tests/translator.unit.test.js +134 -0
  79. package/dist/tests/translator.unit.test.js.map +1 -0
  80. package/dist/translationKey.d.ts +2 -3
  81. package/dist/translationKey.d.ts.map +1 -1
  82. package/dist/translationKey.js +2 -3
  83. package/dist/translationKey.js.map +1 -1
  84. package/dist/translations.d.ts +3 -2
  85. package/dist/translations.d.ts.map +1 -1
  86. package/dist/translations.js +3 -3
  87. package/dist/translations.js.map +1 -1
  88. package/dist/translator.d.ts +9 -0
  89. package/dist/translator.d.ts.map +1 -0
  90. package/dist/translator.js +29 -0
  91. package/dist/translator.js.map +1 -0
  92. package/dist/types.d.ts +16 -0
  93. package/dist/types.d.ts.map +1 -0
  94. package/dist/types.js +2 -0
  95. package/dist/types.js.map +1 -0
  96. package/package.json +4 -9
  97. package/src/i18SharedOptions.ts +3 -3
  98. package/src/i18nFrontend.ts +240 -78
  99. package/src/i18nMiddleware.ts +6 -1
  100. package/src/index.ts +11 -4
  101. package/src/loadI18next.ts +8 -9
  102. package/src/locales/ar/translation.json +1 -0
  103. package/src/locales/az/translation.json +1 -0
  104. package/src/locales/cs/translation.json +1 -0
  105. package/src/locales/de/translation.json +1 -0
  106. package/src/locales/el/translation.json +1 -0
  107. package/src/locales/en/translation.json +1 -0
  108. package/src/locales/es/translation.json +1 -0
  109. package/src/locales/fi/translation.json +1 -0
  110. package/src/locales/fr/translation.json +1 -0
  111. package/src/locales/hi/translation.json +1 -0
  112. package/src/locales/hu/translation.json +1 -0
  113. package/src/locales/id/translation.json +1 -0
  114. package/src/locales/it/translation.json +1 -0
  115. package/src/locales/ja/translation.json +1 -0
  116. package/src/locales/jv/translation.json +1 -0
  117. package/src/locales/ko/translation.json +1 -0
  118. package/src/locales/ml/translation.json +1 -0
  119. package/src/locales/ms/translation.json +1 -0
  120. package/src/locales/nl/translation.json +1 -0
  121. package/src/locales/no/translation.json +1 -0
  122. package/src/locales/pl/translation.json +1 -0
  123. package/src/locales/pt/translation.json +1 -0
  124. package/src/locales/pt-BR/translation.json +1 -0
  125. package/src/locales/ro/translation.json +1 -0
  126. package/src/locales/ru/translation.json +1 -0
  127. package/src/locales/sr/translation.json +1 -0
  128. package/src/locales/sv/translation.json +1 -0
  129. package/src/locales/th/translation.json +1 -0
  130. package/src/locales/tr/translation.json +1 -0
  131. package/src/locales/uk/translation.json +1 -0
  132. package/src/locales/vi/translation.json +1 -0
  133. package/src/locales/zh-CN/translation.json +1 -0
  134. package/src/tests/i18nFrontend.unit.test.ts +310 -219
  135. package/src/tests/i18nMiddleware.unit.test.ts +21 -2
  136. package/src/tests/loadI18next.unit.test.ts +11 -11
  137. package/src/tests/locale.test-d.ts +36 -32
  138. package/src/tests/translationKey.unit.test.ts +12 -38
  139. package/src/tests/translations.unit.test.ts +18 -31
  140. package/src/tests/translator.unit.test.ts +213 -0
  141. package/src/translationKey.ts +10 -9
  142. package/src/translations.ts +6 -5
  143. package/src/translator.ts +68 -0
  144. package/src/types.ts +42 -0
  145. package/tsconfig.cjs.json +3 -0
  146. package/tsconfig.json +3 -0
  147. package/tsconfig.tsbuildinfo +1 -1
  148. package/dist/cjs/useTranslation.cjs +0 -12
  149. package/dist/tests/useTranslation.unit.test.d.ts +0 -2
  150. package/dist/tests/useTranslation.unit.test.d.ts.map +0 -1
  151. package/dist/tests/useTranslation.unit.test.js +0 -81
  152. package/dist/tests/useTranslation.unit.test.js.map +0 -1
  153. package/dist/useTranslation.d.ts +0 -7
  154. package/dist/useTranslation.d.ts.map +0 -1
  155. package/dist/useTranslation.js +0 -11
  156. package/dist/useTranslation.js.map +0 -1
  157. package/src/tests/useTranslation.unit.test.ts +0 -134
  158. package/src/useTranslation.ts +0 -31
@@ -1,10 +1,12 @@
1
1
 
2
- > @prosopo/locale@3.4.3 build:cjs
2
+ > @prosopo/locale@3.6.0 build:cjs
3
3
  > NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV
4
4
 
5
5
  ViteCommonJSConfig: .
6
- { tsConfigPaths: [] }
7
- { externals: [] }
6
+ {
7
+ tsConfigPaths: [ '/home/runner/work/captcha/captcha/packages/util/tsconfig.json' ]
8
+ }
9
+ { externals: [ '@prosopo/util' ] }
8
10
  The plugin "vite-tsconfig-paths" is detected. Vite now supports tsconfig paths resolution natively via the resolve.tsconfigPaths option. You can remove the plugin and set resolve.tsconfigPaths: true in your Vite config instead.
9
11
  vite v8.1.5 building ssr environment for production...
10
12
  Bundle build started
@@ -13,20 +15,20 @@ Bundle build started
13
15
  rendering chunks...
14
16
  computing gzip size...
15
17
  dist/cjs/util.cjs 0.28 kB │ gzip: 0.17 kB
16
- dist/cjs/useTranslation.cjs 0.35 kB │ gzip: 0.20 kB
17
- dist/cjs/i18nMiddleware.cjs 0.36 kB │ gzip: 0.21 kB
18
- dist/cjs/i18SharedOptions.cjs 0.41 kB │ gzip: 0.25 kB
19
- dist/cjs/translationKey.cjs 0.74 kB │ gzip: 0.38 kB
18
+ dist/cjs/i18SharedOptions.cjs 0.39 kB │ gzip: 0.24 kB
19
+ dist/cjs/i18nMiddleware.cjs 0.51 kB │ gzip: 0.26 kB
20
+ dist/cjs/translationKey.cjs 0.68 kB │ gzip: 0.35 kB
20
21
  dist/cjs/process.cjs 0.78 kB │ gzip: 0.46 kB
21
- dist/cjs/index.cjs 0.83 kB │ gzip: 0.27 kB
22
- dist/cjs/translations.cjs 0.86 kB │ gzip: 0.46 kB
22
+ dist/cjs/translations.cjs 0.80 kB │ gzip: 0.40 kB
23
+ dist/cjs/translator.cjs 0.83 kB │ gzip: 0.45 kB
24
+ dist/cjs/index.cjs 0.87 kB │ gzip: 0.27 kB
23
25
  dist/cjs/_virtual/_rolldown/runtime.cjs 1.00 kB │ gzip: 0.50 kB
24
26
  dist/cjs/i18nBackend.cjs 1.47 kB │ gzip: 0.62 kB
25
27
  dist/cjs/loadI18next.cjs 2.03 kB │ gzip: 0.84 kB
26
- dist/cjs/i18nFrontend.cjs 2.47 kB │ gzip: 0.87 kB
27
- dist/cjs/locales/en/translation.cjs 16.56 kB │ gzip: 5.98 kB
28
+ dist/cjs/i18nFrontend.cjs 6.07 kB │ gzip: 2.44 kB
29
+ dist/cjs/locales/en/translation.cjs 16.64 kB │ gzip: 6.01 kB
28
30
 
29
- ✓ built in 79ms
31
+ ✓ built in 40ms
30
32
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/ar/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/ar/translation.json
31
33
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/az/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/az/translation.json
32
34
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/cs/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/cs/translation.json
@@ -41,8 +43,8 @@ dist/cjs/locales/en/translation.cjs 16.56 kB │ gzip: 5.98 kB
41
43
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/id/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/id/translation.json
42
44
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/it/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/it/translation.json
43
45
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/ja/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/ja/translation.json
44
- [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/jv/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/jv/translation.json
45
46
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/ko/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/ko/translation.json
47
+ [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/jv/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/jv/translation.json
46
48
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/ml/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/ml/translation.json
47
49
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/ms/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/ms/translation.json
48
50
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/nl/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/nl/translation.json
@@ -1,14 +1,17 @@
1
1
 
2
- > @prosopo/locale@3.4.3 build:tsc
2
+ > @prosopo/locale@3.6.0 build:tsc
3
3
  > tsc --build --verbose
4
4
 
5
- 1:03:47 PM - Projects in this build:
5
+ 3:28:19 PM - Projects in this build:
6
6
  * ../../dev/config/tsconfig.json
7
+ * ../util/tsconfig.json
7
8
  * tsconfig.json
8
9
 
9
- 1:03:48 PM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
10
+ 3:28:19 PM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
10
11
 
11
- 1:03:48 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
12
+ 3:28:19 PM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
12
13
 
13
- 1:03:48 PM - Building project '/home/runner/work/captcha/captcha/packages/locale/tsconfig.json'...
14
+ 3:28:19 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
15
+
16
+ 3:28:19 PM - Building project '/home/runner/work/captcha/captcha/packages/locale/tsconfig.json'...
14
17
 
@@ -1,14 +1,16 @@
1
1
 
2
- > @prosopo/locale@3.4.3 build
2
+ > @prosopo/locale@3.6.0 build
3
3
  > npm run build:cross-env -- --mode ${NODE_ENV:-development}
4
4
 
5
5
 
6
- > @prosopo/locale@3.4.3 build:cross-env
6
+ > @prosopo/locale@3.6.0 build:cross-env
7
7
  > vite build --config vite.esm.config.ts --mode production
8
8
 
9
9
  ViteEsmConfig: .
10
- { tsConfigPaths: [] }
11
- { externals: [] }
10
+ {
11
+ tsConfigPaths: [ '/home/runner/work/captcha/captcha/packages/util/tsconfig.json' ]
12
+ }
13
+ { externals: [ '@prosopo/util' ] }
12
14
  The plugin "vite-tsconfig-paths" is detected. Vite now supports tsconfig paths resolution natively via the resolve.tsconfigPaths option. You can remove the plugin and set resolve.tsconfigPaths: true in your Vite config instead.
13
15
  vite v8.1.5 building ssr environment for production...
14
16
  Bundle build started
@@ -18,22 +20,22 @@ rendering chunks...
18
20
  computing gzip size...
19
21
  dist/_virtual/_rolldown/runtime.js 0.04 kB │ gzip: 0.06 kB
20
22
  dist/util.js 0.25 kB │ gzip: 0.16 kB
21
- dist/i18nMiddleware.js 0.27 kB │ gzip: 0.18 kB
22
- dist/useTranslation.js 0.30 kB │ gzip: 0.19 kB
23
- dist/i18SharedOptions.js 0.37 kB │ gzip: 0.25 kB
24
- dist/index.js 0.47 kB │ gzip: 0.20 kB
25
- dist/translationKey.js 0.68 kB │ gzip: 0.37 kB
26
- dist/translations.js 0.70 kB │ gzip: 0.39 kB
23
+ dist/i18nMiddleware.js 0.31 kB │ gzip: 0.19 kB
24
+ dist/i18SharedOptions.js 0.35 kB │ gzip: 0.24 kB
25
+ dist/index.js 0.48 kB │ gzip: 0.20 kB
26
+ dist/translationKey.js 0.63 kB │ gzip: 0.34 kB
27
+ dist/translations.js 0.74 kB │ gzip: 0.40 kB
27
28
  dist/process.js 0.77 kB │ gzip: 0.46 kB
29
+ dist/translator.js 0.79 kB │ gzip: 0.44 kB
28
30
  dist/i18nBackend.js 1.01 kB │ gzip: 0.53 kB
29
- dist/i18nFrontend.js 1.75 kB │ gzip: 0.78 kB
30
31
  dist/loadI18next.js 1.93 kB │ gzip: 0.83 kB
31
- dist/locales/en/translation.js 16.32 kB │ gzip: 5.94 kB
32
+ dist/i18nFrontend.js 5.98 kB │ gzip: 2.42 kB
33
+ dist/locales/en/translation.js 16.41 kB │ gzip: 5.97 kB
32
34
 
33
- ✓ built in 82ms
35
+ ✓ built in 30ms
34
36
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/ar/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/ar/translation.json
35
- [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/az/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/az/translation.json
36
37
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/cs/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/cs/translation.json
38
+ [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/az/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/az/translation.json
37
39
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/de/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/de/translation.json
38
40
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/el/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/el/translation.json
39
41
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/en/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/en/translation.json
@@ -61,5 +63,5 @@ dist/locales/en/translation.js 16.32 kB │ gzip: 5.94 kB
61
63
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/th/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/th/translation.json
62
64
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/tr/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/tr/translation.json
63
65
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/uk/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/uk/translation.json
64
- [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/vi/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/vi/translation.json
65
66
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/zh-CN/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/zh-CN/translation.json
67
+ [copy-plugin] copying /home/runner/work/captcha/captcha/packages/locale/src/locales/vi/translation.json to /home/runner/work/captcha/captcha/packages/locale/dist/locales/vi/translation.json
package/CHANGELOG.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # @prosopo/locale
2
2
 
3
+ ## 3.6.0
4
+ ### Minor Changes
5
+
6
+ - a9141c3: Stop shipping i18next to the browser. Saves about 22KB gzipped off the widget.
7
+
8
+ The widget was pulling in i18next and four of its plugins — a language detector, an HTTP backend, a chained backend and a resources-to-backend adapter, which between them also dragged in the `cross-fetch` polyfill — to look up 444 short strings with no plurals, no nesting and two interpolated values. That machinery is about 23KB gzipped; the replacement is 1.25KB.
9
+
10
+ `i18nFrontend.ts` now does the job directly: pick a language, fetch the matching `locales/<lang>/translation.json` next to the bundle, and look keys up in it. Behaviour is unchanged in the ways a visitor can see:
11
+
12
+ - language is chosen from the widget's own setting first, then a cookie, then localStorage, then the browser — the same order as before, and the choice is still remembered in both cookie and localStorage
13
+ - a regional tag like `de-AT` still resolves to `de`
14
+ - English is still fetched alongside the chosen language, so a key a translation is missing still renders English rather than its key
15
+ - `{{name}}` placeholders are still filled in, including in a caller's `defaultValue`
16
+ - if the catalogue cannot be fetched the widget still renders, in English, rather than waiting
17
+
18
+ i18next stays for the server, where `i18next-http-middleware` needs the real instance to read the `Accept-Language` header. `Ti18n` is now a small interface describing the handful of methods this repository actually calls, which both implementations satisfy, so `@prosopo/common` no longer needs i18next for a type either.
19
+
20
+ Covered by 189 tests in `@prosopo/locale`, and the built bundle was checked in a real browser: French picked up from the browser, a switch to German, interpolation, and both fallbacks.
21
+ - a9141c3: Stop loading zod before the widget can draw itself. Takes another 13KB gzipped off the critical path.
22
+
23
+ zod is 14KB gzipped and it was being downloaded and parsed before the checkbox appeared, because six small things on the startup path happened to use it:
24
+
25
+ - two lists of strings in `@prosopo/logger` (log levels, output format)
26
+ - two lists of strings in `@prosopo/locale` (language codes, translation keys)
27
+ - two lists of two strings in `@prosopo/types` (start mode, challenge placement)
28
+ - one four-field object in `@prosopo/load-balancer` (a provider entry)
29
+ - an `instanceof ZodError` check in `@prosopo/common`
30
+ - `INPUT_LIMITS`, a plain table of numbers, that happened to live in the same file as zod-based string builders
31
+
32
+ None of these need a validation library. They are now plain TypeScript: a list, a type, and where input is untrusted, a one-line guard. `INPUT_LIMITS` moved to its own file so reading it no longer drags the builders along.
33
+
34
+ zod has not gone anywhere — the real request and response schemas in `@prosopo/types` still use it, and still validate exactly as before. It now arrives with the code that needs it, after the widget is on screen, rather than in front of it.
35
+
36
+ Two API changes for anyone importing these directly:
37
+
38
+ - `LanguageSchema`, `TranslationKeysSchema`, `StartModeSchema` and `Placement` are no longer exported as zod schemas. Use `isLanguage()`, `isStartMode()`, `isPlacement()` to check a value, and `LanguageCodes`, `translationKeys`, `StartModes`, `Placements` for the lists.
39
+ - `isZodError()` now recognises a zod error by its name rather than `instanceof`. That is strictly more tolerant: the name still matches when an error crosses a realm boundary or comes from a second copy of zod, which `instanceof` misses — it was already the fallback arm of the same check.
40
+
41
+ Two behaviour notes: a malformed entry in the fetched provider list now throws a plain `Error` naming the entry, where it used to throw an untranslated zod error; and the language codes accepted are unchanged.
42
+
43
+ Covered by the existing suites for every package touched (types, types-database, locale, logger, common, load-balancer, all five procaptcha packages, api, cli, api-express-router, server, and the provider's 1322 unit tests), all passing. The built bundle was also loaded in a real browser: the widget renders from the first eight chunks, zod arrives in the second wave, and the provider's error came back translated into German.
44
+
45
+ ### Patch Changes
46
+
47
+ - Updated dependencies [a9141c3]
48
+ - @prosopo/util@3.3.11
49
+
50
+ ## 3.5.0
51
+ ### Minor Changes
52
+
53
+ - 59c02da: Replace React with vanilla TS/DOM in the widget.
54
+
55
+ The widget packages no longer depend on react, react-dom, @emotion or
56
+ react-i18next: every component is now a `mount*` function returning a handle
57
+ with `update`/`destroy`. `useTranslation` is replaced by `createTranslator`,
58
+ which exposes i18next's `t` plus the events that used to trigger a re-render.
59
+ The rendered markup, styling and behaviour are unchanged — only the
60
+ implementation is.
61
+
62
+ Everything the widget has gained since this rewrite started is carried over,
63
+ so nothing is lost by dropping React: the shared challenge surface (popup and
64
+ float placement, escape/outside-click dismissal and the dialog focus trap),
65
+ the image-tile and puzzle-piece keyboard paths, the checkbox's focus handover
66
+ across the loading swap, the server-rendered puzzle imagery, `startMode:
67
+ "manual"` with `window.procaptcha.start()`, `data-bind` / targeted
68
+ `execute(widgetId)`, the Web Bot Auth "authenticated" badge, the client
69
+ session id, and the bounded session re-mint and reload handling in the
70
+ frictionless wrapper.
71
+
72
+ ## 3.4.4
73
+ ### Patch Changes
74
+
75
+ - a22069d: Let a Block access rule name the reason it fired, so the 403 says why instead of "Forbidden"
76
+
3
77
  ## 3.4.3
4
78
  ### Patch Changes
5
79
 
@@ -1,9 +1,9 @@
1
1
  const require_translations = require("./translations.cjs");
2
2
  var i18nSharedOptions = {
3
3
  debug: require("./process.cjs").getProcess()?.env?.PROSOPO_LOG_LEVEL === "debug",
4
- fallbackLng: require_translations.LanguageSchema.enum.en,
4
+ fallbackLng: require_translations.Languages.english,
5
5
  namespace: "translation",
6
- supportedLngs: Object.values(require_translations.Languages),
6
+ supportedLngs: require_translations.LanguageCodes,
7
7
  nonExplicitSupportedLngs: false
8
8
  };
9
9
  //#endregion
@@ -1,9 +1,9 @@
1
1
  const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
2
  const require_util = require("./util.cjs");
3
3
  const require_i18SharedOptions = require("./i18SharedOptions.cjs");
4
- let i18next_http_middleware = require("i18next-http-middleware");
5
4
  let i18next = require("i18next");
6
5
  i18next = require_runtime.__toESM(i18next, 1);
6
+ let i18next_http_middleware = require("i18next-http-middleware");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_runtime.__toESM(node_path, 1);
9
9
  let i18next_fs_backend_cjs = require("i18next-fs-backend/cjs");
@@ -1,62 +1,176 @@
1
- const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
1
  const require_i18SharedOptions = require("./i18SharedOptions.cjs");
3
- let react_i18next = require("react-i18next");
4
- let i18next = require("i18next");
5
- i18next = require_runtime.__toESM(i18next, 1);
6
- let i18next_browser_languagedetector = require("i18next-browser-languagedetector");
7
- i18next_browser_languagedetector = require_runtime.__toESM(i18next_browser_languagedetector, 1);
8
- let i18next_chained_backend = require("i18next-chained-backend");
9
- i18next_chained_backend = require_runtime.__toESM(i18next_chained_backend, 1);
10
- let i18next_http_backend = require("i18next-http-backend");
11
- i18next_http_backend = require_runtime.__toESM(i18next_http_backend, 1);
12
- let i18next_resources_to_backend = require("i18next-resources-to-backend");
13
- i18next_resources_to_backend = require_runtime.__toESM(i18next_resources_to_backend, 1);
2
+ let _prosopo_util = require("@prosopo/util");
14
3
  //#region src/i18nFrontend.ts
15
- var reactOptions = {
16
- react: { useSuspense: true },
17
- detection: {
18
- order: [
19
- "navigator",
20
- "htmlTag",
21
- "path",
22
- "subdomain"
23
- ],
24
- caches: ["localStorage", "cookie"]
4
+ var NAMESPACE = "translation";
5
+ var FALLBACK_LANGUAGE = require_i18SharedOptions.i18nSharedOptions.fallbackLng;
6
+ var STORAGE_KEY = "i18nextLng";
7
+ var COOKIE_KEY = "i18next";
8
+ var COOKIE_MAX_AGE_SECONDS = 10080 * 60;
9
+ /**
10
+ * How long to wait for a catalogue before rendering untranslated.
11
+ *
12
+ * A widget with no text is worse than a widget in English: every caller
13
+ * supplies a `defaultValue`, and `t()` falls back to the key otherwise, so a
14
+ * catalogue that never arrives degrades rather than blocks.
15
+ */
16
+ var CATALOGUE_FETCH_TIMEOUT_MS = 1e4;
17
+ var SUPPORTED_LANGUAGES = require_i18SharedOptions.i18nSharedOptions.supportedLngs;
18
+ /**
19
+ * Locale JSON sits next to the bundle that asked for it. The widget is embedded
20
+ * by third parties and cannot know a base URL, but it does know where its own
21
+ * module was served from.
22
+ */
23
+ var catalogueUrl = (language) => {
24
+ const moduleUrl = require("url").pathToFileURL(__filename).href;
25
+ return `${moduleUrl.slice(0, moduleUrl.lastIndexOf("/"))}/locales/${language}/${NAMESPACE}.json`;
26
+ };
27
+ /**
28
+ * `en-GB` is not in the catalogue list but `en` is, so a regional tag resolves
29
+ * to its base language rather than falling all the way back to English.
30
+ */
31
+ var resolveLanguage = (candidate) => {
32
+ if (!candidate) return;
33
+ if (SUPPORTED_LANGUAGES.includes(candidate)) return candidate;
34
+ const base = candidate.split("-")[0];
35
+ return base && SUPPORTED_LANGUAGES.includes(base) ? base : void 0;
36
+ };
37
+ var readCookie = () => {
38
+ try {
39
+ const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_KEY}=([^;]*)`));
40
+ return match?.[1] ? decodeURIComponent(match[1]) : void 0;
41
+ } catch {
42
+ return;
43
+ }
44
+ };
45
+ var readStorage = () => {
46
+ try {
47
+ return localStorage.getItem(STORAGE_KEY) ?? void 0;
48
+ } catch {
49
+ return;
50
+ }
51
+ };
52
+ var navigatorLanguages = () => {
53
+ try {
54
+ return navigator.languages?.length ? navigator.languages : [navigator.language];
55
+ } catch {
56
+ return [];
57
+ }
58
+ };
59
+ /**
60
+ * Cookie and localStorage are both written, matching what the widget did when
61
+ * i18next-browser-languagedetector handled this, so a visitor's choice survives
62
+ * across pages that embed the widget. Either store can throw in a sandboxed
63
+ * iframe or with third-party storage blocked; neither is load-bearing.
64
+ */
65
+ var cacheLanguage = (language) => {
66
+ try {
67
+ localStorage.setItem(STORAGE_KEY, language);
68
+ } catch {}
69
+ try {
70
+ document.cookie = `${COOKIE_KEY}=${encodeURIComponent(language)};max-age=${COOKIE_MAX_AGE_SECONDS};path=/`;
71
+ } catch {}
72
+ };
73
+ var detectLanguage = () => {
74
+ const candidates = [
75
+ readCookie(),
76
+ readStorage(),
77
+ ...navigatorLanguages()
78
+ ];
79
+ for (const candidate of candidates) {
80
+ const resolved = resolveLanguage(candidate);
81
+ if (resolved) return resolved;
82
+ }
83
+ return FALLBACK_LANGUAGE;
84
+ };
85
+ var fetchCatalogue = async (language) => {
86
+ const controller = new AbortController();
87
+ const timer = setTimeout(() => controller.abort(), CATALOGUE_FETCH_TIMEOUT_MS);
88
+ try {
89
+ const response = await fetch(catalogueUrl(language), { signal: controller.signal });
90
+ if (!response.ok) return {};
91
+ const body = await response.json();
92
+ return typeof body === "object" && body !== null ? (0, _prosopo_util.flatten)(body) : {};
93
+ } catch {
94
+ return {};
95
+ } finally {
96
+ clearTimeout(timer);
25
97
  }
26
98
  };
27
- var getLoadPath = (language, namespace) => {
28
- const filename = require("url").pathToFileURL(__filename).href.split("/").pop();
29
- return `${require("url").pathToFileURL(__filename).href.replace(`/${filename}`, "")}/locales/${language}/${namespace}.json`;
99
+ var interpolate = (template, options) => options ? template.replace(/\{\{(\w+)\}\}/g, (placeholder, name) => name in options ? String(options[name]) : placeholder) : template;
100
+ var createBrowserI18n = (requested) => {
101
+ const catalogues = /* @__PURE__ */ new Map();
102
+ const listeners = /* @__PURE__ */ new Map();
103
+ let language = resolveLanguage(requested) ?? detectLanguage();
104
+ let initialised = false;
105
+ const emit = (event) => {
106
+ for (const listener of listeners.get(event) ?? []) listener();
107
+ };
108
+ /**
109
+ * The fallback catalogue is fetched alongside the active one so a key
110
+ * missing from a translation still renders English rather than its key —
111
+ * the same key-level fallback i18next gave us via `fallbackLng`.
112
+ */
113
+ const loadCatalogues = async (target) => {
114
+ const wanted = [.../* @__PURE__ */ new Set([target, FALLBACK_LANGUAGE])].filter((candidate) => !catalogues.has(candidate));
115
+ await Promise.all(wanted.map(async (candidate) => {
116
+ catalogues.set(candidate, await fetchCatalogue(candidate));
117
+ }));
118
+ };
119
+ const lookup = (candidate, key) => catalogues.get(candidate)?.[key];
120
+ cacheLanguage(language);
121
+ const loading = loadCatalogues(language).then(() => {
122
+ initialised = true;
123
+ emit("initialized");
124
+ emit("loaded");
125
+ });
126
+ const instance = {
127
+ get language() {
128
+ return language;
129
+ },
130
+ get isInitialized() {
131
+ return initialised;
132
+ },
133
+ t: (key, options) => interpolate(lookup(language, key) ?? lookup(FALLBACK_LANGUAGE, key) ?? options?.defaultValue ?? key, options),
134
+ hasLoadedNamespace: (namespace) => namespace === NAMESPACE && catalogues.has(language),
135
+ changeLanguage: async (next) => {
136
+ const resolved = resolveLanguage(next) ?? FALLBACK_LANGUAGE;
137
+ if (resolved !== language) {
138
+ language = resolved;
139
+ cacheLanguage(resolved);
140
+ await loadCatalogues(resolved);
141
+ emit("loaded");
142
+ emit("languageChanged");
143
+ }
144
+ return instance;
145
+ },
146
+ on: (event, listener) => {
147
+ const existing = listeners.get(event) ?? /* @__PURE__ */ new Set();
148
+ existing.add(listener);
149
+ listeners.set(event, existing);
150
+ },
151
+ off: (event, listener) => {
152
+ listeners.get(event)?.delete(listener);
153
+ },
154
+ ready: async () => {
155
+ await loading;
156
+ return instance;
157
+ }
158
+ };
159
+ return instance;
30
160
  };
161
+ /**
162
+ * The widget owns this instance. A host page running its own i18n must not be
163
+ * able to capture the widget's translations, and every widget on the page
164
+ * shares one catalogue fetch.
165
+ */
166
+ var instance;
31
167
  function initializeI18n(i18nLoadedCallback, lng) {
32
- if (!i18next.default.isInitialized) {
33
- i18next.default.use(i18next_chained_backend.default).use(new i18next_browser_languagedetector.default(null, {
34
- order: [
35
- "cookie",
36
- "localStorage",
37
- "navigator"
38
- ],
39
- lookupQuerystring: "lng",
40
- lookupCookie: "i18next",
41
- lookupLocalStorage: "i18nextLng",
42
- caches: ["localStorage", "cookie"]
43
- })).use(react_i18next.initReactI18next).init({
44
- ...require_i18SharedOptions.i18nSharedOptions,
45
- ...lng ? { lng } : {},
46
- backend: {
47
- backends: [i18next_http_backend.default, (0, i18next_resources_to_backend.default)((language, namespace) => import(getLoadPath(language, namespace)))],
48
- backendOptions: [{ loadPath: getLoadPath("{{lng}}", "{{ns}}") }]
49
- },
50
- ...reactOptions
51
- });
52
- let notified = false;
53
- i18next.default.on("loaded", () => {
54
- if (notified) return;
55
- notified = true;
56
- i18nLoadedCallback?.(i18next.default);
57
- });
58
- } else i18nLoadedCallback?.(i18next.default);
59
- return i18next.default;
168
+ if (!instance) {
169
+ const created = createBrowserI18n(lng);
170
+ instance = created;
171
+ created.ready().then(() => i18nLoadedCallback?.(created));
172
+ } else i18nLoadedCallback?.(instance);
173
+ return instance;
60
174
  }
61
175
  //#endregion
62
176
  exports.default = initializeI18n;
@@ -1,9 +1,12 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
1
2
  const require_loadI18next = require("./loadI18next.cjs");
3
+ let i18next = require("i18next");
4
+ i18next = require_runtime.__toESM(i18next, 1);
2
5
  let i18next_http_middleware = require("i18next-http-middleware");
3
6
  //#region src/i18nMiddleware.ts
4
7
  async function i18nMiddleware(options) {
5
- const i18n = await require_loadI18next.default(true);
6
- return (0, i18next_http_middleware.handle)(i18n, { ...options });
8
+ await require_loadI18next.default(true);
9
+ return (0, i18next_http_middleware.handle)(i18next.default, { ...options });
7
10
  }
8
11
  //#endregion
9
12
  exports.default = i18nMiddleware;
@@ -4,11 +4,12 @@ const require_i18nMiddleware = require("./i18nMiddleware.cjs");
4
4
  const require_translations = require("./translations.cjs");
5
5
  const require_util = require("./util.cjs");
6
6
  const require_translationKey = require("./translationKey.cjs");
7
- const require_useTranslation = require("./useTranslation.cjs");
8
- exports.LanguageSchema = require_translations.LanguageSchema;
7
+ const require_translator = require("./translator.cjs");
8
+ exports.LanguageCodes = require_translations.LanguageCodes;
9
9
  exports.Languages = require_translations.Languages;
10
- exports.TranslationKeysSchema = require_translationKey.TranslationKeysSchema;
10
+ exports.createTranslator = require_translator.createTranslator;
11
11
  exports.i18nMiddleware = require_i18nMiddleware.default;
12
12
  exports.isClientSide = require_util.isClientSide;
13
+ exports.isLanguage = require_translations.isLanguage;
13
14
  exports.loadI18next = require_loadI18next.default;
14
- exports.useTranslation = require_useTranslation.default;
15
+ exports.translationKeys = require_translationKey.translationKeys;
@@ -137,6 +137,7 @@ var API = {
137
137
  "SPAM_EMAIL_COUNT_EXCEEDED": "Too many submissions from this email address",
138
138
  "VPN_BLOCKED": "Requests from VPN connections are not allowed for this site",
139
139
  "PROXY_BLOCKED": "Requests from proxy servers are not allowed for this site",
140
+ "TOO_MANY_LOCALHOST": "Too many CAPTCHA requests from localhost for this site key",
140
141
  "TOR_BLOCKED": "Requests from Tor exit nodes are not allowed for this site",
141
142
  "ABUSER_BLOCKED": "Requests from networks flagged for abuse are not allowed for this site",
142
143
  "DATACENTER_BLOCKED": "Requests from datacenter IP addresses are not allowed for this site",
@@ -1,5 +1,4 @@
1
1
  const require_translation = require("./locales/en/translation.cjs");
2
- let zod = require("zod");
3
2
  //#region src/translationKey.ts
4
3
  function getLeafFieldPath(obj) {
5
4
  if (typeof obj === "string") return [];
@@ -13,7 +12,7 @@ function getLeafFieldPath(obj) {
13
12
  }));
14
13
  }, []);
15
14
  }
16
- var TranslationKeysSchema = zod.z.enum(getLeafFieldPath(require_translation.default));
15
+ var translationKeys = getLeafFieldPath(require_translation.default);
17
16
  //#endregion
18
- exports.TranslationKeysSchema = TranslationKeysSchema;
19
17
  exports.getLeafFieldPath = getLeafFieldPath;
18
+ exports.translationKeys = translationKeys;
@@ -1,6 +1,3 @@
1
- const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
- let zod = require("zod");
3
- zod = require_runtime.__toESM(zod, 1);
4
1
  //#region src/translations.ts
5
2
  var Languages = {
6
3
  arabic: "ar",
@@ -36,7 +33,9 @@ var Languages = {
36
33
  vietnamese: "vi",
37
34
  chinese: "zh-CN"
38
35
  };
39
- var LanguageSchema = zod.default.enum(Object.values(Languages));
36
+ var LanguageCodes = Object.values(Languages);
37
+ var isLanguage = (value) => LanguageCodes.includes(value);
40
38
  //#endregion
41
- exports.LanguageSchema = LanguageSchema;
39
+ exports.LanguageCodes = LanguageCodes;
42
40
  exports.Languages = Languages;
41
+ exports.isLanguage = isLanguage;
@@ -0,0 +1,29 @@
1
+ const require_i18nFrontend = require("./i18nFrontend.cjs");
2
+ //#region src/translator.ts
3
+ var NAMESPACE = "translation";
4
+ /**
5
+ * Vanilla replacement for the `useTranslation` hook: the instance's `t` plus
6
+ * the three events that used to trigger a React re-render.
7
+ */
8
+ var createTranslator = (existing) => {
9
+ const i18n = existing ?? require_i18nFrontend.default();
10
+ const isReady = () => i18n.isInitialized && i18n.hasLoadedNamespace(NAMESPACE);
11
+ return {
12
+ t: (key, options) => i18n.t(key, options),
13
+ isReady,
14
+ i18n,
15
+ subscribe: (listener) => {
16
+ const events = [
17
+ "initialized",
18
+ "loaded",
19
+ "languageChanged"
20
+ ];
21
+ for (const event of events) i18n.on(event, listener);
22
+ return () => {
23
+ for (const event of events) i18n.off(event, listener);
24
+ };
25
+ }
26
+ };
27
+ };
28
+ //#endregion
29
+ exports.createTranslator = createTranslator;
@@ -1,8 +1,8 @@
1
1
  export declare const i18nSharedOptions: {
2
2
  debug: boolean;
3
- fallbackLng: string | undefined;
3
+ fallbackLng: "en";
4
4
  namespace: string;
5
- supportedLngs: ("ar" | "az" | "cs" | "de" | "el" | "en" | "es" | "fi" | "fr" | "hi" | "hu" | "id" | "it" | "ja" | "jv" | "ko" | "ml" | "ms" | "nl" | "no" | "pl" | "pt-BR" | "pt" | "ro" | "ru" | "sr" | "sv" | "th" | "tr" | "uk" | "vi" | "zh-CN")[];
5
+ supportedLngs: readonly string[];
6
6
  nonExplicitSupportedLngs: boolean;
7
7
  };
8
8
  //# sourceMappingURL=i18SharedOptions.d.ts.map
@@ -1,10 +1,10 @@
1
- import { LanguageSchema, Languages } from "./translations.js";
1
+ import { LanguageCodes, Languages } from "./translations.js";
2
2
  import { getProcess } from "./process.js";
3
3
  var i18nSharedOptions = {
4
4
  debug: getProcess()?.env?.PROSOPO_LOG_LEVEL === "debug",
5
- fallbackLng: LanguageSchema.enum.en,
5
+ fallbackLng: Languages.english,
6
6
  namespace: "translation",
7
- supportedLngs: Object.values(Languages),
7
+ supportedLngs: LanguageCodes,
8
8
  nonExplicitSupportedLngs: false
9
9
  };
10
10
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"i18SharedOptions.js","sourceRoot":"","sources":["../src/i18SharedOptions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAM9D,MAAM,QAAQ,GAAG,UAAU,EAAE,EAAE,GAAG,EAAE,iBAAiB,CAAC;AAEtD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAChC,KAAK,EAAE,QAAQ,KAAK,OAAO;IAC3B,WAAW,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE;IACnC,SAAS,EAAE,aAAa;IACxB,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;IACvC,wBAAwB,EAAE,KAAK;CAC/B,CAAC"}
1
+ {"version":3,"file":"i18SharedOptions.js","sourceRoot":"","sources":["../src/i18SharedOptions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAM7D,MAAM,QAAQ,GAAG,UAAU,EAAE,EAAE,GAAG,EAAE,iBAAiB,CAAC;AAEtD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAChC,KAAK,EAAE,QAAQ,KAAK,OAAO;IAC3B,WAAW,EAAE,SAAS,CAAC,OAAO;IAC9B,SAAS,EAAE,aAAa;IACxB,aAAa,EAAE,aAAa;IAC5B,wBAAwB,EAAE,KAAK;CAC/B,CAAC"}
@@ -1,8 +1,8 @@
1
1
  import "./_virtual/_rolldown/runtime.js";
2
2
  import { isServerSide } from "./util.js";
3
3
  import { i18nSharedOptions } from "./i18SharedOptions.js";
4
- import { LanguageDetector } from "i18next-http-middleware";
5
4
  import i18n from "i18next";
5
+ import { LanguageDetector } from "i18next-http-middleware";
6
6
  import path from "node:path";
7
7
  import FSBackend from "i18next-fs-backend/cjs";
8
8
  //#region src/i18nBackend.ts