crelte 0.5.8 → 0.5.10

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 (175) hide show
  1. package/dist/blocks/Blocks.d.ts +41 -4
  2. package/dist/blocks/Blocks.d.ts.map +1 -1
  3. package/dist/blocks/Blocks.js +47 -2
  4. package/dist/blocks/Blocks.svelte +2 -16
  5. package/dist/blocks/Blocks.svelte.d.ts +20 -37
  6. package/dist/blocks/Blocks.svelte.d.ts.map +1 -1
  7. package/dist/blocks/index.d.ts +2 -2
  8. package/dist/blocks/index.d.ts.map +1 -1
  9. package/dist/blocks/index.js +2 -2
  10. package/dist/cookies/ServerCookies.d.ts +1 -1
  11. package/dist/cookies/ServerCookies.js +1 -1
  12. package/dist/cookies/index.d.ts +1 -1
  13. package/dist/crelte.d.ts +21 -10
  14. package/dist/crelte.d.ts.map +1 -1
  15. package/dist/index.d.ts +19 -14
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +17 -13
  18. package/dist/init/client.d.ts +2 -2
  19. package/dist/init/client.d.ts.map +1 -1
  20. package/dist/init/client.js +3 -2
  21. package/dist/init/server.d.ts +7 -6
  22. package/dist/init/server.d.ts.map +1 -1
  23. package/dist/init/server.js +6 -6
  24. package/dist/loadData/Globals.d.ts +3 -3
  25. package/dist/loadData/Globals.js +3 -3
  26. package/dist/loadData/loadData.d.ts +8 -18
  27. package/dist/loadData/loadData.d.ts.map +1 -1
  28. package/dist/loadData/loadData.js +2 -2
  29. package/dist/node/index.d.ts +5 -0
  30. package/dist/node/index.d.ts.map +1 -1
  31. package/dist/node/index.js +84 -35
  32. package/dist/plugins/Events.d.ts +4 -4
  33. package/dist/plugins/Plugins.d.ts +2 -2
  34. package/dist/plugins/index.d.ts +2 -3
  35. package/dist/plugins/index.d.ts.map +1 -1
  36. package/dist/plugins/index.js +2 -1
  37. package/dist/queries/Queries.d.ts +9 -0
  38. package/dist/queries/Queries.d.ts.map +1 -1
  39. package/dist/queries/Queries.js +29 -17
  40. package/dist/queries/QueryError.d.ts +2 -0
  41. package/dist/queries/QueryError.d.ts.map +1 -1
  42. package/dist/queries/QueryError.js +6 -1
  43. package/dist/queries/gql.d.ts +2 -2
  44. package/dist/queries/gql.js +2 -2
  45. package/dist/queries/index.d.ts +7 -5
  46. package/dist/queries/index.d.ts.map +1 -1
  47. package/dist/queries/vars.d.ts +7 -7
  48. package/dist/queries/vars.d.ts.map +1 -1
  49. package/dist/queries/vars.js +10 -8
  50. package/dist/routing/LoadRunner.d.ts +1 -1
  51. package/dist/routing/LoadRunner.js +1 -1
  52. package/dist/routing/index.d.ts +4 -4
  53. package/dist/routing/index.d.ts.map +1 -1
  54. package/dist/routing/route/BaseRoute.d.ts +25 -25
  55. package/dist/routing/route/BaseRoute.js +24 -24
  56. package/dist/routing/route/Request.d.ts +4 -4
  57. package/dist/routing/route/Request.js +4 -4
  58. package/dist/routing/route/Route.d.ts +2 -2
  59. package/dist/routing/route/Route.js +2 -2
  60. package/dist/routing/router/BaseRouter.d.ts +4 -4
  61. package/dist/routing/router/BaseRouter.js +3 -3
  62. package/dist/routing/router/ClientRouter.d.ts +1 -1
  63. package/dist/routing/router/ClientRouter.js +1 -1
  64. package/dist/routing/router/Router.d.ts +27 -25
  65. package/dist/routing/router/Router.d.ts.map +1 -1
  66. package/dist/routing/router/Router.js +24 -21
  67. package/dist/routing/router/ServerRouter.d.ts +1 -1
  68. package/dist/routing/router/ServerRouter.js +1 -1
  69. package/dist/server/CrelteServer.d.ts +5 -5
  70. package/dist/server/CrelteServer.d.ts.map +1 -1
  71. package/dist/server/CrelteServer.js +4 -4
  72. package/dist/server/Request.d.ts +7 -1
  73. package/dist/server/Request.d.ts.map +1 -1
  74. package/dist/server/Request.js +7 -1
  75. package/dist/server/ServerRouter.d.ts +1 -1
  76. package/dist/server/ServerRouter.d.ts.map +1 -1
  77. package/dist/server/index.d.ts +4 -4
  78. package/dist/server/index.d.ts.map +1 -1
  79. package/dist/server/index.js +1 -1
  80. package/dist/server/shared.d.ts +7 -13
  81. package/dist/server/shared.d.ts.map +1 -1
  82. package/dist/server/shared.js +14 -16
  83. package/dist/ssr/SsrCache.d.ts +56 -4
  84. package/dist/ssr/SsrCache.d.ts.map +1 -1
  85. package/dist/ssr/SsrCache.js +94 -20
  86. package/dist/std/index.d.ts +13 -0
  87. package/dist/std/index.d.ts.map +1 -1
  88. package/dist/std/index.js +13 -0
  89. package/dist/std/rand/index.d.ts +34 -0
  90. package/dist/std/rand/index.d.ts.map +1 -0
  91. package/dist/std/rand/index.js +45 -0
  92. package/dist/std/rand/internal.d.ts +8 -0
  93. package/dist/std/rand/internal.d.ts.map +1 -0
  94. package/dist/std/rand/internal.js +15 -0
  95. package/dist/std/stores/Writable.d.ts +1 -1
  96. package/dist/std/stores/Writable.js +1 -1
  97. package/dist/translations/index.d.ts +28 -0
  98. package/dist/translations/index.d.ts.map +1 -0
  99. package/dist/translations/index.js +31 -0
  100. package/dist/translations/loadTranslations.d.ts +23 -0
  101. package/dist/translations/loadTranslations.d.ts.map +1 -0
  102. package/dist/translations/loadTranslations.js +26 -0
  103. package/dist/translations/loader/GlobalLoader.d.ts +34 -0
  104. package/dist/translations/loader/GlobalLoader.d.ts.map +1 -0
  105. package/dist/translations/loader/GlobalLoader.js +45 -0
  106. package/dist/translations/loader/fileLoader/ClientFileLoader.d.ts +10 -0
  107. package/dist/translations/loader/fileLoader/ClientFileLoader.d.ts.map +1 -0
  108. package/dist/translations/loader/fileLoader/ClientFileLoader.js +24 -0
  109. package/dist/translations/loader/fileLoader/NodeFileLoader.d.ts +9 -0
  110. package/dist/translations/loader/fileLoader/NodeFileLoader.d.ts.map +1 -0
  111. package/dist/translations/loader/fileLoader/NodeFileLoader.js +21 -0
  112. package/dist/translations/loader/fileLoader/fileLoader.d.ts +5 -0
  113. package/dist/translations/loader/fileLoader/fileLoader.d.ts.map +1 -0
  114. package/dist/translations/loader/fileLoader/fileLoader.js +16 -0
  115. package/dist/translations/loader/index.d.ts +7 -0
  116. package/dist/translations/loader/index.d.ts.map +1 -0
  117. package/dist/translations/loader/index.js +1 -0
  118. package/dist/translations/translationsPlugin.d.ts +64 -0
  119. package/dist/translations/translationsPlugin.d.ts.map +1 -0
  120. package/dist/translations/translationsPlugin.js +110 -0
  121. package/dist/translations/utils.d.ts +8 -0
  122. package/dist/translations/utils.d.ts.map +1 -0
  123. package/dist/translations/utils.js +15 -0
  124. package/dist/vite/index.d.ts.map +1 -1
  125. package/dist/vite/index.js +30 -38
  126. package/package.json +9 -1
  127. package/src/blocks/Blocks.svelte +3 -78
  128. package/src/blocks/Blocks.ts +63 -6
  129. package/src/blocks/index.ts +2 -2
  130. package/src/cookies/ServerCookies.ts +1 -1
  131. package/src/cookies/index.ts +1 -1
  132. package/src/crelte.ts +21 -10
  133. package/src/index.ts +19 -14
  134. package/src/init/client.ts +3 -2
  135. package/src/init/server.ts +14 -8
  136. package/src/loadData/Globals.ts +3 -3
  137. package/src/loadData/loadData.ts +8 -18
  138. package/src/node/index.ts +100 -39
  139. package/src/plugins/Events.ts +4 -4
  140. package/src/plugins/Plugins.ts +2 -2
  141. package/src/plugins/index.ts +2 -3
  142. package/src/queries/Queries.ts +35 -18
  143. package/src/queries/QueryError.ts +10 -1
  144. package/src/queries/gql.ts +2 -2
  145. package/src/queries/index.ts +7 -4
  146. package/src/queries/vars.ts +12 -8
  147. package/src/routing/LoadRunner.ts +1 -1
  148. package/src/routing/index.ts +13 -3
  149. package/src/routing/route/BaseRoute.ts +25 -25
  150. package/src/routing/route/Request.ts +4 -4
  151. package/src/routing/route/Route.ts +2 -2
  152. package/src/routing/router/BaseRouter.ts +4 -4
  153. package/src/routing/router/ClientRouter.ts +1 -1
  154. package/src/routing/router/Router.ts +28 -25
  155. package/src/routing/router/ServerRouter.ts +1 -1
  156. package/src/server/CrelteServer.ts +5 -5
  157. package/src/server/Request.ts +7 -1
  158. package/src/server/ServerRouter.ts +1 -2
  159. package/src/server/index.ts +17 -3
  160. package/src/server/shared.ts +28 -33
  161. package/src/ssr/SsrCache.ts +104 -22
  162. package/src/std/index.ts +14 -0
  163. package/src/std/rand/index.ts +55 -0
  164. package/src/std/rand/internal.ts +17 -0
  165. package/src/std/stores/Writable.ts +1 -1
  166. package/src/translations/index.ts +56 -0
  167. package/src/translations/loadTranslations.ts +33 -0
  168. package/src/translations/loader/GlobalLoader.ts +62 -0
  169. package/src/translations/loader/fileLoader/ClientFileLoader.ts +40 -0
  170. package/src/translations/loader/fileLoader/NodeFileLoader.ts +32 -0
  171. package/src/translations/loader/fileLoader/fileLoader.ts +19 -0
  172. package/src/translations/loader/index.ts +8 -0
  173. package/src/translations/translationsPlugin.ts +145 -0
  174. package/src/translations/utils.ts +17 -0
  175. package/src/vite/index.ts +43 -39
