svelte-intlayer 9.5.10 → 9.5.12

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.
@@ -31,14 +31,14 @@ const loadingProxy = () => new Proxy({ isLoading: true, error: null }, {
31
31
  },
32
32
  });
33
33
  /** Merges the resolved content with the loading/error state. */
34
- const withState = (value) => {
34
+ const withState = (value, isLoading = false) => {
35
35
  if (Array.isArray(value)) {
36
- return Object.assign(value.slice(), { isLoading: false, error: null });
36
+ return Object.assign(value.slice(), { isLoading, error: null });
37
37
  }
38
38
  if (value && typeof value === 'object') {
39
- return { ...value, isLoading: false, error: null };
39
+ return { ...value, isLoading, error: null };
40
40
  }
41
- return { isLoading: false, error: null };
41
+ return { isLoading, error: null };
42
42
  };
43
43
  /**
44
44
  * Svelte hook for dynamic dictionary loading.
@@ -70,7 +70,15 @@ export function useDictionaryDynamic(dictionaryPromise, key, localeOrSelector) {
70
70
  ? localeOrSelector.locale
71
71
  : localeOrSelector;
72
72
  const localeStore = derived(intlayerStore, ($store) => explicitLocale ?? context?.locale ?? $store.locale);
73
+ // Once content has resolved, a locale switch keeps it on screen (flagged as
74
+ // loading) until the new chunk lands. Falling back to `loadingProxy()` would
75
+ // blank every text node for a frame before rendering the target locale.
76
+ let resolvedContent;
73
77
  return derived(localeStore, ($locale, set) => {
78
+ const publish = (content) => {
79
+ resolvedContent = content;
80
+ set(withState(content));
81
+ };
74
82
  // A build-tool plugin may have resolved this locale's dictionary while
75
83
  // the entry point evaluated. Publishing it synchronously lets the store
76
84
  // open on real content — otherwise every dynamic dictionary starts on
@@ -78,11 +86,12 @@ export function useDictionaryDynamic(dictionaryPromise, key, localeOrSelector) {
78
86
  // each, even when nothing has to be fetched.
79
87
  const preloadedDictionary = getPreloadedDictionary(dictionaryPromise, $locale);
80
88
  if (preloadedDictionary) {
81
- set(withState(getDictionary(preloadedDictionary, $locale)));
89
+ publish(getDictionary(preloadedDictionary, $locale));
82
90
  return;
83
91
  }
84
- // Set loading state immediately with proxy
85
- set(loadingProxy());
92
+ set(resolvedContent === undefined
93
+ ? loadingProxy()
94
+ : withState(resolvedContent, true));
86
95
  let isCancelled = false;
87
96
  const load = async () => {
88
97
  try {
@@ -106,7 +115,7 @@ export function useDictionaryDynamic(dictionaryPromise, key, localeOrSelector) {
106
115
  }
107
116
  if (isCancelled)
108
117
  return;
109
- set(withState(resolved));
118
+ publish(resolved);
110
119
  }
111
120
  catch (error) {
112
121
  if (isCancelled)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { get } from 'svelte/store';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ vi.mock('@intlayer/config/built', () => {
4
+ const internationalization = { defaultLocale: 'en', locales: ['en', 'fr'] };
5
+ return { internationalization, default: { internationalization } };
6
+ });
7
+ vi.mock('./intlayerContext', () => ({
8
+ getIntlayerContext: () => undefined,
9
+ }));
10
+ // Interpretation is covered elsewhere: expose the raw content.
11
+ vi.mock('../getDictionary', () => ({
12
+ getDictionary: (dictionary) => dictionary.content,
13
+ }));
14
+ import { intlayerStore } from './intlayerStore';
15
+ import { useDictionaryDynamic } from './useDictionaryDynamic';
16
+ /** Loader map whose chunks resolve only when the test says so. */
17
+ const createDeferredLoaders = () => {
18
+ const resolvers = new Map();
19
+ const createLoader = (locale) => () => new Promise((resolve) => {
20
+ resolvers.set(locale, resolve);
21
+ });
22
+ return {
23
+ loaders: { en: createLoader('en'), fr: createLoader('fr') },
24
+ resolve: (locale, title) => resolvers.get(locale)?.({ key: 'message', content: { title } }),
25
+ };
26
+ };
27
+ const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
28
+ describe('useDictionaryDynamic', () => {
29
+ afterEach(() => {
30
+ intlayerStore.reset();
31
+ });
32
+ it('keeps the resolved content, flagged as loading, while the next locale loads', async () => {
33
+ const { loaders, resolve } = createDeferredLoaders();
34
+ const content = useDictionaryDynamic(loaders, 'message');
35
+ const unsubscribe = content.subscribe(() => { });
36
+ expect(get(content).isLoading).toBe(true);
37
+ resolve('en', 'Hello');
38
+ await flushPromises();
39
+ expect(get(content)).toMatchObject({ title: 'Hello', isLoading: false });
40
+ intlayerStore.setLocale('fr');
41
+ expect(get(content)).toMatchObject({ title: 'Hello', isLoading: true });
42
+ resolve('fr', 'Bonjour');
43
+ await flushPromises();
44
+ expect(get(content)).toMatchObject({ title: 'Bonjour', isLoading: false });
45
+ unsubscribe();
46
+ });
47
+ });
package/dist/plugins.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { editor, internationalization } from '@intlayer/config/built';
2
- import { conditionPlugin, enumerationPlugin, fallbackPlugin, filePlugin, genderPlugin, isInterpolableWrapperNode, nestedPlugin, pluralPlugin, resolveInsertedSelector, selectPlugin, transformInterpolableNode, translationPlugin, } from '@intlayer/core/interpreter';
2
+ import { conditionPlugin, enumerationPlugin, fallbackPlugin, filePlugin, genderPlugin, isInterpolableWrapperNode, nestedPlugin, pluralPlugin, resolveInsertedSelector, resolveInterpreterLocale, selectPlugin, transformInterpolableNode, translationPlugin, } from '@intlayer/core/interpreter';
3
3
  import * as NodeTypes from '@intlayer/types/nodeType';
4
4
  import { reportExposure } from './analytics/exposureSink';
5
5
  import { default as ContentSelector } from './editor/ContentSelector.svelte';
@@ -321,7 +321,7 @@ const pluginsCache = new Map();
321
321
  * This function is used by both getIntlayer and getDictionary to ensure consistent plugin configuration.
322
322
  */
323
323
  export const getPlugins = (locale, fallback = true) => {
324
- const currentLocale = locale ?? internationalization.defaultLocale;
324
+ const currentLocale = resolveInterpreterLocale(locale);
325
325
  const cacheKey = `${currentLocale}_${fallback}`;
326
326
  if (pluginsCache.has(cacheKey)) {
327
327
  return pluginsCache.get(cacheKey);
@@ -329,11 +329,11 @@ export const getPlugins = (locale, fallback = true) => {
329
329
  const plugins = [
330
330
  // First: most nodes are plain strings, which every other plugin rejects
331
331
  intlayerNodePlugins,
332
- translationPlugin(locale ?? internationalization.defaultLocale, fallback ? internationalization.defaultLocale : undefined),
332
+ translationPlugin(currentLocale, fallback ? internationalization.defaultLocale : undefined),
333
333
  enumerationPlugin,
334
- pluralPlugin(locale ?? internationalization.defaultLocale),
334
+ pluralPlugin(currentLocale),
335
335
  conditionPlugin,
336
- nestedPlugin(locale ?? internationalization.defaultLocale),
336
+ nestedPlugin(currentLocale),
337
337
  filePlugin,
338
338
  genderPlugin,
339
339
  selectPlugin,
@@ -1,13 +1,22 @@
1
+ import type { ResolvedEditor } from '@intlayer/types/module_augmentation';
1
2
  type IntlayerNodeProps = {
2
3
  value: any;
3
4
  component: any;
4
5
  props: Record<string, any>;
5
6
  additionalProps?: Record<string, any>;
6
7
  };
7
- export type IntlayerNode<T, AdditionalProps = Record<string, any>> = {
8
+ type IntlayerNodeComponent<T> = {
8
9
  new (...args: any[]): any;
9
10
  (anchor: any, props: any): any;
10
11
  value: T;
11
- } & AdditionalProps & T;
12
+ };
13
+ /**
14
+ * Content node returned by `useIntlayer`: a component (`<Title />`).
15
+ *
16
+ * Without the editor it is also typed as its value, so it can be used wherever
17
+ * a string is expected. With the editor, it renders the editable selector, so
18
+ * string usages must go through `.value`.
19
+ */
20
+ export type IntlayerNode<T, AdditionalProps = Record<string, any>> = ResolvedEditor<IntlayerNodeComponent<T> & AdditionalProps & T, IntlayerNodeComponent<T> & AdditionalProps>;
12
21
  export declare const renderIntlayerNode: <T, AdditionalProps = Record<string, any>>(args: IntlayerNodeProps) => IntlayerNode<T, AdditionalProps>;
13
22
  export {};
@@ -1,4 +1,3 @@
1
- // import type { ResolvedEditor } from '@intlayer/types/module_augmentation';
2
1
  import { getIntlayerNodePrototype } from '@intlayer/core/utils';
3
2
  import IntlayerNodeWrapper from './IntlayerNodeWrapper.svelte';
4
3
  export const renderIntlayerNode = (args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-intlayer",
3
- "version": "9.5.10",
3
+ "version": "9.5.12",
4
4
  "description": "Easily internationalize i18n your Svelte applications with type-safe multilingual content management.",
5
5
  "keywords": [
6
6
  "intlayer",
@@ -83,11 +83,11 @@
83
83
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
84
84
  },
85
85
  "dependencies": {
86
- "@intlayer/api": "9.5.10",
87
- "@intlayer/config": "9.5.10",
88
- "@intlayer/core": "9.5.10",
89
- "@intlayer/editor": "9.5.10",
90
- "@intlayer/types": "9.5.10"
86
+ "@intlayer/api": "9.5.12",
87
+ "@intlayer/config": "9.5.12",
88
+ "@intlayer/core": "9.5.12",
89
+ "@intlayer/editor": "9.5.12",
90
+ "@intlayer/types": "9.5.12"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@sveltejs/adapter-auto": "7.0.1",
@@ -109,7 +109,7 @@
109
109
  "svelte": ">=5.0.0"
110
110
  },
111
111
  "optionalDependencies": {
112
- "@intlayer/analytics": "9.5.10"
112
+ "@intlayer/analytics": "9.5.12"
113
113
  },
114
114
  "engines": {
115
115
  "node": ">=14.18"