@@ -1,4 +1,9 @@
1
- export async function calcKey(data: any) {
1
+ /**
2
+ * Calculates a key based on the json representation of the data
3
+ *
4
+ * If available hashes the data using SHA-1
5
+ */
6
+ export async function calcKey(data: any): Promise<string> {
2
7
  const json = JSON.stringify(data);
3
8
  // this should only happen in an unsecure context
4
9
  // specifically in the craft preview locally
@@ -27,53 +32,130 @@ export async function calcKey(data: any) {
27
32
  * generally. Storing data and retrieving it will also work on the client.
28
33
  */
29
34
  export default class SsrCache {
30
- private store: Record<string, any>;
35
+ private store: Map<string, any>;
31
36
 
32
37
  constructor() {
33
- this.store = {};
34
-
35
- // @ts-ignore
36
- if (typeof window !== 'undefined' && window.SSR_STORE) {
37
- // @ts-ignore
38
- this.store = window.SSR_STORE;
39
- }
38
+ this.store = new Map();
40
39
  }
41
40
 
42
41
  /**
43
- * check if the value is in the cache else calls the fn
42
+ * Check if a key exists in the cache
44
43
  */
45
- async load<T>(key: string, fn: () => Promise<T>) {
46
- if (key in this.store) return this.store[key];
47
- const v = await fn();
48
- this.set(key, v);
49
- return v;
44
+ has(key: string): boolean {
45
+ return this.store.has(key);
50
46
  }
51
47
 
52
48
  /**
53
49
  * Get a value from the cache
54
50
  */
55
51
  get<T>(key: string): T | null {
56
- return this.store[key] ?? null;
52
+ return this.store.get(key) ?? null;
57
53
  }
58
54
 
59
55
  /**
60
56
  * Set a value in the cache
61
57
  */
62
58
  set<T>(key: string, val: T): T {
63
- return (this.store[key] = val);
59
+ this.store.set(key, val);
60
+ return val;
61
+ }
62
+
63
+ /**
64
+ * check if the value is in the cache else calls the fn
65
+ *
66
+ * See also {@link getOrInsertLoaded}
67
+ */
68
+ getOrInsertComputed<T>(key: string, fn: () => T): T {
69
+ if (this.store.has(key)) return this.store.get(key);
70
+ return this.set(key, fn());
71
+ }
72
+
73
+ /**
74
+ * check if the value is in the cache else calls the fn
75
+ *
76
+ * See also {@link getOrInsertComputed}
77
+ *
78
+ * @deprecated use {@link getOrInsertLoaded} instead
79
+ */
80
+ async load<T>(key: string, fn: () => Promise<T>): Promise<T> {
81
+ return this.getOrInsertLoaded<T>(key, fn);
82
+ }
83
+
84
+ /**
85
+ * check if the value is in the cache else calls the fn
86
+ *
87
+ * See also {@link getOrInsertComputed}
88
+ */
89
+ async getOrInsertLoaded<T>(key: string, fn: () => Promise<T>): Promise<T> {
90
+ if (this.store.has(key)) return this.store.get(key);
91
+ return this.set(key, await fn());
92
+ }
93
+
94
+ /**
95
+ * One-shot SSR handoff value.
96
+ *
97
+ * Intended use: call this once per request (per key) during SSR to generate
98
+ * a value that must match between server render and client hydration.
99
+ *
100
+ * On the server, the value is generated once per key and stored for hydration.
101
+ * Subsequent calls with the same key during SSR currently return the same value,
102
+ * but this behaviour is an implementation detail and may change in the future.
103
+ * Consumers should rely on calling this at most once per key during SSR.
104
+ *
105
+ * On the client, the stored value is returned exactly once and removed.
106
+ * Subsequent calls return a fresh value and are not cached.
107
+ *
108
+ * Warning: this function is designed to be called once per key during SSR.
109
+ * Calling it multiple times may lead to unexpected behaviour if the server-side
110
+ * implementation changes.
111
+ *
112
+ * See also {@link getOrInsertComputed}
113
+ */
114
+ takeOnce<T>(key: string, fn: () => T): T {
115
+ if (import.meta.env.SSR) {
116
+ if (this.store.has(key)) {
117
+ console.warn(
118
+ `SsrCache.takeOnce called multiple times for key "${key}" during SSR.`,
119
+ );
120
+ return this.store.get(key);
121
+ }
122
+ return this.set(key, fn());
123
+ }
124
+ if (this.store.has(key)) return this.remove<T>(key)!;
125
+ return fn();
126
+ }
127
+
128
+ /**
129
+ * Remove a value from the cache and return it
130
+ */
131
+ remove<T>(key: string): T | null {
132
+ const val = this.get<T>(key);
133
+ this.store.delete(key);
134
+ return val;
64
135
  }
65
136
 
66
137
  /** @hidden */
67
- clear() {
68
- this.store = {};
138
+ z_clear() {
139
+ this.store.clear();
140
+ }
141
+
142
+ /** @hidden */
143
+ z_importFromHead() {
144
+ // @ts-ignore
145
+ this.store = new Map(window._SSR_STORE ?? []);
146
+ // @ts-ignore
147
+ delete window._SSR_STORE;
69
148
  }
70
149
 
71
150
  private exportAsJson(): string {
72
- return JSON.stringify(this.store).replace(/</g, '\\u003c');
151
+ return JSON.stringify(Array.from(this.store.entries())).replace(
152
+ /</g,
153
+ '\\u003c',
154
+ );
73
155
  }
74
156
 
75
157
  /** @hidden */
76
- exportToHead(): string {
77
- return `\n\t\t<script>window.SSR_STORE = ${this.exportAsJson()};</script>`;
158
+ z_exportToHead(): string {
159
+ return `\n\t\t<script>window._SSR_STORE = ${this.exportAsJson()};</script>`;
78
160
  }
79
161
  }
package/src/std/index.ts CHANGED
@@ -1,3 +1,17 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * Crelte Standard library
5
+ *
6
+ * Provides standard utility functions and types for Crelte applications.
7
+ *
8
+ * ### Modules:
9
+ * - {@link std/intl}
10
+ * - {@link std/stores}
11
+ * - {@link std/sync}
12
+ * - {@link std/rand}
13
+ */
14
+
1
15
  /**
2
16
  * Delays for a specified amount of time.
3
17
  *
@@ -0,0 +1,55 @@
1
+ import { Crelte } from '../../crelte.js';
2
+ import { getCrelte } from '../../index.js';
3
+
4
+ /**
5
+ * Returns a random number between 0 (inclusive) and 1 (exclusive).
6
+ *
7
+ * In SSR mode, the same key will always return the same random number.
8
+ * In client mode, the first call will return the number generated during SSR.
9
+ */
10
+ export function random(key: string, crelte: Crelte = getCrelte()): number {
11
+ return crelte.ssrCache.takeOnce('_rand_' + key, () => Math.random());
12
+ }
13
+
14
+ /**
15
+ * Create a deterministic random number generator.
16
+ *
17
+ * Intended use: create this once during component initialization when a sequence
18
+ * of random values must be consistent between server-side rendering and client
19
+ * hydration.
20
+ *
21
+ * The generator is seeded during SSR using the provided key. During hydration,
22
+ * the same seed is reused so that the client produces the same sequence of random
23
+ * numbers as the server for the initial render.
24
+ *
25
+ * After hydration, the generator continues independently on the client.
26
+ *
27
+ * @returns A function that returns a pseudo-random number in the range
28
+ * [0, 1).
29
+ *
30
+ * #### Example
31
+ * ```ts
32
+ * const rng = createRng('lucky-number');
33
+ *
34
+ * const val1 = rng(); // same on server and client
35
+ * const val2 = rng(); // same on server and client
36
+ * ```
37
+ */
38
+ export function createRng(
39
+ key: string,
40
+ crelte: Crelte = getCrelte(),
41
+ ): () => number {
42
+ const seed = crelte.ssrCache.takeOnce('_rng_' + key, () => Math.random());
43
+
44
+ // park miller algorithm
45
+
46
+ const MOD = 2147483647;
47
+ const MUL = 16807;
48
+
49
+ let state = Math.floor(seed * (MOD - 1)) + 1;
50
+
51
+ return () => {
52
+ state = (state * MUL) % MOD;
53
+ return (state - 1) / (MOD - 1);
54
+ };
55
+ }
@@ -0,0 +1,17 @@
1
+ const ALPHABET: string =
2
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
3
+ const ALPHABET_LENGTH = ALPHABET.length;
4
+
5
+ /**
6
+ * Generates a random token of a specified length.
7
+ *
8
+ * @param length - The desired length of the token.
9
+ * @returns A random token.
10
+ */
11
+ export function randomToken(length: number = 8): string {
12
+ let s = '';
13
+ for (let i = 0; i < length; i++) {
14
+ s += ALPHABET[Math.floor(Math.random() * ALPHABET_LENGTH)];
15
+ }
16
+ return s;
17
+ }
@@ -27,7 +27,7 @@ export default class Writable<T> {
27
27
  * The function get's called once with the current value and then when the
28
28
  * values changes
29
29
  *
30
- * ## Note
30
+ * #### Note
31
31
  * This does not check for equality like svelte.
32
32
  *
33
33
  * @return a function which should be called to unsubscribe
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * ## Unstable
5
+ * The translations module is not yet stable. APIs may change without a major version bump.
6
+ */
7
+
8
+ import { createFileLoader } from './loader/fileLoader/fileLoader.js';
9
+ import { createGlobalLoader } from './loader/GlobalLoader.js';
10
+ import { type TranslationsLoader, type LoaderCreator } from './loader/index.js';
11
+ import loadTranslations from './loadTranslations.js';
12
+ import {
13
+ createTranslations,
14
+ getTranslationsPlugin,
15
+ type TranslateFunction,
16
+ type TranslateStore,
17
+ type Translations,
18
+ TranslationsPlugin,
19
+ type TranslationsPluginOptions,
20
+ } from './translationsPlugin.js';
21
+
22
+ /**
23
+ * Creates a translate store for the given namespace.
24
+ *
25
+ * #### Example
26
+ * ```svelte
27
+ * <script>
28
+ * import { getTranslations } from 'crelte/translations';
29
+ *
30
+ * const t = getTranslations();
31
+ * </script>
32
+ *
33
+ * <h1>{$t('welcome_message')}</h1>
34
+ * ```
35
+ */
36
+ function getTranslations(namespace: string = 'common'): TranslateStore {
37
+ const plugin = getTranslationsPlugin();
38
+ return plugin.z_createTranslateStore(namespace);
39
+ }
40
+
41
+ export {
42
+ createTranslations,
43
+ TranslationsPluginOptions,
44
+ loadTranslations,
45
+ getTranslations,
46
+ getTranslationsPlugin,
47
+ Translations,
48
+ TranslationsPlugin,
49
+ TranslationsLoader,
50
+ LoaderCreator,
51
+ TranslateStore,
52
+ TranslateFunction,
53
+ // loaders
54
+ createFileLoader,
55
+ createGlobalLoader,
56
+ };
@@ -0,0 +1,33 @@
1
+ import { CrelteRequest } from '../crelte.js';
2
+ import { getTranslationsPlugin, TranslateStore } from './translationsPlugin.js';
3
+
4
+ /**
5
+ * Creates a translate store and loads the specified namespace.
6
+ *
7
+ * #### Example
8
+ * ```svelte
9
+ * <script module>
10
+ * import { loadTranslations } from 'crelte/translations';
11
+ * export const loadData = {
12
+ * t: cr => loadTranslations(cr, 'customNamespace')
13
+ * };
14
+ * </script>
15
+ *
16
+ * <script>
17
+ * let { t } = $props();
18
+ * </script>
19
+ *
20
+ * <h1>{$t('welcome_message')}</h1>
21
+ * ```
22
+ */
23
+ export default async function loadTranslations(
24
+ cr: CrelteRequest,
25
+ namespace: string,
26
+ ): Promise<TranslateStore> {
27
+ const plugin = getTranslationsPlugin(cr);
28
+
29
+ // we don't need the return value here as it's cached
30
+ await plugin.load(cr, namespace);
31
+
32
+ return plugin.z_createTranslateStore(namespace);
33
+ }
@@ -0,0 +1,62 @@
1
+ import { Translations } from '../translationsPlugin.js';
2
+ import { Crelte, CrelteRequest } from '../../crelte.js';
3
+ import { LoaderCreator, TranslationsLoader } from '../index.js';
4
+
5
+ export default class GlobalLoader implements TranslationsLoader {
6
+ private handle: string;
7
+
8
+ constructor(_crelte: Crelte, opts: { handle?: string }) {
9
+ this.handle = opts.handle ?? 'translations';
10
+ }
11
+
12
+ async load(cr: CrelteRequest, namespace: string): Promise<Translations> {
13
+ const globalSet = await cr.globals.getAsync(this.handle);
14
+ if (!globalSet || typeof globalSet !== 'object')
15
+ throw new Error(`missing globals \`${this.handle}\``);
16
+
17
+ const data = globalSet[namespace];
18
+ if (!data)
19
+ throw new Error(
20
+ `could not find \`${namespace}\` in globals \`${this.handle}\``,
21
+ );
22
+
23
+ if (typeof data !== 'string') return data;
24
+
25
+ try {
26
+ return JSON.parse(data);
27
+ } catch (e) {
28
+ throw new Error(
29
+ `could not parse \`${this.handle}.${namespace}\` as json \n\n` +
30
+ (e instanceof Error ? e.message : ''),
31
+ );
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Creates a loader that loads translations from a global set.
38
+ *
39
+ * ```ts
40
+ * import { createTranslations, createGlobalLoader } from 'crelte/translations';
41
+ *
42
+ * createTranslations({ loader: createGlobalLoader() });
43
+ * ```
44
+ *
45
+ * Then in you're global export a `translations` globalSet with namespaces
46
+ * as fields, at least `common`.
47
+ *
48
+ * ```graphql
49
+ * translations: globalSet(handle: "translations", siteId: $siteId) {
50
+ * ... on translations_GlobalSet {
51
+ * common
52
+ * }
53
+ * }
54
+ */
55
+ export function createGlobalLoader(
56
+ opts: {
57
+ /** the handle for the global set containing the namespaces (default = translations) */
58
+ handle?: string;
59
+ } = {},
60
+ ): LoaderCreator {
61
+ return crelte => new GlobalLoader(crelte, opts);
62
+ }
@@ -0,0 +1,40 @@
1
+ import { CrelteRequest } from '../../../crelte.js';
2
+ import SsrCache from '../../../ssr/SsrCache.js';
3
+ import { Translations } from '../../translationsPlugin.js';
4
+ import { TranslationsLoader } from '../index.js';
5
+ import { SSR_RUN_NUMBER_KEY, translationFilePath } from './fileLoader.js';
6
+
7
+ export default class ClientFileLoader implements TranslationsLoader {
8
+ private readonly runNumber: string;
9
+
10
+ constructor(cache: SsrCache) {
11
+ this.runNumber = cache.get(SSR_RUN_NUMBER_KEY) ?? '';
12
+ }
13
+
14
+ async load(cr: CrelteRequest, namespace: string): Promise<Translations> {
15
+ const lang = cr.site.language;
16
+
17
+ try {
18
+ const resp = await fetch(
19
+ `${translationFilePath(lang, namespace)}?run=${this.runNumber}`,
20
+ );
21
+
22
+ if (!resp.ok) {
23
+ throw new Error(
24
+ `Failed to fetch translations file for ${lang}/${namespace}`,
25
+ );
26
+ }
27
+
28
+ // todo: validate data
29
+ const data: Translations = await resp.json();
30
+
31
+ return data;
32
+ } catch (e: unknown) {
33
+ console.error(e);
34
+ throw new Error(
35
+ 'There is something wrong with your translations file. \n\n' +
36
+ (e instanceof Error ? e.message : ''),
37
+ );
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs/promises';
2
+ import { SSR_RUN_NUMBER_KEY, translationFilePath } from './fileLoader.js';
3
+ import { randomToken } from '../../utils.js';
4
+ import { Translations } from '../../translationsPlugin.js';
5
+ import SsrCache from '../../../ssr/SsrCache.js';
6
+ import { CrelteRequest } from '../../../crelte.js';
7
+ import { TranslationsLoader } from '../index.js';
8
+
9
+ export default class NodeFileLoader implements TranslationsLoader {
10
+ constructor(cache: SsrCache) {
11
+ // store RUN_NUMBER in ssr store
12
+ cache.set(SSR_RUN_NUMBER_KEY, randomToken());
13
+ }
14
+
15
+ async load(cr: CrelteRequest, namespace: string): Promise<Translations> {
16
+ const lang = cr.site.language;
17
+
18
+ try {
19
+ const fileString = await fs.readFile(
20
+ `./public${translationFilePath(lang, namespace)}`,
21
+ 'utf-8',
22
+ );
23
+ // todo: validate data
24
+ return JSON.parse(fileString) as Translations;
25
+ } catch (e: unknown) {
26
+ throw new Error(
27
+ 'There is something wrong with your translations file. \n\n' +
28
+ (e instanceof Error ? e.message : ''),
29
+ );
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ import { LoaderCreator } from '../index.js';
2
+ import ClientFileLoader from './ClientFileLoader.js';
3
+ import NodeFileLoader from './NodeFileLoader.js';
4
+
5
+ export const SSR_RUN_NUMBER_KEY = 'st-run-number';
6
+
7
+ /// Returns a path to the translations file for the given language and namespace.
8
+ /// Always prefixed with a slash.
9
+ export function translationFilePath(lang: string, namespace: string): string {
10
+ return `/translations/${lang}/${namespace}.json`;
11
+ }
12
+
13
+ export function createFileLoader(): LoaderCreator {
14
+ if (import.meta.env.SSR) {
15
+ return crelte => new NodeFileLoader(crelte.ssrCache);
16
+ } else {
17
+ return crelte => new ClientFileLoader(crelte.ssrCache);
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ import { Translations } from '../translationsPlugin.js';
2
+ import { Crelte, CrelteRequest } from '../../crelte.js';
3
+
4
+ export interface TranslationsLoader {
5
+ load(cr: CrelteRequest, namespace: string): Promise<Translations>;
6
+ }
7
+
8
+ export type LoaderCreator = (crelte: Crelte) => TranslationsLoader;
@@ -0,0 +1,145 @@
1
+ import { derived } from 'svelte/store';
2
+ import { LoaderCreator, TranslationsLoader } from './loader/index.js';
3
+ import { Plugin, PluginCreator } from '../plugins/Plugins.js';
4
+ import { Crelte, CrelteRequest } from '../crelte.js';
5
+ import Readable from '../std/stores/Readable.js';
6
+ import { getCrelte } from '../index.js';
7
+
8
+ export type TranslateFunction = (
9
+ key: string,
10
+ // replacements?: Record<string, string>,
11
+ ) => string;
12
+ export type TranslateStore = Readable<TranslateFunction>;
13
+ export type Translations = Record<string, string>;
14
+ export type TranslationsPluginOptions = {
15
+ /** use either `createFileLoader()` or `createGlobalLoader()` */
16
+ loader: LoaderCreator;
17
+ /** The common namespace is always loaded */
18
+ loadNamespaces?: string[];
19
+ };
20
+
21
+ const SSR_KEY = '_translations';
22
+
23
+ export class TranslationsPlugin implements Plugin {
24
+ private crelte: Crelte;
25
+ /// Map<`lang-namespace`, Translations>
26
+ private translations: Map<string, Translations>;
27
+ private loader: TranslationsLoader;
28
+ /// When the derived of site is used in the first loadData site is not defined
29
+ /// so we need to store the first language here
30
+ private firstLang: string | null;
31
+
32
+ get name(): string {
33
+ return 'translations';
34
+ }
35
+
36
+ constructor(crelte: Crelte, opts: TranslationsPluginOptions) {
37
+ this.crelte = crelte;
38
+ this.translations = new Map(crelte.ssrCache.get(SSR_KEY));
39
+ this.firstLang = null;
40
+
41
+ if (import.meta.env.DEV && typeof opts?.loader !== 'function') {
42
+ throw new Error(
43
+ 'TranslationsPlugin requires a loader, use `createFileLoader()` or ' +
44
+ '`createGlobalLoader()`',
45
+ );
46
+ }
47
+
48
+ this.loader = opts.loader(crelte);
49
+ const loadNamespaces = opts.loadNamespaces ?? [];
50
+
51
+ // preload default translations
52
+ crelte.events.on('loadGlobalData', async (cr: CrelteRequest) => {
53
+ this.firstLang = cr.site.language;
54
+ await Promise.all([
55
+ this.load(cr, 'common'),
56
+ ...loadNamespaces.map(ns => this.load(cr, ns)),
57
+ ]);
58
+ });
59
+ }
60
+
61
+ /**
62
+ * load needs to take a CrelteRequest to be able to get the correct site
63
+ * and globals
64
+ *
65
+ * in crelte 0.5 this should also be possible without a CrelteRequest
66
+ * but with multiple plugin instances / Request State plugins
67
+ */
68
+ async load(cr: CrelteRequest, namespace: string): Promise<Translations> {
69
+ const lang = cr.site.language;
70
+ const key = `${lang}-${namespace}`;
71
+ const tData = this.translations.get(key);
72
+ if (tData) return tData;
73
+
74
+ const data = await this.loader.load(cr, namespace);
75
+ this.translations.set(key, data);
76
+ if (import.meta.env.SSR) {
77
+ // only store the translations in the ssrCache during ssr
78
+ // since after that we wont read it again
79
+ cr.ssrCache.set(SSR_KEY, Array.from(this.translations.entries()));
80
+ }
81
+
82
+ return data;
83
+ }
84
+
85
+ get(lang: string, namespace: string): Translations | null {
86
+ return this.translations.get(`${lang}-${namespace}`) ?? null;
87
+ }
88
+
89
+ /** @hidden */
90
+ z_createTranslateStore(namespace: string): TranslateStore {
91
+ const store = derived(this.crelte.router.site, site => {
92
+ return (key: string) => {
93
+ const lang = site?.language ?? this.firstLang;
94
+ if (!lang) throw new Error('no lang');
95
+
96
+ const data = this.get(lang, namespace);
97
+ if (!data) console.error(`namespace '${namespace}' not loaded`);
98
+ return data?.[key] || key;
99
+ };
100
+ });
101
+
102
+ return new Readable(store);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Create the TranslationsPlugin
108
+ *
109
+ * #### Setup file loader
110
+ *
111
+ * ```ts
112
+ * import { createTranslations, createFileLoader } from 'crelte/translations';
113
+ *
114
+ * createTranslations({ loader: createFileLoader() });
115
+ * ```
116
+ *
117
+ * #### Setup global loader
118
+ * ```ts
119
+ * import { createTranslations, createGlobalLoader } from 'crelte/translations';
120
+ *
121
+ * createTranslations({ loader: createGlobalLoader() });
122
+ * ```
123
+ *
124
+ * Then in you're global export a `translations` globalSet with namespaces
125
+ * as fields, at least `common`.
126
+ *
127
+ * ```graphql
128
+ * translations: globalSet(handle: "translations", siteId: $siteId) {
129
+ * ... on translations_GlobalSet {
130
+ * common
131
+ * }
132
+ * }
133
+ * ```
134
+ */
135
+ export function createTranslations(
136
+ opts: TranslationsPluginOptions,
137
+ ): PluginCreator {
138
+ return crelte => new TranslationsPlugin(crelte, opts);
139
+ }
140
+
141
+ export function getTranslationsPlugin(
142
+ crelte: Crelte = getCrelte(),
143
+ ): TranslationsPlugin {
144
+ return crelte.getPlugin('translations') as TranslationsPlugin;
145
+ }
@@ -0,0 +1,17 @@
1
+ const ALPHABET: string =
2
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
3
+ const ALPHABET_LENGTH = ALPHABET.length;
4
+
5
+ /**
6
+ * Generates a random token of a specified length.
7
+ *
8
+ * @param length - The desired length of the token.
9
+ * @returns A random token.
10
+ */
11
+ export function randomToken(length: number = 8): string {
12
+ let s = '';
13
+ for (let i = 0; i < length; i++) {
14
+ s += ALPHABET[Math.floor(Math.random() * ALPHABET_LENGTH)];
15
+ }
16
+ return s;
17
+ }