diff --git a/packages/core/src/Controller/Cache/Cache.test.ts b/packages/core/src/Controller/Cache/Cache.test.ts new file mode 100644 index 0000000000..6949e4dee8 --- /dev/null +++ b/packages/core/src/Controller/Cache/Cache.test.ts @@ -0,0 +1,188 @@ +import { ListenerEvent } from '../../types'; +import { EventEmitterInstance } from '../Events/EventEmitter'; +import { ValueObserver } from '../ValueObserver'; +import { Cache, CacheInstance } from './Cache'; + +describe('cache', () => { + function createEventMock(): EventEmitterInstance> { + return { + emit: jest.fn(), + listen: jest.fn(), + }; + } + + let mockedBackendGetRecord: jest.Mock; + let mockedBackendGetDevRecord: jest.Mock; + + let onFetchingChange: jest.Mock; + let onLoadingChange: jest.Mock; + + let cache: CacheInstance; + + beforeEach(() => { + const mockedEvents = { + on: jest.fn(), + onCacheChange: createEventMock(), + onError: createEventMock(), + onFetchingChange: createEventMock(), + onInitialLoaded: createEventMock(), + onLanguageChange: createEventMock(), + onLoadingChange: createEventMock(), + onPendingLanguageChange: createEventMock(), + onPermanentChange: createEventMock(), + onRunningChange: createEventMock(), + onUpdate: createEventMock() as any, + setEmitterActive: jest.fn(), + }; + + mockedBackendGetRecord = jest.fn((args) => { + return Promise.resolve({ data: 'Prod', ...args }); + }); + mockedBackendGetDevRecord = jest.fn((args) => { + return Promise.resolve({ data: 'Dev', ...args }); + }); + + onFetchingChange = jest.fn(); + onLoadingChange = jest.fn(); + + const fetchingObserver = ValueObserver( + false, + () => cache.isFetching(), + onFetchingChange + ); + + const loadingObserver = ValueObserver( + false, + () => cache.isLoading('en'), + onLoadingChange + ); + + cache = Cache( + mockedEvents, + mockedBackendGetRecord, + mockedBackendGetDevRecord, + (descriptor) => ({ namespace: '', ...descriptor }), + () => false, + fetchingObserver, + loadingObserver + ); + }); + + it('fetches language with default namespace', async () => { + const result = await cache.loadRecords([{ language: 'en' }]); + expect(result[0].data).toEqual({ + language: 'en', + namespace: '', + data: 'Dev', + }); + }); + + it('fetches language with specified namespace', async () => { + const result = await cache.loadRecords([ + { language: 'en', namespace: 'test' }, + ]); + expect(result[0].data).toEqual({ + language: 'en', + namespace: 'test', + data: 'Dev', + }); + }); + + it('fetches language with specified namespace', async () => { + const result = await cache.loadRecords([ + { language: 'en', namespace: 'test' }, + ]); + expect(result[0].data).toEqual({ + language: 'en', + namespace: 'test', + data: 'Dev', + }); + }); + + it('uses cache when fetching twice the same thing', async () => { + await cache.loadRecords([{ language: 'en' }]); + expect(mockedBackendGetDevRecord).toBeCalledTimes(1); + const result = await cache.loadRecords([{ language: 'en' }], { + useCache: true, + }); + expect(mockedBackendGetDevRecord).toBeCalledTimes(1); + expect(result[0].data).toEqual({ + language: 'en', + namespace: '', + data: 'Dev', + }); + }); + + it('uses cache when fetching twice production data', async () => { + await cache.loadRecords([{ language: 'en' }], { noDev: true }); + expect(mockedBackendGetRecord).toBeCalledTimes(1); + const result = await cache.loadRecords([{ language: 'en' }], { + noDev: true, + useCache: true, + }); + expect(mockedBackendGetRecord).toBeCalledTimes(1); + expect(result[0].data).toEqual({ + language: 'en', + namespace: '', + data: 'Prod', + }); + }); + + it('does not use cache when `noCache` is on', async () => { + await cache.loadRecords([{ language: 'en' }]); + expect(mockedBackendGetDevRecord).toBeCalledTimes(1); + await cache.loadRecords([{ language: 'en' }]); + expect(mockedBackendGetDevRecord).toBeCalledTimes(2); + }); + + it('correctly returns combination of non-cached and cached', async () => { + await cache.loadRecords([{ language: 'en' }]); + expect(mockedBackendGetDevRecord).toBeCalledTimes(1); + const result = await cache.loadRecords( + [{ language: 'en' }, { language: 'en', namespace: 'new' }], + { useCache: true } + ); + expect(mockedBackendGetDevRecord).toBeCalledTimes(2); + expect(result).toEqual([ + { + cacheKey: 'en', + data: { data: 'Dev', language: 'en', namespace: '' }, + language: 'en', + namespace: '', + }, + { + cacheKey: 'en:new', + data: { data: 'Dev', language: 'en', namespace: 'new' }, + language: 'en', + namespace: 'new', + }, + ]); + }); + + it('correctly refetches dev data', async () => { + await cache.loadRecords([{ language: 'en' }], { noDev: true }); + expect(mockedBackendGetRecord).toBeCalledTimes(1); + expect(mockedBackendGetDevRecord).toBeCalledTimes(0); + cache.invalidate(); + const result = await cache.loadRecords([{ language: 'en' }]); + expect(mockedBackendGetRecord).toBeCalledTimes(1); + expect(mockedBackendGetDevRecord).toBeCalledTimes(1); + expect(result[0].data).toEqual({ + language: 'en', + namespace: '', + data: 'Dev', + }); + }); + + it('correctly notifies about fetching and loading', async () => { + expect(onLoadingChange).toBeCalledTimes(0); + expect(onFetchingChange).toBeCalledTimes(0); + await cache.loadRecords([{ language: 'en' }]); + expect(onLoadingChange).toBeCalledTimes(2); + expect(onFetchingChange).toBeCalledTimes(2); + cache.invalidate(); + await cache.loadRecords([{ language: 'en' }]); + expect(onFetchingChange).toBeCalledTimes(4); + expect(onLoadingChange).toBeCalledTimes(2); + }); +}); diff --git a/packages/core/src/Controller/Cache/Cache.ts b/packages/core/src/Controller/Cache/Cache.ts index 39ed478f03..007d164496 100644 --- a/packages/core/src/Controller/Cache/Cache.ts +++ b/packages/core/src/Controller/Cache/Cache.ts @@ -2,14 +2,16 @@ import { CacheDescriptor, CacheDescriptorInternal, NsFallback, - TranslationsFlat, TranslationValue, TreeTranslationsData, BackendGetRecordInternal, RecordFetchError, + LoadOptions, + CacheInternalRecord, + TranslationsFlat, } from '../../types'; import { getFallbackArray, isPromise, unique } from '../../helpers'; -import { TolgeeStaticData } from '../State/initState'; +import { TolgeeStaticData, TolgeeStaticDataProp } from '../State/initState'; import { ValueObserverInstance } from '../ValueObserver'; import { decodeCacheKey, encodeCacheKey, flattenTranslations } from './helpers'; @@ -43,7 +45,7 @@ export function Cache( function addRecordInternal( descriptor: CacheDescriptorInternal, - data: TreeTranslationsData, + data: TranslationsFlat, recordVersion: number ) { const cacheKey = encodeCacheKey(descriptor); @@ -51,7 +53,7 @@ export function Cache( data: flattenTranslations(data), version: recordVersion, }); - events.onCacheChange.emit(descriptor); + events.onCacheChange.emit(decodeCacheKey(cacheKey)); } /** @@ -108,15 +110,23 @@ export function Cache( } const self = Object.freeze({ - addStaticData(data: TolgeeStaticData | undefined) { - if (data) { + addStaticData(data: TolgeeStaticDataProp | undefined) { + if (Array.isArray(data)) { + for (const record of data) { + const key = encodeCacheKey(record); + const existing = cache.get(key); + if (!existing || existing.version === 0) { + addRecordInternal(record, flattenTranslations(record.data), 0); + } + } + } else if (data) { staticData = { ...staticData, ...data }; Object.entries(data).forEach(([key, value]) => { if (typeof value !== 'function') { const descriptor = decodeCacheKey(key); const existing = cache.get(key); if (!existing || existing.version === 0) { - addRecordInternal(descriptor, value, 0); + addRecordInternal(descriptor, flattenTranslations(value), 0); } } }); @@ -129,7 +139,7 @@ export function Cache( }, addRecord(descriptor: CacheDescriptorInternal, data: TreeTranslationsData) { - addRecordInternal(descriptor, data, version); + addRecordInternal(descriptor, flattenTranslations(data), version); }, exists(descriptor: CacheDescriptorInternal, strict = false) { @@ -140,20 +150,34 @@ export function Cache( return Boolean(record); }, - getRecord(descriptor: CacheDescriptor) { - return cache.get(encodeCacheKey(withDefaultNs(descriptor)))?.data; + getRecord(descriptor: CacheDescriptor): CacheInternalRecord | undefined { + const descriptorWithNs = withDefaultNs(descriptor); + const cacheKey = encodeCacheKey(descriptorWithNs); + const cacheRecord = cache.get(cacheKey); + if (!cacheRecord) { + return undefined; + } + return { + ...descriptorWithNs, + cacheKey, + data: cacheRecord.data, + }; + }, + + getAllRecords() { + const entries = Array.from(cache.entries()); + return entries.map(([key]) => self.getRecord(decodeCacheKey(key))); }, getTranslation(descriptor: CacheDescriptorInternal, key: string) { - return cache.get(encodeCacheKey(descriptor))?.data.get(key); + return cache.get(encodeCacheKey(descriptor))?.data[key]; }, getTranslationNs(namespaces: string[], languages: string[], key: string) { for (const namespace of namespaces) { for (const language of languages) { - const value = cache - .get(encodeCacheKey({ language, namespace })) - ?.data.get(key); + const value = cache.get(encodeCacheKey({ language, namespace })) + ?.data[key]; if (value !== undefined && value !== null) { return [namespace]; } @@ -169,9 +193,8 @@ export function Cache( ) { for (const namespace of namespaces) { for (const language of languages) { - const value = cache - .get(encodeCacheKey({ language, namespace })) - ?.data.get(key); + const value = cache.get(encodeCacheKey({ language, namespace })) + ?.data[key]; if (value !== undefined && value !== null) { return value; } @@ -186,8 +209,10 @@ export function Cache( value: TranslationValue ) { const record = cache.get(encodeCacheKey(descriptor))?.data; - record?.set(key, value); - events.onCacheChange.emit({ ...descriptor, key }); + if (record?.[key]) { + record[key] = value; + events.onCacheChange.emit({ ...descriptor, key }); + } }, isFetching(ns?: NsFallback) { @@ -206,84 +231,119 @@ export function Cache( ); }, - isLoading(language: string | undefined, ns?: NsFallback) { + isLoading(language: string, ns?: NsFallback) { const namespaces = getFallbackArray(ns); + if (isInitialLoading()) { + return true; + } + + const pendingCacheKeys = Array.from(asyncRequests.keys()); + return Boolean( - isInitialLoading() || - Array.from(asyncRequests.keys()).find((key) => { - const descriptor = decodeCacheKey(key); - return ( - (!namespaces.length || - namespaces.includes(descriptor.namespace)) && - !self.exists({ - namespace: descriptor.namespace, - language: language!, - }) - ); - }) + pendingCacheKeys.find((key) => { + const descriptor = decodeCacheKey(key); + return ( + (!namespaces.length || namespaces.includes(descriptor.namespace)) && + !self.exists({ + namespace: descriptor.namespace, + language: language, + }) + ); + }) ); }, - async loadRecords(descriptors: CacheDescriptor[], isDev: boolean) { - const withPromises = descriptors.map((descriptor) => { + async loadRecords( + descriptors: CacheDescriptor[], + options?: LoadOptions + ): Promise { + type WithPromise = { + new: boolean; + language: string; + namespace: string; + cacheKey: string; + promise?: Promise; + data?: TranslationsFlat; + }; + + const withPromises: WithPromise[] = descriptors.map((descriptor) => { const keyObject = withDefaultNs(descriptor); const cacheKey = encodeCacheKey(keyObject); + if (options?.useCache) { + const exists = self.exists(keyObject, true); + + if (exists) { + return { + ...keyObject, + new: false, + cacheKey, + data: self.getRecord(keyObject)!.data, + } as WithPromise; + } + } + const existingPromise = asyncRequests.get(cacheKey); if (existingPromise) { return { + ...keyObject, new: false, promise: existingPromise, - keyObject, cacheKey, }; } + const dataPromise = - fetchData(keyObject, isDev) || Promise.resolve(undefined); + fetchData(keyObject, !options?.noDev) || Promise.resolve(undefined); + asyncRequests.set(cacheKey, dataPromise); + return { + ...keyObject, new: true, promise: dataPromise, - keyObject, cacheKey, }; }); fetchingObserver.notify(); loadingObserver.notify(); - const results = await Promise.all(withPromises.map((val) => val.promise)); + const promisesToWait = withPromises + .map((val) => val.promise) + .filter(Boolean); - withPromises.forEach((value, i) => { - const promiseChanged = - asyncRequests.get(value.cacheKey) !== value.promise; + const fetchedData = await Promise.all(promisesToWait); + + withPromises.forEach((value) => { + if (value.promise) { + value.data = flattenTranslations(fetchedData[0] ?? {}); + fetchedData.shift(); + } // if promise has changed in between, it means cache been invalidated or // new data are being fetched + const promiseChanged = + asyncRequests.get(value.cacheKey) !== value.promise; if (value.new && !promiseChanged) { asyncRequests.delete(value.cacheKey); - const data = results[i]; - if (data) { - self.addRecord(value.keyObject, data); - } else if (!self.getRecord(value.keyObject)) { + if (value.data) { + self.addRecord(value, value.data); + } else if (!self.getRecord(value)) { // if no data exist, put empty object - self.addRecord(value.keyObject, {}); + // so we know we don't have to fetch again + self.addRecord(value, {}); } } }); fetchingObserver.notify(); loadingObserver.notify(); - return withPromises.map((val) => self.getRecord(val.keyObject)!); - }, - - getAllRecords() { - const entries = Array.from(cache.entries()); - return entries.map(([key, entry]) => { - return { - ...decodeCacheKey(key), - data: entry.data, - }; - }); + return withPromises.map((val) => ({ + language: val.language, + namespace: val.namespace, + data: val.data ?? {}, + cacheKey: val.cacheKey, + })); }, }); diff --git a/packages/core/src/Controller/Cache/helpers.ts b/packages/core/src/Controller/Cache/helpers.ts index 9188dcfe8e..a168175279 100644 --- a/packages/core/src/Controller/Cache/helpers.ts +++ b/packages/core/src/Controller/Cache/helpers.ts @@ -1,6 +1,10 @@ -import { CacheDescriptorInternal, TreeTranslationsData } from '../../types'; +import { + CacheDescriptorInternal, + TranslationsFlat, + TreeTranslationsData, +} from '../../types'; -export const flattenTranslations = ( +export const flattenTranslationsToMap = ( data: TreeTranslationsData ): Map => { const result: Map = new Map(); @@ -10,7 +14,7 @@ export const flattenTranslations = ( return; } if (typeof value === 'object') { - flattenTranslations(value).forEach((flatValue, flatKey) => { + flattenTranslationsToMap(value).forEach((flatValue, flatKey) => { result.set(key + '.' + flatKey, flatValue); }); return; @@ -20,6 +24,11 @@ export const flattenTranslations = ( return result; }; +export const flattenTranslations = ( + data: TreeTranslationsData +): TranslationsFlat => { + return Object.fromEntries(flattenTranslationsToMap(data).entries()); +}; export const decodeCacheKey = (key: string): CacheDescriptorInternal => { const [firstPart, ...rest] = key.split(':'); // if namespaces contains ":" it won't get lost diff --git a/packages/core/src/Controller/Controller.ts b/packages/core/src/Controller/Controller.ts index c4bfcf6fe8..e6afebded3 100644 --- a/packages/core/src/Controller/Controller.ts +++ b/packages/core/src/Controller/Controller.ts @@ -6,9 +6,14 @@ import { TFnType, NsType, KeyAndNamespacesInternal, + CacheDescriptorInternal, + LoadOptions, + LoadRequiredOptions, + LoadMatrixOptions, + MatrixOptions, } from '../types'; import { Cache } from './Cache/Cache'; -import { getFallbackArray } from '../helpers'; +import { getFallbackArray, unique } from '../helpers'; import { Plugins } from './Plugins/Plugins'; import { ValueObserver } from './ValueObserver'; import { State } from './State/State'; @@ -20,7 +25,7 @@ type StateServiceProps = { }; export function Controller({ options }: StateServiceProps) { - const events = Events(getFallbackNs, getDefaultNs); + const events = Events(); const fetchingObserver = ValueObserver( false, () => cache.isFetching(), @@ -82,16 +87,16 @@ export function Controller({ options }: StateServiceProps) { // gets all namespaces where translation could be located // takes (ns|default, fallback ns) function getDefaultAndFallbackNs(ns?: NsType) { - return [...getFallbackArray(getDefaultNs(ns)), ...getFallbackNs()]; + return unique([...getFallbackArray(getDefaultNs(ns)), ...getFallbackNs()]); } // gets all namespaces which need to be loaded // takes (ns|default, initial ns, fallback ns, active ns) - function getRequiredNamespaces(ns: NsFallback) { - return [ + function getRequiredNamespaces(ns?: NsFallback) { + return unique([ ...getFallbackArray(ns ?? getDefaultNs()), ...state.getRequiredNamespaces(), - ]; + ]); } function changeTranslation( @@ -114,25 +119,55 @@ export function Controller({ options }: StateServiceProps) { cache.addStaticData(state.getInitialOptions().staticData); } - function getRequiredRecords(lang?: string, ns?: NsFallback) { + function getRequiredDescriptors(lang?: string, ns?: NsFallback) { const languages = state.getFallbackLangs(lang); const namespaces = getRequiredNamespaces(ns); - const result: CacheDescriptor[] = []; + const result: CacheDescriptorInternal[] = []; languages.forEach((language) => { namespaces.forEach((namespace) => { - if (!cache.exists({ language, namespace }, true)) { - result.push({ language, namespace }); - } + result.push({ language, namespace }); }); }); return result; } - function loadRequiredRecords(lang?: string, ns?: NsFallback) { - const descriptors = getRequiredRecords(lang, ns); - if (descriptors.length) { - return valueOrPromise(self.loadRecords(descriptors), () => {}); + function getMissingDescriptors(lang?: string, ns?: NsFallback) { + return getRequiredDescriptors(lang, ns).filter( + (descriptor) => !cache.exists(descriptor, true) + ); + } + + function getMatrixRecords(options: MatrixOptions) { + let languages: string[] = []; + let namespaces: string[] = []; + if (Array.isArray(options.languages)) { + languages = options.languages; + } else if (options.languages === 'all') { + const availableLanguages = self.getAvailableLanguages(); + if (!availableLanguages) { + throw new Error(missingOptionError('availableLanguages')); + } + languages = availableLanguages; + } + + if (Array.isArray(options.namespaces)) { + namespaces = options.namespaces; + } else if (options.namespaces === 'all') { + const availableNs = self.getAvailableNs(); + if (!availableNs) { + throw new Error(missingOptionError('availableNs')); + } + namespaces = availableNs; } + + const records: CacheDescriptorInternal[] = []; + + languages.forEach((language) => { + namespaces.forEach((namespace) => { + records.push({ language, namespace }); + }); + }); + return records; } function getTranslationNs({ key, ns }: KeyAndNamespacesInternal) { @@ -149,8 +184,13 @@ export function Controller({ options }: StateServiceProps) { function loadInitial() { const data = valueOrPromise(initializeLanguage(), () => { - // fail if there is no language - return loadRequiredRecords(); + const missingDescriptors = getMissingDescriptors(); + if ( + missingDescriptors.length && + state.getInitialOptions().autoLoadRequiredData + ) { + return cache.loadRecords(missingDescriptors, { useCache: true }); + } }); if (isPromise(data)) { @@ -208,7 +248,7 @@ export function Controller({ options }: StateServiceProps) { getTranslationNs: getTranslationNs, getDefaultAndFallbackNs: getDefaultAndFallbackNs, findPositions: pluginService.findPositions, - getRequiredRecords: getRequiredRecords, + getRequiredDescriptors: getRequiredDescriptors, async changeLanguage(language: string) { if ( state.getPendingLanguage() === language && @@ -218,8 +258,10 @@ export function Controller({ options }: StateServiceProps) { } state.setPendingLanguage(language); - if (state.isRunning()) { - await loadRequiredRecords(language); + if (state.isRunning() && state.getInitialOptions().autoLoadRequiredData) { + await cache.loadRecords(getRequiredDescriptors(language), { + useCache: true, + }); } if (language === state.getPendingLanguage()) { @@ -235,16 +277,14 @@ export function Controller({ options }: StateServiceProps) { state.addActiveNs(ns); } if (state.isRunning()) { - await loadRequiredRecords(undefined, ns); + await cache.loadRecords(getRequiredDescriptors(undefined, ns), { + useCache: true, + }); } }, - loadRecords(descriptors: CacheDescriptor[]) { - return cache.loadRecords(descriptors, self.isDev()); - }, - - async loadRecord(descriptor: CacheDescriptor) { - return (await self.loadRecords([descriptor]))[0]; + async loadRecord(descriptor: CacheDescriptor, options?: LoadOptions) { + return (await self.loadRecords([descriptor], options))[0]?.data; }, isLoading(ns?: NsFallback) { @@ -282,6 +322,19 @@ export function Controller({ options }: StateServiceProps) { ); }, + async loadRequired(options?: LoadRequiredOptions) { + if (!options?.language) { + await initializeLanguage(); + } + const requiredRecords = getRequiredDescriptors(options?.language); + return self.loadRecords(requiredRecords, options); + }, + + async loadMatrix(options: LoadMatrixOptions) { + const records = getMatrixRecords(options); + return self.loadRecords(records, options); + }, + run() { checkCorrectConfiguration(); if (!state.isRunning()) { diff --git a/packages/core/src/Controller/Events/EventEmitter.ts b/packages/core/src/Controller/Events/EventEmitter.ts index 11323472c9..24c4b50859 100644 --- a/packages/core/src/Controller/Events/EventEmitter.ts +++ b/packages/core/src/Controller/Events/EventEmitter.ts @@ -1,13 +1,14 @@ -import { Subscription, Listener } from '../../types'; +import { Subscription, Handler, ListenerEvent } from '../../types'; -export function EventEmitter( +export const EventEmitter = >( + type: Event['type'], isActive: () => boolean -): EventEmitterInstance { - let handlers: Listener[] = []; +): EventEmitterInstance => { + let handlers: Handler[] = []; - return Object.freeze({ - listen(handler: Listener): Subscription { - const handlerWrapper: Listener = (e) => { + return { + listen(handler: (e: Event) => void): Subscription { + const handlerWrapper: Handler = (e) => { handler(e); }; @@ -19,15 +20,22 @@ export function EventEmitter( }, }; }, - emit(data: T) { + emit(data: Event['value']) { if (isActive()) { - handlers.forEach((handler) => handler({ value: data })); + handlers.forEach((handler) => + handler({ type: type, value: data } as Event) + ); } }, - }); -} - -export type EventEmitterInstance = { - readonly listen: (handler: Listener) => Subscription; - readonly emit: (data: T) => void; + } as unknown as EventEmitterInstance; }; + +export type EventEmitterInstance = + Event extends ListenerEvent + ? { + readonly listen: ( + handler: Handler> + ) => Subscription; + readonly emit: (data: T) => void; + } + : never; diff --git a/packages/core/src/Controller/Events/EventEmitterCombined.ts b/packages/core/src/Controller/Events/EventEmitterCombined.ts new file mode 100644 index 0000000000..c95a275b4d --- /dev/null +++ b/packages/core/src/Controller/Events/EventEmitterCombined.ts @@ -0,0 +1,55 @@ +import { Subscription, ListenerEvent, CombinedHandler } from '../../types'; + +export function EventEmitterCombined>( + isActive: () => boolean +): EventEmitterCombinedInstance { + let handlers: CombinedHandler[] = []; + + let queue: E[] = []; + + // merge events in queue into one event + function solveQueue() { + if (queue.length === 0) { + return; + } + const queueCopy = queue; + queue = []; + handlers.forEach((handler) => { + handler(queueCopy); + }); + } + + return Object.freeze({ + listen(handler: (e: E[]) => void): Subscription { + const handlerWrapper: CombinedHandler = (events) => { + handler(events); + }; + + handlers.push(handlerWrapper); + + return { + unsubscribe() { + handlers = handlers.filter((i) => handlerWrapper !== i); + }, + }; + }, + emit(e: E, delayed: boolean) { + if (isActive()) { + if (isActive()) { + queue.push(e); + if (!delayed) { + solveQueue(); + } else { + setTimeout(solveQueue, 0); + } + } + } + }, + }); +} + +export type EventEmitterCombinedInstance> = + { + readonly listen: (handler: CombinedHandler) => Subscription; + readonly emit: (e: E, delayed: boolean) => void; + }; diff --git a/packages/core/src/Controller/Events/EventEmitterSelective.test.ts b/packages/core/src/Controller/Events/EventEmitterSelective.test.ts deleted file mode 100644 index 9030c57ba6..0000000000 --- a/packages/core/src/Controller/Events/EventEmitterSelective.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { EventEmitterSelective } from './EventEmitterSelective'; - -describe('event emitter selective', () => { - it('handles correctly default namespace', () => { - const emitter = EventEmitterSelective( - () => true, - () => [], - () => 'default' - ); - const handler = jest.fn(); - const listener = emitter.listenSome(handler); - - // subscribe to default ns - listener.subscribeNs(); - - // emmit - emitter.emit(['default']); - // should be ignored - emitter.emit(['c']); - - expect(handler).toBeCalledTimes(1); - }); - - it('unsubscribes', () => { - const emitter = EventEmitterSelective( - () => true, - () => [], - () => '' - ); - const handler = jest.fn(); - const listener = emitter.listen(handler); - - emitter.emit(); - - listener.unsubscribe(); - emitter.emit(); - expect(handler).toBeCalledTimes(1); - }); - - it('groups events correctly', async () => { - const emitter = EventEmitterSelective( - () => true, - () => ['test', 'opqrst'], - () => '' - ); - const handler = jest.fn(); - const hanlderAll = jest.fn(); - const listener = emitter.listenSome(handler); - const listenerAll = emitter.listen(hanlderAll); - - listener.subscribeNs('test'); - - // is fallback should always call handler - emitter.emit(['opqrst'], true); - - await new Promise((resolve) => setTimeout(resolve)); - - expect(hanlderAll).toBeCalledTimes(1); - expect(handler).toBeCalledTimes(1); - - // these should be merged together - emitter.emit(['abcd'], true); - emitter.emit(['abcd']); - - expect(hanlderAll).toBeCalledTimes(2); - expect(handler).toBeCalledTimes(1); - - listener.unsubscribe(); - listenerAll.unsubscribe(); - emitter.emit(); - }); - - it('always subscribes to fallback ns', async () => { - const emitter = EventEmitterSelective( - () => true, - () => ['fallback1', 'fallback2'], - () => '' - ); - const handler = jest.fn(); - emitter.listenSome(handler); - - emitter.emit(['fallback1']); - expect(handler).toBeCalledTimes(1); - - emitter.emit(['fallback2']); - expect(handler).toBeCalledTimes(2); - - emitter.emit(['test']); - expect(handler).toBeCalledTimes(2); - }); - - it('switches off emitting', () => { - const emitter = EventEmitterSelective( - () => false, - () => ['fallback1', 'fallback2'], - () => '' - ); - const handler = jest.fn(); - emitter.listenSome(handler); - - emitter.emit(['fallback1']); - expect(handler).toBeCalledTimes(0); - - emitter.emit(['fallback2']); - expect(handler).toBeCalledTimes(0); - - emitter.emit(['']); - expect(handler).toBeCalledTimes(0); - }); -}); diff --git a/packages/core/src/Controller/Events/EventEmitterSelective.ts b/packages/core/src/Controller/Events/EventEmitterSelective.ts deleted file mode 100644 index 84d197128b..0000000000 --- a/packages/core/src/Controller/Events/EventEmitterSelective.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { getFallbackArray } from '../../helpers'; -import { - NsFallback, - Subscription, - Listener, - ListenerEvent, - SubscriptionSelective, - NsType, -} from '../../types'; - -type NsListType = string; - -type HandlerWrapperType = { - fn: Listener; - namespaces: Set; -}; - -export function EventEmitterSelective( - isActive: () => boolean, - getFallbackNs: () => string[], - getDefaultNs: () => string -): EventEmitterSelectiveInstance { - const listeners: Set> = new Set(); - const partialListeners: Set = new Set(); - - function callHandlers(ns: Array | undefined) { - // everything is implicitly subscribed to fallbacks - // as it can always fall through to it - const fallbackNamespaces = new Set(getFallbackNs()); - - partialListeners.forEach((handler) => { - const nsMatches = - ns === undefined || - ns?.findIndex( - (ns) => fallbackNamespaces.has(ns) || handler.namespaces.has(ns!) - ) !== -1; - - if (nsMatches) { - handler.fn({ value: undefined as any }); - } - }); - } - - let queue: (string[] | undefined)[] = []; - - // merge events in queue into one event - function solveQueue() { - if (queue.length === 0) { - return; - } - const queueCopy = queue; - queue = []; - - listeners.forEach((handler) => { - handler({ value: undefined as any }); - }); - - let namespaces: Set | undefined = new Set(); - - queueCopy.forEach((ns) => { - if (ns === undefined) { - // when no ns specified, it affects all namespaces - namespaces = undefined; - } else if (namespaces !== undefined) { - ns.forEach((ns) => namespaces!.add(ns)); - } - }); - - const namespacesArray = namespaces - ? Array.from(namespaces.keys()) - : undefined; - - callHandlers(namespacesArray); - } - - return Object.freeze({ - emit(ns?: string[], delayed?: boolean) { - if (isActive()) { - queue.push(ns); - if (!delayed) { - solveQueue(); - } else { - setTimeout(solveQueue, 0); - } - } - }, - - listen(handler: Listener) { - listeners.add(handler); - const result = { - unsubscribe: () => { - listeners.delete(handler); - }, - }; - return result; - }, - - listenSome(handler: Listener) { - const handlerWrapper = { - fn: (e: ListenerEvent) => { - handler(e); - }, - namespaces: new Set(), - }; - - partialListeners.add(handlerWrapper); - - const result = { - unsubscribe: () => { - partialListeners.delete(handlerWrapper); - }, - subscribeNs: (ns: NsFallback) => { - getFallbackArray(ns).forEach((val) => - handlerWrapper.namespaces.add(val) - ); - if (ns === undefined) { - // subscribing to default ns - handlerWrapper.namespaces.add(getDefaultNs()); - } - return result; - }, - }; - - return result; - }, - }); -} - -export type EventEmitterSelectiveInstance = { - readonly listenSome: (handler: Listener) => SubscriptionSelective; - readonly listen: (handler: Listener) => Subscription; - readonly emit: (ns?: string[], delayed?: boolean) => void; -}; diff --git a/packages/core/src/Controller/Events/Events.ts b/packages/core/src/Controller/Events/Events.ts index 26f3bc4ac9..3bcbe966dc 100644 --- a/packages/core/src/Controller/Events/Events.ts +++ b/packages/core/src/Controller/Events/Events.ts @@ -1,16 +1,20 @@ import { EventEmitter } from './EventEmitter'; -import { EventEmitterSelective } from './EventEmitterSelective'; import { - CacheDescriptorWithKey, - TolgeeError, + CacheEvent, + FetchingEvent, + InitialLoadEvent, + LanguageEvent, + LoadingEvent, + PendingLanguageEvent, + PermanentChangeEvent, + RunningEvent, TolgeeOn, - TranslationDescriptor, + UpdateEvent, + ErrorEvent, } from '../../types'; +import { EventEmitterCombined } from './EventEmitterCombined'; -export function Events( - getFallbackNs: () => string[], - getDefaultNs: () => string -) { +export function Events() { let emitterActive = true; function isActive() { @@ -18,16 +22,22 @@ export function Events( } const self = Object.freeze({ - onPendingLanguageChange: EventEmitter(isActive), - onLanguageChange: EventEmitter(isActive), - onLoadingChange: EventEmitter(isActive), - onFetchingChange: EventEmitter(isActive), - onInitialLoaded: EventEmitter(isActive), - onRunningChange: EventEmitter(isActive), - onCacheChange: EventEmitter(isActive), - onUpdate: EventEmitterSelective(isActive, getFallbackNs, getDefaultNs), - onPermanentChange: EventEmitter(isActive), - onError: EventEmitter(isActive), + onPendingLanguageChange: EventEmitter( + 'pendingLanguage', + isActive + ), + onLanguageChange: EventEmitter('language', isActive), + onLoadingChange: EventEmitter('loading', isActive), + onFetchingChange: EventEmitter('fetching', isActive), + onInitialLoaded: EventEmitter('initialLoad', isActive), + onRunningChange: EventEmitter('running', isActive), + onCacheChange: EventEmitter('cache', isActive), + onPermanentChange: EventEmitter( + 'permanentChange', + isActive + ), + onError: EventEmitter('error', isActive), + onUpdate: EventEmitterCombined(isActive), setEmitterActive(active: boolean) { emitterActive = active; }, @@ -57,11 +67,9 @@ export function Events( }) as TolgeeOn, }); - self.onInitialLoaded.listen(() => self.onUpdate.emit()); - self.onLanguageChange.listen(() => self.onUpdate.emit()); - self.onCacheChange.listen(({ value }) => - self.onUpdate.emit([value.namespace], true) - ); + self.onInitialLoaded.listen((e) => self.onUpdate.emit(e, false)); + self.onLanguageChange.listen((e) => self.onUpdate.emit(e, false)); + self.onCacheChange.listen((e) => self.onUpdate.emit(e, true)); return self; } diff --git a/packages/core/src/Controller/Plugins/Plugins.ts b/packages/core/src/Controller/Plugins/Plugins.ts index 5671d42eda..d03a6489a1 100644 --- a/packages/core/src/Controller/Plugins/Plugins.ts +++ b/packages/core/src/Controller/Plugins/Plugins.ts @@ -267,6 +267,11 @@ export function Plugins( getBackendDevRecord: (async ({ language, namespace }) => { const { apiKey, apiUrl, projectId, filterTag } = getInitialOptions(); + + if (!apiKey || !apiUrl || !self.hasDevBackend()) { + return undefined; + } + return instances.devBackend?.getRecord({ apiKey, apiUrl, diff --git a/packages/core/src/Controller/State/State.ts b/packages/core/src/Controller/State/State.ts index ebe43084b8..1143912368 100644 --- a/packages/core/src/Controller/State/State.ts +++ b/packages/core/src/Controller/State/State.ts @@ -2,8 +2,11 @@ import { CacheDescriptor, CacheDescriptorInternal, DevCredentials, + LanguageEvent, NsFallback, NsType, + PendingLanguageEvent, + RunningEvent, } from '../../types'; import { decodeCacheKey } from '../Cache/helpers'; @@ -17,9 +20,9 @@ import { import { initState, TolgeeOptions } from './initState'; export function State( - onLanguageChange: EventEmitterInstance, - onPendingLanguageChange: EventEmitterInstance, - onRunningChange: EventEmitterInstance + onLanguageChange: EventEmitterInstance, + onPendingLanguageChange: EventEmitterInstance, + onRunningChange: EventEmitterInstance ) { let state = initState(); let devCredentials: DevCredentials = undefined; @@ -99,7 +102,8 @@ export function State( }, getRequiredNamespaces() { return unique([ - ...(state.initialOptions.ns || [state.initialOptions.defaultNs]), + self.getDefaultNs(), + ...(state.initialOptions.ns || []), ...getFallbackArray(state.initialOptions.fallbackNs), ...state.activeNamespaces.keys(), ]); @@ -123,8 +127,16 @@ export function State( return getFallbackArray(state.initialOptions.fallbackNs); }, + getNs() { + return state.initialOptions.ns?.length + ? state.initialOptions.ns + : [state.initialOptions.defaultNs ?? '']; + }, + getDefaultNs(ns?: NsType) { - return ns === undefined ? state.initialOptions.defaultNs : ns; + return ns === undefined + ? state.initialOptions.defaultNs ?? state.initialOptions.ns?.[0] ?? '' + : ns; }, getAvailableLanguages() { @@ -138,11 +150,15 @@ export function State( } }, + getAvailableNs() { + return state.initialOptions.availableNs; + }, + withDefaultNs(descriptor: CacheDescriptor): CacheDescriptorInternal { return { namespace: descriptor.namespace === undefined - ? self.getInitialOptions().defaultNs + ? self.getDefaultNs() : descriptor.namespace, language: descriptor.language, }; diff --git a/packages/core/src/Controller/State/initState.ts b/packages/core/src/Controller/State/initState.ts index f37c651b23..01b5e9470f 100644 --- a/packages/core/src/Controller/State/initState.ts +++ b/packages/core/src/Controller/State/initState.ts @@ -5,6 +5,7 @@ import { OnFormatError, FetchFn, MissingTranslationHandler, + CachePublicRecord, } from '../../types'; import { createFetchFunction, sanitizeUrl } from '../../helpers'; import { @@ -23,6 +24,8 @@ export type TolgeeStaticData = { [key: string]: TreeTranslationsData | (() => Promise); }; +export type TolgeeStaticDataProp = TolgeeStaticData | CachePublicRecord[]; + export type TolgeeOptionsInternal = { /** * Initial language @@ -50,8 +53,8 @@ export type TolgeeOptionsInternal = { defaultLanguage?: string; /** - * Languages which can be used for language detection - * and also limits which values can be stored + * Specify all available languages. Required for language detection or loading all languages at once (loadMatrix). + * It also limits which values can be stored. Is derrived from `staticData` keys if not provided. */ availableLanguages?: string[]; @@ -61,7 +64,7 @@ export type TolgeeOptionsInternal = { fallbackLanguage?: FallbackLanguageOption; /** - * Namespaces which should be always fetched + * Namespaces which should be always fetched (default: [defaultNs] or ['']) */ ns?: string[]; @@ -71,9 +74,14 @@ export type TolgeeOptionsInternal = { fallbackNs?: FallbackGeneral; /** - * Default namespace when no namespace defined (default: '') + * Default namespace when no namespace defined (default: first from `ns`) + */ + defaultNs?: string; + + /** + * Specify all available namespaces. Required for loading all namespaces at once (loadMatrix). */ - defaultNs: string; + availableNs?: string[]; /** * These data go directly to cache or you can specify async @@ -85,8 +93,17 @@ export type TolgeeOptionsInternal = { * 'language:namespace': * } * ``` + * + * You can also pass list of `CachePublicRecord`, which is in format: + * + * { + * 'language': , + * 'namespace': + * 'data': + * } + * */ - staticData?: TolgeeStaticData; + staticData?: TolgeeStaticDataProp; /** * Switches between invisible and text observer. (Default: invisible) @@ -124,6 +141,11 @@ export type TolgeeOptionsInternal = { * Use only keys tagged with one of the listed tags */ filterTag?: string[]; + + /** + * automatically load required records on `run` and `changeLanguage` (default: true) + */ + autoLoadRequiredData: boolean; }; export type TolgeeOptions = Partial< @@ -142,11 +164,11 @@ export type State = { }; const defaultValues: TolgeeOptionsInternal = { - defaultNs: '', observerOptions: defaultObserverOptions, observerType: 'invisible', onFormatError: DEFAULT_FORMAT_ERROR, apiUrl: DEFAULT_API_URL, + autoLoadRequiredData: true, fetch: createFetchFunction(), onTranslationMissing: DEFAULT_MISSING_TRANSLATION, }; diff --git a/packages/core/src/TolgeeCore.ts b/packages/core/src/TolgeeCore.ts index f1fbbb2c35..d8b95f3155 100644 --- a/packages/core/src/TolgeeCore.ts +++ b/packages/core/src/TolgeeCore.ts @@ -35,21 +35,6 @@ function createTolgee(options: TolgeeOptions) { */ on: controller.on, - /** - * Listen for specific namespaces changes. - * - * ``` - * const sub = tolgee.onUpdate(handler) - * - * // subscribe to selected namespace - * sub.subscribeNs(['common']) - * - * // unsubscribe - * sub.unsubscribe() - * ``` - */ - onNsUpdate: controller.onUpdate.listenSome, - /** * Turn off/on events emitting. Is on by default. */ @@ -94,6 +79,18 @@ function createTolgee(options: TolgeeOptions) { */ removeActiveNs: controller.removeActiveNs, + /** + * Load records which would be loaded by `run` function + * + * You can provide language if not previously set on tolgee instance + */ + loadRequired: controller.loadRequired, + + /** + * Load records in matrix (languages x namespaces) + */ + loadMatrix: controller.loadMatrix, + /** * Manually load multiple records from `Backend` (or `DevBackend` when in dev mode) * @@ -128,9 +125,9 @@ function createTolgee(options: TolgeeOptions) { isLoaded: controller.isLoaded, /** - * Returns records needed for instance to be `loaded` + * Returns descriptors of records needed for instance to be `loaded` */ - getRequiredRecords: controller.getRequiredRecords, + getRequiredDescriptors: controller.getRequiredDescriptors, /** * @return `true` if tolgee is loading initial data (triggered by `run`). diff --git a/packages/core/src/__test/cache.test.ts b/packages/core/src/__test/cache.test.ts index db3ec991ea..556c280605 100644 --- a/packages/core/src/__test/cache.test.ts +++ b/packages/core/src/__test/cache.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { TolgeeCore, TolgeeInstance } from '../TolgeeCore'; -import { TolgeePlugin, TreeTranslationsData } from '../types'; -import { resolvablePromise } from './testTools'; +import { TreeTranslationsData } from '../types'; +import { DevToolsPlugin, DevToolsThrow, resolvablePromise } from './testTools'; function waitForInitialLoad(tolgee: TolgeeInstance) { return new Promise((resolve) => { @@ -12,30 +12,6 @@ function waitForInitialLoad(tolgee: TolgeeInstance) { }); } -const DevToolsPlugin = - (postfix = ''): TolgeePlugin => - (tolgee, tools) => { - tolgee.updateOptions({ apiKey: 'test', apiUrl: 'test' }); - tools.setDevBackend({ - getRecord({ language, namespace }) { - return Promise.resolve({ - test: { sub: `${language}.${namespace || 'default'}${postfix}` }, - }); - }, - }); - return tolgee; - }; - -const DevToolsThrow = (): TolgeePlugin => (tolgee, tools) => { - tolgee.updateOptions({ apiKey: 'test', apiUrl: 'test' }); - tools.setDevBackend({ - getRecord() { - return Promise.reject(); - }, - }); - return tolgee; -}; - describe('cache', () => { let tolgee: TolgeeInstance; diff --git a/packages/core/src/__test/client.test.ts b/packages/core/src/__test/client.test.ts index 56a386bef8..d1520d869b 100644 --- a/packages/core/src/__test/client.test.ts +++ b/packages/core/src/__test/client.test.ts @@ -28,14 +28,14 @@ describe('using tolgee as client', () => { }); expect(promiseEnTest).toBeCalledTimes(1); expect(promiseEnCommon).not.toBeCalled(); - expect(enTest).toEqual(new Map([['test', 'Test']])); + expect(enTest).toEqual({ test: 'Test' }); const enCommon = await tolgee.loadRecord({ language: 'en', namespace: 'common', }); expect(promiseEnCommon).toBeCalledTimes(1); - expect(enCommon).toEqual(new Map([['cancel', 'Cancel']])); + expect(enCommon).toEqual({ cancel: 'Cancel' }); const esTest = await tolgee.loadRecord({ language: 'es', @@ -43,6 +43,6 @@ describe('using tolgee as client', () => { }); expect(promiseEsTest).toBeCalledTimes(1); expect(promiseEsCommon).not.toBeCalled(); - expect(esTest).toEqual(new Map([['test', 'Testa']])); + expect(esTest).toEqual({ test: 'Testa' }); }); }); diff --git a/packages/core/src/__test/events.test.ts b/packages/core/src/__test/events.test.ts index 2338270df0..9b9b08112f 100644 --- a/packages/core/src/__test/events.test.ts +++ b/packages/core/src/__test/events.test.ts @@ -6,29 +6,56 @@ describe('events', () => { const handler = jest.fn((lang) => {}); tolgee.on('language', handler); await tolgee.changeLanguage('es'); - expect(handler).toHaveBeenCalledWith({ value: 'es' }); + expect(handler).toHaveBeenCalledWith({ type: 'language', value: 'es' }); }); - it('correctly emits translation change listeners', async () => { + it('emits pendingLanguage event correctly', async () => { const tolgee = TolgeeCore().init({ language: 'en', staticData: { - en: { hello: 'World', language: 'English' }, - es: { hello: 'Mundo', language: 'Spanish' }, + en: () => Promise.resolve().then(() => ({ test: 'Test' })), + es: () => Promise.resolve().then(() => ({ test: 'El Test' })), }, }); - const helloHandler = jest.fn(() => {}); - const languageHandler = jest.fn(() => {}); - - tolgee.onNsUpdate(helloHandler); - tolgee.onNsUpdate(languageHandler); + await tolgee.run(); + const languageHandler = jest.fn(); + const pendingLanguageHandler = jest.fn(); + tolgee.on('language', languageHandler); + tolgee.on('pendingLanguage', pendingLanguageHandler); + const promise = tolgee.changeLanguage('es'); + expect(pendingLanguageHandler).toHaveBeenCalledTimes(1); + expect(languageHandler).toHaveBeenCalledTimes(0); + await promise; + expect(pendingLanguageHandler).toHaveBeenCalledTimes(1); + expect(languageHandler).toHaveBeenCalledTimes(1); + }); - tolgee.changeTranslation({ language: 'es' }, 'hello', 'Světe'); + it('groups cache events with language change event', async () => { + const tolgee = TolgeeCore().init({ language: 'en' }); + const handler = jest.fn((e) => {}); + tolgee.on('update', handler); + tolgee.addStaticData({ en: { test: 'Test' } }); tolgee.changeLanguage('es'); + expect(handler).toHaveBeenCalledWith([ + { type: 'cache', value: { language: 'en', namespace: '' } }, + { type: 'language', value: 'es' }, + ]); + }); - await Promise.resolve(); - expect(helloHandler).toHaveBeenCalledTimes(1); - expect(languageHandler).toHaveBeenCalledTimes(1); + it('groups cache events with initialLoad event', async () => { + const tolgee = TolgeeCore().init({ + language: 'en', + staticData: { + en: () => Promise.resolve({ test: 'Test' }), + }, + }); + const handler = jest.fn((e) => {}); + tolgee.on('update', handler); + await tolgee.run(); + expect(handler).toHaveBeenCalledWith([ + { type: 'cache', value: { language: 'en', namespace: '' } }, + { type: 'initialLoad', value: undefined }, + ]); }); it('stop emitting when turned off', async () => { diff --git a/packages/core/src/__test/load.matrix.test.ts b/packages/core/src/__test/load.matrix.test.ts new file mode 100644 index 0000000000..3f69064977 --- /dev/null +++ b/packages/core/src/__test/load.matrix.test.ts @@ -0,0 +1,123 @@ +import { TolgeeCore } from '../TolgeeCore'; +import { TolgeeInstance } from '../types'; +import { BackendPlugin, DevToolsPlugin } from './testTools'; + +describe('load required', () => { + let tolgee: TolgeeInstance; + + beforeEach(async () => { + tolgee = TolgeeCore() + .use(DevToolsPlugin('.dev')) + .use(BackendPlugin('.prod')) + .init({ + language: 'en', + apiKey: 'test', + apiUrl: 'test', + }); + }); + + it('loads english and empty ns', async () => { + const records = await tolgee.loadMatrix({ + languages: ['en'], + namespaces: [''], + }); + expect(records).toEqual([ + { + cacheKey: 'en', + data: { 'test.sub': 'en.default.dev' }, + language: 'en', + namespace: '', + }, + ]); + }); + + it('loads english and empty ns', async () => { + const records = await tolgee.loadMatrix({ + languages: ['en'], + namespaces: [''], + }); + expect(records).toEqual([ + { + cacheKey: 'en', + data: { 'test.sub': 'en.default.dev' }, + language: 'en', + namespace: '', + }, + ]); + }); + + it('fails to load all namespaces when no availableNs specified', async () => { + const promise = tolgee.loadMatrix({ + languages: ['en'], + namespaces: 'all', + }); + + expect(() => promise).rejects.toThrow( + "Tolgee: You need to specify 'availableNs' option" + ); + }); + + it('fails to load all languages when no availableLanguages specified', async () => { + const promise = tolgee.loadMatrix({ + languages: 'all', + namespaces: [''], + }); + + expect(() => promise).rejects.toThrow( + "Tolgee: You need to specify 'availableLanguages' option" + ); + }); + + it('loads all dev', async () => { + tolgee.updateOptions({ + availableLanguages: ['en'], + availableNs: ['', 'test'], + }); + const result = await tolgee.loadMatrix({ + languages: 'all', + namespaces: 'all', + }); + + expect(result).toEqual([ + { + cacheKey: 'en', + data: { 'test.sub': 'en.default.dev' }, + language: 'en', + namespace: '', + }, + { + cacheKey: 'en:test', + data: { 'test.sub': 'en.test.dev' }, + language: 'en', + namespace: 'test', + }, + ]); + }); + + it('loads all prod', async () => { + tolgee.updateOptions({ + availableLanguages: ['en'], + availableNs: ['', 'test'], + }); + const result = await tolgee.loadMatrix({ + languages: 'all', + namespaces: 'all', + noDev: true, + }); + + expect(result).toEqual([ + { + cacheKey: 'en', + data: { 'test.sub': 'en.default.prod' }, + language: 'en', + namespace: '', + }, + { + cacheKey: 'en:test', + data: { 'test.sub': 'en.test.prod' }, + language: 'en', + namespace: 'test', + }, + ]); + }); +}); diff --git a/packages/core/src/__test/load.required.test.ts b/packages/core/src/__test/load.required.test.ts new file mode 100644 index 0000000000..e8d714bdd3 --- /dev/null +++ b/packages/core/src/__test/load.required.test.ts @@ -0,0 +1,71 @@ +import { TolgeeCore } from '../TolgeeCore'; +import { TolgeeInstance } from '../types'; +import { BackendPlugin, DevToolsPlugin } from './testTools'; + +describe('load required', () => { + let tolgee: TolgeeInstance; + + beforeEach(async () => { + tolgee = TolgeeCore() + .use(DevToolsPlugin('.dev')) + .use(BackendPlugin('.prod')) + .init({ + language: 'en', + apiKey: 'test', + apiUrl: 'test', + }); + }); + + it('loads required records', async () => { + const records = await tolgee.loadRequired(); + expect(records).toEqual([ + { + cacheKey: 'en', + data: { 'test.sub': 'en.default.dev' }, + language: 'en', + namespace: '', + }, + ]); + }); + + it('loads required records when language is passed through parameter', async () => { + tolgee.updateOptions({ language: undefined }); + const noRecords = await tolgee.loadRequired(); + expect(noRecords).toEqual([]); + const records = await tolgee.loadRequired({ language: 'en' }); + expect(records).toEqual([ + { + cacheKey: 'en', + data: { 'test.sub': 'en.default.dev' }, + language: 'en', + namespace: '', + }, + ]); + }); + + it('loads records with namespaces', async () => { + tolgee.updateOptions({ defaultNs: 'namespace' }); + const records = await tolgee.loadRequired(); + expect(records).toEqual([ + { + cacheKey: 'en:namespace', + data: { 'test.sub': 'en.namespace.dev' }, + language: 'en', + namespace: 'namespace', + }, + ]); + }); + + it('loads records production', async () => { + tolgee.updateOptions(); + const records = await tolgee.loadRequired({ noDev: true }); + expect(records).toEqual([ + { + cacheKey: 'en', + data: { 'test.sub': 'en.default.prod' }, + language: 'en', + namespace: '', + }, + ]); + }); +}); diff --git a/packages/core/src/__test/namespaces.required.test.ts b/packages/core/src/__test/namespaces.required.test.ts new file mode 100644 index 0000000000..522a4fae57 --- /dev/null +++ b/packages/core/src/__test/namespaces.required.test.ts @@ -0,0 +1,52 @@ +import { Controller } from '../Controller/Controller'; + +describe('required namespaces', () => { + it('', () => { + const controller = Controller({ + options: {}, + }); + expect(controller.getDefaultNs()).toEqual(''); + expect(controller.getRequiredNamespaces()).toEqual(['']); + }); + + it('ns:[common]', () => { + const controller = Controller({ + options: { ns: ['common'] }, + }); + expect(controller.getRequiredNamespaces()).toEqual(['common']); + expect(controller.getDefaultNs()).toEqual('common'); + }); + + it('defaultNs: test', () => { + const controller = Controller({ + options: { defaultNs: 'test' }, + }); + expect(controller.getRequiredNamespaces()).toEqual(['test']); + expect(controller.getDefaultNs()).toEqual('test'); + }); + + it('defaultNs: test, ns:[common]', () => { + const controller = Controller({ + options: { defaultNs: 'test', ns: ['common'] }, + }); + expect(controller.getRequiredNamespaces()).toEqual(['test', 'common']); + expect(controller.getDefaultNs()).toEqual('test'); + }); + + it('defaultNs, ns, fallbackNs', () => { + const controller = Controller({ + options: { + defaultNs: 'test', + ns: ['common', 'test2'], + fallbackNs: ['fallback', 'fallback2'], + }, + }); + expect(controller.getRequiredNamespaces()).toEqual([ + 'test', + 'common', + 'test2', + 'fallback', + 'fallback2', + ]); + }); +}); diff --git a/packages/core/src/__test/options.test.ts b/packages/core/src/__test/options.test.ts index e2e31ca14b..89237bc457 100644 --- a/packages/core/src/__test/options.test.ts +++ b/packages/core/src/__test/options.test.ts @@ -41,7 +41,7 @@ describe('initial options', () => { expect(restrictedElements).toEqual(['a']); expect(highlightColor).toEqual('red'); expect(inputPrefix).toEqual('%-%tolgee:'); - expect(defaultNs).toEqual(''); + expect(defaultNs).toEqual(undefined); }); it('sanitizes url', () => { diff --git a/packages/core/src/__test/testTools.ts b/packages/core/src/__test/testTools.ts index 79b5c9d24c..d2a70390ce 100644 --- a/packages/core/src/__test/testTools.ts +++ b/packages/core/src/__test/testTools.ts @@ -1,3 +1,5 @@ +import { TolgeePlugin } from '../types'; + export const resolvablePromise = () => { let resolve: (value: T) => void; const promise = new Promise((innerResolve) => { @@ -5,3 +7,40 @@ export const resolvablePromise = () => { }); return [promise, resolve!] as const; }; + +export const DevToolsPlugin = + (postfix = ''): TolgeePlugin => + (tolgee, tools) => { + tolgee.updateOptions({ apiKey: 'test', apiUrl: 'test' }); + tools.setDevBackend({ + getRecord({ language, namespace }) { + return Promise.resolve({ + test: { sub: `${language}.${namespace || 'default'}${postfix}` }, + }); + }, + }); + return tolgee; + }; + +export const BackendPlugin = + (postfix = ''): TolgeePlugin => + (tolgee, tools) => { + tools.addBackend({ + getRecord({ language, namespace }) { + return Promise.resolve({ + test: { sub: `${language}.${namespace || 'default'}${postfix}` }, + }); + }, + }); + return tolgee; + }; + +export const DevToolsThrow = (): TolgeePlugin => (tolgee, tools) => { + tolgee.updateOptions({ apiKey: 'test', apiUrl: 'test' }); + tools.setDevBackend({ + getRecord() { + return Promise.reject(); + }, + }); + return tolgee; +}; diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index c14deef72d..eef57fb1cb 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -4,6 +4,7 @@ import { FallbackLanguageOption, FetchFn, TolgeeError, + ErrorEvent, } from './types'; import { EventEmitterInstance } from './Controller/Events/EventEmitter'; @@ -25,7 +26,7 @@ export function valueOrPromise( } export function handleRegularOrAsyncErr( - onError: EventEmitterInstance, + onError: EventEmitterInstance, createError: (e: any) => TolgeeError, callback: () => Promise | T ): Promise | T { diff --git a/packages/core/src/types/cache.ts b/packages/core/src/types/cache.ts index cf36615459..bc3aaf3eb0 100644 --- a/packages/core/src/types/cache.ts +++ b/packages/core/src/types/cache.ts @@ -1,6 +1,6 @@ export type TranslationValue = string | undefined | null; -export type TranslationsFlat = Map; +export type TranslationsFlat = Record; export type TreeTranslationsData = { [key: string]: TranslationValue | TreeTranslationsData; @@ -35,3 +35,26 @@ export type CachePublicRecord = { language: string; namespace: string; }; + +export type CacheInternalRecord = { + data: TranslationsFlat; + language: string; + namespace: string; + cacheKey: string; +}; + +export type LoadOptions = { + noDev?: boolean; + useCache?: boolean; +}; + +export type LoadRequiredOptions = LoadOptions & { + language?: string; +}; + +export type MatrixOptions = { + languages: string[] | 'all'; + namespaces: string[] | 'all'; +}; + +export type LoadMatrixOptions = LoadOptions & MatrixOptions; diff --git a/packages/core/src/types/events.ts b/packages/core/src/types/events.ts index dabe0a6379..b8bec0196a 100644 --- a/packages/core/src/types/events.ts +++ b/packages/core/src/types/events.ts @@ -1,4 +1,3 @@ -import type { NsFallback } from './general'; import type { CacheDescriptorWithKey } from './cache'; import { TolgeeError } from './errors'; @@ -6,19 +5,26 @@ export type Subscription = { unsubscribe: () => void; }; -export type SubscriptionSelective = { - unsubscribe: () => void; - /** - * Subscribes to namespace(s) - * @param ns - namespace(s), if empty default namespace is used - * - * Can be used multiple times to subscribe for more. - */ - subscribeNs(ns?: NsFallback): SubscriptionSelective; -}; - -export type ListenerEvent = { value: T }; -export type Listener = (e: ListenerEvent) => void; +export type ListenerEvent = { type: E; value: T }; +export type Handler> = (e: E) => void; +export type CombinedHandler> = ( + e: E[] +) => void; + +export type LanguageEvent = ListenerEvent<'language', string>; +export type PendingLanguageEvent = ListenerEvent<'pendingLanguage', string>; +export type LoadingEvent = ListenerEvent<'loading', boolean>; +export type FetchingEvent = ListenerEvent<'fetching', boolean>; +export type InitialLoadEvent = ListenerEvent<'initialLoad', void>; +export type RunningEvent = ListenerEvent<'running', boolean>; +export type CacheEvent = ListenerEvent<'cache', CacheDescriptorWithKey>; +export type ErrorEvent = ListenerEvent<'error', TolgeeError>; +export type PermanentChangeEvent = ListenerEvent< + 'permanentChange', + TranslationDescriptor +>; + +export type UpdateEvent = LanguageEvent | CacheEvent | InitialLoadEvent; export type TolgeeEvent = | 'language' @@ -49,54 +55,57 @@ export type TolgeeOn = { * Emitted when any key needs (or might need) to be re-rendered. * Similar to tolgee.onNsUpdate, except for all namespaces. */ - (event: 'update', handler: Listener): Subscription; + (event: 'update', handler: CombinedHandler): Subscription; /** * Emitted on language change. */ - (event: 'language', handler: Listener): Subscription; + (event: 'language', handler: Handler): Subscription; /** * Emitted on pendingLanguage change. */ - (event: 'pendingLanguage', handler: Listener): Subscription; + ( + event: 'pendingLanguage', + handler: Handler + ): Subscription; /** * Emitted on loading change. Changes when tolgee is loading some data for the first time. */ - (event: 'loading', handler: Listener): Subscription; + (event: 'loading', handler: Handler): Subscription; /** * Emitted on fetching change. Changes when tolgee is fetching any data. */ - (event: 'fetching', handler: Listener): Subscription; + (event: 'fetching', handler: Handler): Subscription; /** * Emitted when `tolgee.run` method finishes. */ - (event: 'initialLoad', handler: Listener): Subscription; + (event: 'initialLoad', handler: Handler): Subscription; /** * Emitted when internal `running` state changes. */ - (event: 'running', handler: Listener): Subscription; + (event: 'running', handler: Handler): Subscription; /** * Emitted when cache changes. */ - (event: 'cache', handler: Listener): Subscription; + (event: 'cache', handler: Handler): Subscription; /** * Emitted on errors */ - (event: 'error', handler: Listener): Subscription; + (event: 'error', handler: Handler): Subscription; /** * Translation was changed or created via dev tools */ ( event: 'permanentChange', - handler: Listener + handler: Handler ): Subscription; (event: E, handler: unknown): Subscription; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 86ebbcbd19..6192ce6424 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -9,6 +9,7 @@ export type { TolgeeOptions, TolgeeOptionsInternal, TolgeeStaticData, + TolgeeStaticDataProp, } from '../Controller/State/initState'; export type { diff --git a/packages/i18next/src/I18nextPlugin.ts b/packages/i18next/src/I18nextPlugin.ts index d1622fefcc..4a43327bc2 100644 --- a/packages/i18next/src/I18nextPlugin.ts +++ b/packages/i18next/src/I18nextPlugin.ts @@ -1,6 +1,6 @@ import { TolgeePlugin } from '@tolgee/web'; export const I18nextPlugin = (): TolgeePlugin => (tolgee) => { - tolgee.updateOptions({ ns: [], defaultNs: undefined }); + tolgee.updateOptions({ autoLoadRequiredData: false }); return tolgee; }; diff --git a/packages/i18next/src/tolgeeApply.ts b/packages/i18next/src/tolgeeApply.ts index e023353382..2b6b9e637f 100644 --- a/packages/i18next/src/tolgeeApply.ts +++ b/packages/i18next/src/tolgeeApply.ts @@ -8,7 +8,7 @@ export const tolgeeApply = (tolgee: TolgeeInstance, i18n: i18n) => { i18n.addResourceBundle( language, namespace, - Object.fromEntries(data), + data instanceof Map ? Object.fromEntries(data) : data, false, true ); diff --git a/packages/i18next/src/tolgeeBackend.ts b/packages/i18next/src/tolgeeBackend.ts index d97454b863..545ea82de8 100644 --- a/packages/i18next/src/tolgeeBackend.ts +++ b/packages/i18next/src/tolgeeBackend.ts @@ -14,7 +14,9 @@ export const tolgeeBackend = (tolgee: TolgeeInstance): Module => { }); callback( null, - translations ? Object.fromEntries(translations) : undefined + translations instanceof Map + ? Object.fromEntries(translations) + : translations ); } catch (e) { // eslint-disable-next-line no-console diff --git a/packages/ngx/projects/ngx-tolgee/src/__integration/resolving/resolving.spec.ts b/packages/ngx/projects/ngx-tolgee/src/__integration/resolving/resolving.spec.ts index c05de22864..331c3eefb4 100644 --- a/packages/ngx/projects/ngx-tolgee/src/__integration/resolving/resolving.spec.ts +++ b/packages/ngx/projects/ngx-tolgee/src/__integration/resolving/resolving.spec.ts @@ -9,6 +9,7 @@ import { wait } from '@tolgee/testing/wait'; import { NgxTolgeeModule } from '../../lib/ngx-tolgee.module'; import { TOLGEE_INSTANCE } from '../../lib/tolgee-instance-token'; import { mockStaticDataAsync } from '@tolgee/testing/mockStaticData'; +import mockTranslations from '@tolgee/testing/mockTranslations'; let staticDataMock: ReturnType; @@ -57,8 +58,11 @@ describe('resolving', () => { beforeEach(async () => { jest.clearAllMocks(); staticDataMock = mockStaticDataAsync(); - staticDataMock.resolvablePromises.en.resolve(); - staticDataMock.resolvablePromises.cs.resolve(); + staticDataMock.promises = { + ...staticDataMock.promises, + en: mockTranslations.en, + cs: mockTranslations.cs, + } as any; }); it('waits for namespace load before module is rendered', async () => { diff --git a/packages/ngx/projects/ngx-tolgee/src/__integration/translate.service.spec.ts b/packages/ngx/projects/ngx-tolgee/src/__integration/translate.service.spec.ts index 9b74b6d4ca..bc87e3786f 100755 --- a/packages/ngx/projects/ngx-tolgee/src/__integration/translate.service.spec.ts +++ b/packages/ngx/projects/ngx-tolgee/src/__integration/translate.service.spec.ts @@ -1,6 +1,8 @@ import { TranslateService } from '../lib/translate.service'; import { Tolgee, TolgeeInstance, DevTools } from '@tolgee/web'; import { mockStaticDataAsync } from '@tolgee/testing/mockStaticData'; +import mockTranslations from '@tolgee/testing/mockTranslations'; +import { wait } from '@tolgee/testing/wait'; let staticDataMock: ReturnType; @@ -11,17 +13,18 @@ describe('translation service', () => { beforeEach(async () => { staticDataMock = mockStaticDataAsync(); tolgee = { - ...Tolgee().use(DevTools()).init({ - staticData: staticDataMock.promises, - language: 'en', - }), + ...Tolgee() + .use(DevTools()) + .init({ + staticData: { ...staticDataMock.promises, en: mockTranslations.en }, + language: 'en', + }), }; service = new TranslateService(tolgee, { runOutsideAngular: jest.fn((fn) => fn()), run: jest.fn((fn) => fn()), } as any); onSpy = jest.spyOn(tolgee, 'on'); - staticDataMock.resolvablePromises.en.resolve(); await tolgee.run(); }); @@ -30,6 +33,7 @@ describe('translation service', () => { service.on('language').subscribe(languageCallback); const promise = tolgee.changeLanguage('cs'); expect(languageCallback).toHaveBeenCalledTimes(0); + await wait(0); staticDataMock.resolvablePromises.cs.resolve(); await promise; expect(languageCallback).toHaveBeenCalledTimes(1); @@ -60,6 +64,7 @@ describe('translation service', () => { const languageCallback = jest.fn(); service.languageAsync.subscribe(languageCallback); const changePromise = service.changeLanguage('cs'); + await wait(0); staticDataMock.resolvablePromises.cs.resolve(); await changePromise; expect(service.language).toEqual('cs'); diff --git a/packages/ngx/projects/ngx-tolgee/src/lib/translate.service.ts b/packages/ngx/projects/ngx-tolgee/src/lib/translate.service.ts index 7136fb6a13..4899b8c32a 100755 --- a/packages/ngx/projects/ngx-tolgee/src/lib/translate.service.ts +++ b/packages/ngx/projects/ngx-tolgee/src/lib/translate.service.ts @@ -59,9 +59,7 @@ export class TranslateService implements OnDestroy { // noinspection JSIgnoredPromiseFromCall translate(); - const subscription = this.tolgee - .onNsUpdate(translate) - .subscribeNs(params.ns); + const subscription = this.tolgee.on('update', translate) return () => { this.tolgee.removeActiveNs(params.ns); @@ -121,7 +119,7 @@ export class TranslateService implements OnDestroy { * @param event the event to listen */ on(event: Event) { - return new Observable>((subscriber) => { + return new Observable>((subscriber) => { const subscription = this.tolgee.on(event, (value) => { this._ngZone.run(() => { subscriber.next(value as any); diff --git a/packages/react/src/TolgeeProvider.tsx b/packages/react/src/TolgeeProvider.tsx index 65622d63ce..9591c8638b 100644 --- a/packages/react/src/TolgeeProvider.tsx +++ b/packages/react/src/TolgeeProvider.tsx @@ -1,10 +1,10 @@ import React, { Suspense, useEffect, useState } from 'react'; -import { TolgeeInstance, TolgeeStaticData } from '@tolgee/web'; +import { TolgeeInstance, TolgeeStaticDataProp } from '@tolgee/web'; import { ReactOptions, TolgeeReactContext } from './types'; import { useTolgeeSSR } from './useTolgeeSSR'; export const DEFAULT_REACT_OPTIONS: ReactOptions = { - useSuspense: true, + useSuspense: false, }; let ProviderInstance: React.Context; @@ -29,7 +29,7 @@ export type SSROptions = { /** * If provided, static data will be hard set to Tolgee cache for initial render */ - staticData?: TolgeeStaticData; + staticData?: TolgeeStaticDataProp; }; export interface TolgeeProviderProps { diff --git a/packages/react/src/__integration/namespaces.spec.tsx b/packages/react/src/__integration/namespaces.spec.tsx index d540228d96..281b9bfbf9 100644 --- a/packages/react/src/__integration/namespaces.spec.tsx +++ b/packages/react/src/__integration/namespaces.spec.tsx @@ -36,7 +36,7 @@ describe('useTranslations namespaces', () => { beforeEach(async () => { staticDataMock = mockStaticDataAsync(); tolgee = Tolgee() - .use(GlobalContextPlugin({ useSuspense: false })) + .use(GlobalContextPlugin()) .use(DevTools()) .use(FormatIcu()) .init({ @@ -49,9 +49,7 @@ describe('useTranslations namespaces', () => { await act(async () => { const runPromise = tolgee.run(); - staticDataMock.resolvablePromises.cs.resolve(); - staticDataMock.resolvablePromises.en.resolve(); - staticDataMock.resolvablePromises['en:fallback'].resolve(); + staticDataMock.resolvePending(); await runPromise; render(); }); @@ -63,7 +61,7 @@ describe('useTranslations namespaces', () => { it('loads namespace after render', async () => { expect(screen.queryByTestId('loading')).toContainHTML('Loading...'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('loading')).toBeFalsy(); expect(screen.queryByTestId('test')).toContainHTML('Český test'); @@ -72,7 +70,7 @@ describe('useTranslations namespaces', () => { }); it('works with english fallback', async () => { - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('test_english_fallback')).toContainHTML( 'Test english fallback' @@ -85,7 +83,7 @@ describe('useTranslations namespaces', () => { it('works with ns fallback', async () => { expect(screen.queryByTestId('ns_fallback')).toContainHTML('fallback'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('ns_fallback')).toContainHTML('Fallback'); expect(screen.queryByTestId('ns_fallback')).toHaveAttribute('_tolgee'); @@ -98,7 +96,7 @@ describe('useTranslations namespaces', () => { ); await act(async () => { const changePromise = tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await changePromise; }); expect(screen.queryByTestId('ns_fallback')).toContainHTML( @@ -107,7 +105,7 @@ describe('useTranslations namespaces', () => { }); it('works with default value', async () => { - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('non_existant')).toContainHTML( 'Non existant' diff --git a/packages/react/src/__integration/useTolgee.spec.tsx b/packages/react/src/__integration/useTolgee.spec.tsx index af517a54be..684f39179d 100644 --- a/packages/react/src/__integration/useTolgee.spec.tsx +++ b/packages/react/src/__integration/useTolgee.spec.tsx @@ -5,6 +5,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import { Tolgee, TolgeeEvent, TolgeeInstance } from '@tolgee/web'; import { FormatIcu } from '@tolgee/format-icu'; import { mockStaticDataAsync } from '@tolgee/testing/mockStaticData'; +import { wait } from '@tolgee/testing/wait'; const API_URL = 'http://localhost'; @@ -66,6 +67,7 @@ describe('useTranslation hook integration', () => { checkState({ initialLoad: 'true' }); await act(async () => { + await wait(0); staticDataMock.resolvablePromises.cs.resolve(); await runPromise; }); @@ -74,6 +76,7 @@ describe('useTranslation hook integration', () => { }); await act(async () => { tolgee.changeLanguage('en'); + await wait(0); staticDataMock.resolvablePromises.en.resolve(); }); await waitFor(() => { @@ -90,6 +93,7 @@ describe('useTranslation hook integration', () => { checkState({ language: 'cs' }); await act(async () => { tolgee.changeLanguage('en'); + await wait(0); staticDataMock.resolvablePromises.en.resolve(); }); checkState({ language: 'en' }); @@ -104,6 +108,7 @@ describe('useTranslation hook integration', () => { checkState({ language: 'cs', pendingLanguage: 'cs' }); await act(async () => { tolgee.changeLanguage('en'); + await wait(0); staticDataMock.resolvablePromises.en.resolve(); }); await waitFor(async () => { diff --git a/packages/react/src/createServerInstance.tsx b/packages/react/src/createServerInstance.tsx index 0075857333..a93d85faa3 100644 --- a/packages/react/src/createServerInstance.tsx +++ b/packages/react/src/createServerInstance.tsx @@ -1,11 +1,11 @@ // @ts-ignore import { cache } from 'react'; +import React from 'react'; import { TFnType } from '@tolgee/web'; +import { TolgeeInstance } from '@tolgee/web'; import { TBase } from './TBase'; import { TProps, ParamsTags } from './types'; -import React from 'react'; -import { TolgeeInstance } from '@tolgee/web'; export type CreateServerInstanceOptions = { createTolgee: (locale: string) => Promise; @@ -16,11 +16,13 @@ export const createServerInstance = ({ createTolgee, getLocale, }: CreateServerInstanceOptions) => { - const getTolgeeInstance = cache(async (locale: string) => { - const tolgee = await createTolgee(locale); - await tolgee.run(); - return tolgee; - }) as (locale: string) => Promise; + const getTolgeeInstance: (locale: string) => Promise = cache( + async (locale: string) => { + const tolgee = await createTolgee(locale); + await tolgee.run(); + return tolgee; + } + ); const getTolgee = async () => { const locale = await getLocale(); @@ -38,5 +40,9 @@ export const createServerInstance = ({ return } {...props} />; } - return { getTolgeeInstance, getTolgee, getTranslate, T }; + return { + getTolgee, + getTranslate, + T, + }; }; diff --git a/packages/react/src/useTolgeeSSR.ts b/packages/react/src/useTolgeeSSR.ts index a152ba06cc..b68059b9e9 100644 --- a/packages/react/src/useTolgeeSSR.ts +++ b/packages/react/src/useTolgeeSSR.ts @@ -1,4 +1,5 @@ import { + CachePublicRecord, getTranslateProps, TolgeeInstance, TolgeeStaticData, @@ -33,7 +34,7 @@ function getTolgeeWithDeactivatedWrapper( export function useTolgeeSSR( tolgeeInstance: TolgeeInstance, language?: string, - staticData?: TolgeeStaticData | undefined, + data?: TolgeeStaticData | CachePublicRecord[] | undefined, enabled = true ) { const [noWrappingTolgee] = useState(() => @@ -52,23 +53,24 @@ export function useTolgeeSSR( // so translations are available right away // events emitting must be off, to not trigger re-render while rendering tolgeeInstance.setEmitterActive(false); - tolgeeInstance.addStaticData(staticData); + tolgeeInstance.addStaticData(data); tolgeeInstance.changeLanguage(language!); tolgeeInstance.setEmitterActive(true); } - }, [language, staticData, tolgeeInstance]); + }, [language, data, tolgeeInstance]); useState(() => { // running this function only on first render if (!tolgeeInstance.isLoaded() && enabled) { // warning user, that static data provided are not sufficient // for proper SSR render - const missingRecords = tolgeeInstance - .getRequiredRecords(language) + const requiredRecords = tolgeeInstance.getRequiredDescriptors(language); + const providedRecords = tolgeeInstance.getAllRecords(); + const missingRecords = requiredRecords .map(({ namespace, language }) => namespace ? `${namespace}:${language}` : language ) - .filter((key) => !staticData?.[key]); + .filter((key) => !providedRecords.find((r) => r?.cacheKey === key)); // eslint-disable-next-line no-console console.warn( diff --git a/packages/react/src/useTranslateInternal.ts b/packages/react/src/useTranslateInternal.ts index a3070beb61..86d8ee6503 100644 --- a/packages/react/src/useTranslateInternal.ts +++ b/packages/react/src/useTranslateInternal.ts @@ -1,6 +1,5 @@ import { useCallback, useEffect, useRef } from 'react'; import { - SubscriptionSelective, TranslateProps, NsFallback, getFallbackArray, @@ -27,25 +26,13 @@ export const useTranslateInternal = ( // dummy state to enable re-rendering const { rerender, instance } = useRerender(); - const subscriptionRef = useRef(); - const subscriptionQueue = useRef([] as NsFallback[]); subscriptionQueue.current = []; - const subscribeToNs = (ns: NsFallback) => { - subscriptionQueue.current.push(ns); - subscriptionRef.current?.subscribeNs(ns); - }; - const isLoaded = tolgee.isLoaded(namespaces); useEffect(() => { - const subscription = tolgee.onNsUpdate(rerender); - subscriptionRef.current = subscription; - subscription.subscribeNs(namespaces); - subscriptionQueue.current.forEach((ns) => { - subscription!.subscribeNs(ns); - }); + const subscription = tolgee.on('update', rerender); return () => { subscription.unsubscribe(); @@ -60,7 +47,6 @@ export const useTranslateInternal = ( const t = useCallback( (props: TranslateProps) => { const fallbackNs = props.ns ?? namespaces?.[0]; - subscribeToNs(fallbackNs); return tolgee.t({ ...props, ns: fallbackNs }) as any; }, [tolgee, instance] diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c46cbdda4e..604325d565 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -68,6 +68,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", "globals": "^15.0.0", + "jest": "^29.7.0", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "publint": "^0.2.0", diff --git a/packages/svelte/src/lib/__integration/getTolgee.spec.ts b/packages/svelte/src/lib/__integration/getTolgee.spec.ts index 94055cfb8e..9e93823aed 100644 --- a/packages/svelte/src/lib/__integration/getTolgee.spec.ts +++ b/packages/svelte/src/lib/__integration/getTolgee.spec.ts @@ -1,13 +1,7 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import '@testing-library/jest-dom'; import { screen, waitFor, render } from '@testing-library/svelte'; -import { - Tolgee, - type TolgeeEvent, - type TolgeeInstance, - DevTools, -} from '@tolgee/web'; +import { Tolgee, type TolgeeEvent, type TolgeeInstance, DevTools } from '@tolgee/web'; import { FormatIcu } from '@tolgee/format-icu'; import TestComponent from './components/TestGetTolgee.svelte'; import { mockStaticDataAsync } from '@tolgee/testing/mockStaticData'; @@ -18,102 +12,98 @@ const API_URL = 'http://localhost'; type CheckStateProps = Partial>; const checkState = (props: CheckStateProps) => { - Object.entries(props).forEach(([event, value]) => { - expect(screen.queryByTestId(event)).toContainHTML(value); - }); + Object.entries(props).forEach(([event, value]) => { + expect(screen.queryByTestId(event)).toContainHTML(value); + }); }; -describe('getTranslate', () => { - let tolgee: TolgeeInstance; - let runPromise: Promise; - let staticDataMock: ReturnType; +describe('getTolgee', () => { + let tolgee: TolgeeInstance; + let runPromise: Promise; + let staticDataMock: ReturnType; - beforeEach(async () => { - staticDataMock = mockStaticDataAsync(); - tolgee = Tolgee() - .use(GlobalContextPlugin()) - .use(DevTools()) - .use(FormatIcu()) - .init({ - apiUrl: API_URL, - language: 'cs', - staticData: staticDataMock.promises, - }); - runPromise = tolgee.run(); - }); + beforeEach(async () => { + staticDataMock = mockStaticDataAsync(); + tolgee = Tolgee().use(GlobalContextPlugin()).use(DevTools()).use(FormatIcu()).init({ + apiUrl: API_URL, + language: 'cs', + staticData: staticDataMock.promises + }); + runPromise = tolgee.run(); + }); - it('updates initialLoading', async () => { - render(TestComponent, { - props: { events: ['initialLoad'] }, - }); + it('updates initialLoading', async () => { + render(TestComponent, { + props: { events: ['initialLoad'] } + }); - checkState({ initialLoad: 'true' }); - staticDataMock.resolveAll(); - await runPromise; - await waitFor(() => { - checkState({ initialLoad: 'false' }); - }); - tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); - await waitFor(() => { - checkState({ initialLoad: 'false' }); - }); - }); + checkState({ initialLoad: 'true' }); + staticDataMock.resolvePending(); + await runPromise; + await waitFor(() => { + checkState({ initialLoad: 'false' }); + }); + tolgee.changeLanguage('en'); + staticDataMock.resolvePending(); + await waitFor(() => { + checkState({ initialLoad: 'false' }); + }); + }); - it('updates language', async () => { - render(TestComponent, { - props: { events: ['language'] }, - }); - staticDataMock.resolveAll(); - await runPromise; - checkState({ language: 'cs' }); - tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); - await waitFor(() => { - checkState({ language: 'en' }); - }); - }); + it('updates language', async () => { + render(TestComponent, { + props: { events: ['language'] } + }); + staticDataMock.resolvePending(); + await runPromise; + checkState({ language: 'cs' }); + tolgee.changeLanguage('en'); + staticDataMock.resolvePending(); + await waitFor(() => { + checkState({ language: 'en' }); + }); + }); - it('updates pending language', async () => { - render(TestComponent, { - props: { events: ['pendingLanguage'] }, - }); - staticDataMock.resolveAll(); - await runPromise; - checkState({ language: 'cs', pendingLanguage: 'cs' }); - tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); - await waitFor(async () => { - checkState({ language: 'cs', pendingLanguage: 'en' }); - }); - }); + it('updates pending language', async () => { + render(TestComponent, { + props: { events: ['pendingLanguage'] } + }); + staticDataMock.resolvePending(); + await runPromise; + checkState({ language: 'cs', pendingLanguage: 'cs' }); + tolgee.changeLanguage('en'); + staticDataMock.resolvePending(); + await waitFor(async () => { + checkState({ language: 'cs', pendingLanguage: 'en' }); + }); + }); - it('updates fetching and loading', async () => { - render(TestComponent, { - props: { events: ['loading', 'fetching'] }, - }); + it('updates fetching and loading', async () => { + render(TestComponent, { + props: { events: ['loading', 'fetching'] } + }); - checkState({ loading: 'true', fetching: 'true' }); - staticDataMock.resolveAll(); - await runPromise; - await waitFor(async () => { - checkState({ loading: 'false', fetching: 'false' }); - }); - tolgee.changeLanguage('en'); - await waitFor(() => { - checkState({ loading: 'false', fetching: 'true' }); - }); - staticDataMock.resolveAll(); - await waitFor(async () => { - checkState({ loading: 'false', fetching: 'false' }); - }); - tolgee.addActiveNs('test'); - await waitFor(() => { - checkState({ loading: 'true', fetching: 'true' }); - }); - staticDataMock.resolveAll(); - await waitFor(async () => { - checkState({ loading: 'false', fetching: 'false' }); - }); - }); + checkState({ loading: 'true', fetching: 'true' }); + staticDataMock.resolvePending(); + await runPromise; + await waitFor(async () => { + checkState({ loading: 'false', fetching: 'false' }); + }); + tolgee.changeLanguage('en'); + await waitFor(() => { + checkState({ loading: 'false', fetching: 'true' }); + }); + staticDataMock.resolvePending(); + await waitFor(async () => { + checkState({ loading: 'false', fetching: 'false' }); + }); + tolgee.addActiveNs('test'); + await waitFor(() => { + checkState({ loading: 'true', fetching: 'true' }); + }); + staticDataMock.resolvePending(); + await waitFor(async () => { + checkState({ loading: 'false', fetching: 'false' }); + }); + }); }); diff --git a/packages/svelte/src/lib/__integration/namespaces.spec.ts b/packages/svelte/src/lib/__integration/namespaces.spec.ts index 8f43195bf6..8a4d03aa4e 100644 --- a/packages/svelte/src/lib/__integration/namespaces.spec.ts +++ b/packages/svelte/src/lib/__integration/namespaces.spec.ts @@ -9,76 +9,66 @@ import { GlobalContextPlugin } from '$lib/GlobalContextPlugin'; const API_URL = 'http://localhost'; describe('useTranslations namespaces', () => { - let tolgee: TolgeeInstance; - let staticDataMock: ReturnType; + let tolgee: TolgeeInstance; + let staticDataMock: ReturnType; - beforeEach(async () => { - staticDataMock = mockStaticDataAsync(); - tolgee = Tolgee() - .use(DevTools()) - .use(GlobalContextPlugin()) - .use(FormatIcu()) - .init({ - apiUrl: API_URL, - language: 'cs', - fallbackLanguage: 'en', - fallbackNs: 'fallback', - staticData: staticDataMock.promises, - }); + beforeEach(async () => { + staticDataMock = mockStaticDataAsync(); + tolgee = Tolgee().use(DevTools()).use(GlobalContextPlugin()).use(FormatIcu()).init({ + apiUrl: API_URL, + language: 'cs', + fallbackLanguage: 'en', + fallbackNs: 'fallback', + staticData: staticDataMock.promises + }); - const runPromise = tolgee.run(); - staticDataMock.resolveAll(); - await runPromise; - render(Namespaces); - }); + const runPromise = tolgee.run(); + staticDataMock.resolvePending(); + await runPromise; + render(Namespaces); + }); - it('loads namespace after render', async () => { - expect(screen.queryByTestId('loading')).toContainHTML('Loading...'); - staticDataMock.resolveAll(); - await waitFor(() => { - expect(screen.queryByTestId('loading')).toBeFalsy(); - expect(screen.queryByTestId('test')).toContainHTML('Český test'); - expect(screen.queryByTestId('test')).toHaveAttribute('_tolgee'); - }); - }); + it('loads namespace after render', async () => { + expect(screen.queryByTestId('loading')).toContainHTML('Loading...'); + staticDataMock.resolvePending(); + await waitFor(() => { + expect(screen.queryByTestId('loading')).toBeFalsy(); + expect(screen.queryByTestId('test')).toContainHTML('Český test'); + expect(screen.queryByTestId('test')).toHaveAttribute('_tolgee'); + }); + }); - it('works with english fallback', async () => { - staticDataMock.resolveAll(); - await waitFor(() => { - expect(screen.queryByTestId('test_english_fallback')).toContainHTML( - 'Test english fallback' - ); - expect(screen.queryByTestId('test_english_fallback')).toHaveAttribute( - '_tolgee' - ); - }); - }); + it('works with english fallback', async () => { + staticDataMock.resolvePending(); + await waitFor(() => { + expect(screen.queryByTestId('test_english_fallback')).toContainHTML('Test english fallback'); + expect(screen.queryByTestId('test_english_fallback')).toHaveAttribute('_tolgee'); + }); + }); - it('works with ns fallback', async () => { - expect(screen.queryByTestId('ns_fallback')).toContainHTML('fallback'); - staticDataMock.resolveAll(); - await waitFor(() => { - expect(screen.queryByTestId('ns_fallback')).toContainHTML('Fallback'); - expect(screen.queryByTestId('ns_fallback')).toHaveAttribute('_tolgee'); - }); - }); + it('works with ns fallback', async () => { + expect(screen.queryByTestId('ns_fallback')).toContainHTML('fallback'); + staticDataMock.resolvePending(); + await waitFor(() => { + expect(screen.queryByTestId('ns_fallback')).toContainHTML('Fallback'); + expect(screen.queryByTestId('ns_fallback')).toHaveAttribute('_tolgee'); + }); + }); - it('works with language and ns fallback', async () => { - tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); - await waitFor(() => { - expect(screen.queryByTestId('ns_fallback')).toContainHTML('Fallback'); - expect(screen.queryByTestId('ns_fallback')).toHaveAttribute('_tolgee'); - }); - }); + it('works with language and ns fallback', async () => { + tolgee.changeLanguage('en'); + staticDataMock.resolvePending(); + await waitFor(() => { + expect(screen.queryByTestId('ns_fallback')).toContainHTML('Fallback'); + expect(screen.queryByTestId('ns_fallback')).toHaveAttribute('_tolgee'); + }); + }); - it('works with default value', async () => { - staticDataMock.resolveAll(); - await waitFor(() => { - expect(screen.queryByTestId('non_existant')).toContainHTML( - 'Non existant' - ); - expect(screen.queryByTestId('non_existant')).toHaveAttribute('_tolgee'); - }); - }); + it('works with default value', async () => { + staticDataMock.resolvePending(); + await waitFor(() => { + expect(screen.queryByTestId('non_existant')).toContainHTML('Non existant'); + expect(screen.queryByTestId('non_existant')).toHaveAttribute('_tolgee'); + }); + }); }); diff --git a/packages/svelte/src/lib/getTranslateInternal.ts b/packages/svelte/src/lib/getTranslateInternal.ts index b764daa38a..506011985c 100644 --- a/packages/svelte/src/lib/getTranslateInternal.ts +++ b/packages/svelte/src/lib/getTranslateInternal.ts @@ -1,59 +1,53 @@ import { writable } from 'svelte/store'; import { onDestroy } from 'svelte'; import { - getFallback, - type NsFallback, - type TolgeeInstance, - type TranslateProps, + getFallback, + type NsFallback, + type TolgeeInstance, + type TranslateProps } from '@tolgee/web'; import { getTolgeeContext } from './getTolgeeContext'; const getTranslateInternal = (ns?: NsFallback) => { - const namespaces = getFallback(ns); - const tolgeeContext = getTolgeeContext(); + const namespaces = getFallback(ns); + const tolgeeContext = getTolgeeContext(); - const tolgee = tolgeeContext?.tolgee as TolgeeInstance; + const tolgee = tolgeeContext?.tolgee as TolgeeInstance; - if (!tolgee) { - throw new Error('Tolgee instance not provided'); - } + if (!tolgee) { + throw new Error('Tolgee instance not provided'); + } - const tFunction = createTFunction(); + const tFunction = createTFunction(); - const t = writable(tFunction); + const t = writable(tFunction); - const subscription = tolgee.onNsUpdate(() => { - t.set(createTFunction()); - isLoading.set(!tolgee.isLoaded(namespaces)); - }); + const subscription = tolgee.on('update', () => { + t.set(createTFunction()); + isLoading.set(!tolgee.isLoaded(namespaces)); + }); - subscription.subscribeNs(namespaces); - tolgee.addActiveNs(namespaces); + tolgee.addActiveNs(namespaces); - onDestroy(() => { - subscription?.unsubscribe(); - tolgee.removeActiveNs(namespaces); - }); + onDestroy(() => { + subscription?.unsubscribe(); + tolgee.removeActiveNs(namespaces); + }); - const isLoading = writable(!tolgee.isLoaded(namespaces)); + const isLoading = writable(!tolgee.isLoaded(namespaces)); - const subscribeToNs = (ns: NsFallback) => { - subscription.subscribeNs(ns); - }; + function createTFunction() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (props: TranslateProps) => { + const fallbackNs = props.ns ?? namespaces?.[0]; + return tolgee.t({ ...props, ns: fallbackNs }); + }; + } - function createTFunction() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (props: TranslateProps) => { - const fallbackNs = props.ns ?? namespaces?.[0]; - subscribeToNs(fallbackNs); - return tolgee.t({ ...props, ns: fallbackNs }); - }; - } - - return { - t: t, - isLoading, - }; + return { + t: t, + isLoading + }; }; export default getTranslateInternal; diff --git a/packages/testing/mockStaticData.ts b/packages/testing/mockStaticData.ts index fc39fdb5fb..0be8b28d6f 100644 --- a/packages/testing/mockStaticData.ts +++ b/packages/testing/mockStaticData.ts @@ -1,5 +1,6 @@ import mockTranslations from './mockTranslations'; import { createResolvablePromise } from './createResolvablePromise'; +import { wait } from './wait'; export const mockStaticDataAsync = () => { const resolvablePromises: Record< @@ -9,16 +10,20 @@ export const mockStaticDataAsync = () => { const promises: Record Promise> = {} as any; Object.entries(mockTranslations).forEach(([key, data]) => { - const resolvablePromise = createResolvablePromise(data); - resolvablePromises[key as keyof typeof mockTranslations] = - resolvablePromise; - promises[key as keyof typeof mockTranslations] = () => - resolvablePromise.promise; + promises[key as keyof typeof mockTranslations] = () => { + const resolvablePromise = createResolvablePromise(data); + resolvablePromises[key as keyof typeof mockTranslations] = + resolvablePromise; + return resolvablePromise.promise; + }; }); return { resolvablePromises, promises, - resolveAll: () => { + resolvePending: async () => { + // wait for promises to be created + await wait(0); + // now resolve them Object.values(resolvablePromises).forEach((p) => p.resolve()); }, }; diff --git a/packages/vue/src/TolgeeProvider.ts b/packages/vue/src/TolgeeProvider.ts index a4cba5ebc4..81b7712fd5 100644 --- a/packages/vue/src/TolgeeProvider.ts +++ b/packages/vue/src/TolgeeProvider.ts @@ -10,9 +10,20 @@ import { computed, } from 'vue'; import type { Ref, ComputedRef } from 'vue'; -import { TolgeeInstance, TolgeeStaticData } from '@tolgee/web'; +import { TolgeeInstance, TolgeeStaticDataProp } from '@tolgee/web'; import { TolgeeVueContext } from './types'; +export type SSROptions = { + /** + * Hard set language to this value, use together with `staticData` + */ + language?: string; + /** + * If provided, static data will be hard set to Tolgee cache for initial render + */ + staticData?: TolgeeStaticDataProp; +}; + export const TolgeeProvider = defineComponent({ name: 'TolgeeProvider', props: { @@ -20,12 +31,8 @@ export const TolgeeProvider = defineComponent({ fallback: { type: [Object, String] as PropType, }, - staticData: { - type: Object as PropType, - required: false, - }, - language: { - type: String as PropType, + ssr: { + type: [Object, Boolean] as PropType, required: false, }, }, @@ -46,27 +53,24 @@ export const TolgeeProvider = defineComponent({ throw new Error('Tolgee instance not provided'); } - if (tolgeeContext.value.isInitialRender) { - if (!props.staticData || !props.language) { - throw new Error( - 'TolgeeProvider: "staticData" and "language" props are required for SSR.' - ); - } - + if (tolgeeContext.value.isInitialRender && Boolean(props.ssr)) { + const ssr = ( + typeof props.ssr === 'object' ? props.ssr : {} + ) as SSROptions; tolgee.value.setEmitterActive(false); - tolgee.value.addStaticData(props.staticData); - tolgee.value.changeLanguage(props.language); + tolgee.value.addStaticData(ssr.staticData); + tolgee.value.changeLanguage(ssr.language); tolgee.value.setEmitterActive(true); if (!tolgee.value.isLoaded()) { // warning user, that static data provided are not sufficient // for proper SSR render const missingRecords = tolgee.value - .getRequiredRecords(props.language) + .getRequiredDescriptors(ssr.language) .map(({ namespace, language }) => namespace ? `${namespace}:${language}` : language ) - .filter((key) => !props.staticData?.[key]); + .filter((key) => !ssr.staticData?.[key]); // eslint-disable-next-line no-console console.warn( diff --git a/packages/vue/src/__integration/namespaces.spec.ts b/packages/vue/src/__integration/namespaces.spec.ts index 8a48311379..aebbb0634e 100644 --- a/packages/vue/src/__integration/namespaces.spec.ts +++ b/packages/vue/src/__integration/namespaces.spec.ts @@ -45,7 +45,7 @@ describe('useTranslations namespaces', () => { }); const runPromise = tolgee.run(); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await runPromise; render(TestComponent, { global: { plugins: [[VueTolgee, { tolgee }]] }, @@ -54,7 +54,7 @@ describe('useTranslations namespaces', () => { it('loads namespace after render', async () => { expect(screen.queryByTestId('loading')).toContainHTML('Loading...'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('loading')).toBeFalsy(); expect(screen.queryByTestId('test')).toContainHTML('Český test'); @@ -63,7 +63,7 @@ describe('useTranslations namespaces', () => { }); it('works with english fallback', async () => { - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('test_english_fallback')).toContainHTML( 'Test english fallback' @@ -76,7 +76,7 @@ describe('useTranslations namespaces', () => { it('works with ns fallback', async () => { expect(screen.queryByTestId('ns_fallback')).toContainHTML('fallback'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('ns_fallback')).toContainHTML('Fallback'); expect(screen.queryByTestId('ns_fallback')).toHaveAttribute('_tolgee'); @@ -85,7 +85,7 @@ describe('useTranslations namespaces', () => { it('works with language and ns fallback', async () => { tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('ns_fallback')).toContainHTML('Fallback'); expect(screen.queryByTestId('ns_fallback')).toHaveAttribute('_tolgee'); @@ -93,7 +93,7 @@ describe('useTranslations namespaces', () => { }); it('works with default value', async () => { - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { expect(screen.queryByTestId('non_existant')).toContainHTML( 'Non existant' diff --git a/packages/vue/src/__integration/useTolgee.spec.ts b/packages/vue/src/__integration/useTolgee.spec.ts index 94668d03a9..03305a1994 100644 --- a/packages/vue/src/__integration/useTolgee.spec.ts +++ b/packages/vue/src/__integration/useTolgee.spec.ts @@ -58,13 +58,13 @@ describe('useTranslation hook integration', () => { }); checkState({ initialLoad: 'true' }); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await runPromise; await waitFor(() => { checkState({ initialLoad: 'false' }); }); tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { checkState({ initialLoad: 'false' }); }); @@ -75,11 +75,11 @@ describe('useTranslation hook integration', () => { props: { events: ['language'] }, global: { plugins: [[VueTolgee, { tolgee }]] }, }); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await runPromise; checkState({ language: 'cs' }); tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(() => { checkState({ language: 'en' }); }); @@ -90,11 +90,11 @@ describe('useTranslation hook integration', () => { props: { events: ['pendingLanguage'] }, global: { plugins: [[VueTolgee, { tolgee }]] }, }); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await runPromise; checkState({ language: 'cs', pendingLanguage: 'cs' }); tolgee.changeLanguage('en'); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(async () => { checkState({ language: 'cs', pendingLanguage: 'en' }); }); @@ -107,7 +107,7 @@ describe('useTranslation hook integration', () => { }); checkState({ loading: 'true', fetching: 'true' }); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await runPromise; await waitFor(async () => { checkState({ loading: 'false', fetching: 'false' }); @@ -116,7 +116,7 @@ describe('useTranslation hook integration', () => { await waitFor(() => { checkState({ loading: 'false', fetching: 'true' }); }); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(async () => { checkState({ loading: 'false', fetching: 'false' }); }); @@ -124,7 +124,7 @@ describe('useTranslation hook integration', () => { await waitFor(() => { checkState({ loading: 'true', fetching: 'true' }); }); - staticDataMock.resolveAll(); + staticDataMock.resolvePending(); await waitFor(async () => { checkState({ loading: 'false', fetching: 'false' }); }); diff --git a/packages/vue/src/useTranslateInternal.ts b/packages/vue/src/useTranslateInternal.ts index f188223223..e40c51a1c1 100644 --- a/packages/vue/src/useTranslateInternal.ts +++ b/packages/vue/src/useTranslateInternal.ts @@ -21,11 +21,10 @@ export const useTranslateInternal = (ns?: NsFallback) => { } const t = ref(createTFunction()); - const subscription = tolgee.value.onNsUpdate(() => { + const subscription = tolgee.value.on('update', () => { t.value = createTFunction(); isLoading.value = !tolgee.value.isLoaded(namespaces); }); - subscription.subscribeNs(namespaces); tolgee.value.addActiveNs(namespaces); onUnmounted(() => { @@ -35,14 +34,9 @@ export const useTranslateInternal = (ns?: NsFallback) => { const isLoading = ref(!tolgee.value.isLoaded(namespaces)); - const subscribeToNs = (ns: NsFallback) => { - subscription.subscribeNs(ns); - }; - function createTFunction() { return (props: TranslateProps) => { const fallbackNs = props.ns ?? namespaces?.[0]; - subscribeToNs(fallbackNs); return tolgee.value.t({ ...props, ns: fallbackNs }) as any; }; } diff --git a/packages/web/src/package/__test__/fetch.apiUrl.test.ts b/packages/web/src/package/__test__/fetch.apiUrl.test.ts index ccb7a92b44..323c4f3120 100644 --- a/packages/web/src/package/__test__/fetch.apiUrl.test.ts +++ b/packages/web/src/package/__test__/fetch.apiUrl.test.ts @@ -20,9 +20,7 @@ describe('can handle relative urls in apiUrl', () => { apiUrl: '/test', apiKey: 'test', }); - await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual( - new Map() - ); + await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual({}); expect(fetchMock).toHaveBeenCalledTimes(1); const args = fetchMock.mock.calls[0] as any; expect(args[0]).toEqual( @@ -41,9 +39,7 @@ describe('can handle relative urls in apiUrl', () => { apiUrl: 'https://test.com/abcd', apiKey: 'test', }); - await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual( - new Map() - ); + await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual({}); expect(fetchMock).toHaveBeenCalledTimes(1); const args = fetchMock.mock.calls[0] as any; expect(args[0]).toEqual( diff --git a/packages/web/src/package/__test__/fetch.fallbacks.test.ts b/packages/web/src/package/__test__/fetch.fallbacks.test.ts index e1fe4e97a3..cc5c3a7c3f 100644 --- a/packages/web/src/package/__test__/fetch.fallbacks.test.ts +++ b/packages/web/src/package/__test__/fetch.fallbacks.test.ts @@ -38,9 +38,7 @@ describe('tolgee with fallback backend fetch', () => { availableLanguages: ['en'], fetch: f.infiniteFetch, }); - await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual( - new Map() - ); + await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual({}); expect(f.infiniteFetch).toHaveBeenCalledTimes(3); }); @@ -56,9 +54,9 @@ describe('tolgee with fallback backend fetch', () => { en: { test: 'test' }, }, }); - await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual( - new Map([['test', 'test']]) - ); + await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual({ + test: 'test', + }); expect(f.infiniteFetch).toHaveBeenCalledTimes(2); }); @@ -74,9 +72,9 @@ describe('tolgee with fallback backend fetch', () => { en: () => Promise.resolve({ test: 'test' }), }, }); - await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual( - new Map([['test', 'test']]) - ); + await expect(tolgee.loadRecord({ language: 'en' })).resolves.toEqual({ + test: 'test', + }); expect(f.infiniteFetch).toHaveBeenCalledTimes(2); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50b1260d59..839c94a6eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -546,6 +546,9 @@ importers: globals: specifier: ^15.0.0 version: 15.10.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.14.6)(ts-node@10.9.1) prettier: specifier: ^3.1.1 version: 3.2.5 @@ -1957,14 +1960,14 @@ packages: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.25.7 - '@babel/generator': 7.25.7 + '@babel/generator': 7.26.3 '@babel/helper-compilation-targets': 7.25.7 '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.7) '@babel/helpers': 7.25.7 '@babel/parser': 7.25.7 '@babel/template': 7.25.7 '@babel/traverse': 7.25.7 - '@babel/types': 7.25.7 + '@babel/types': 7.26.3 convert-source-map: 2.0.0 debug: 4.3.7 gensync: 1.0.0-beta.2 @@ -1988,7 +1991,7 @@ packages: '@babel/traverse': 7.26.4 '@babel/types': 7.26.3 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2018,7 +2021,7 @@ packages: resolution: {integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.25.7 + '@babel/types': 7.26.3 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.0.2 @@ -2975,14 +2978,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.7): - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -2990,7 +2985,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.4): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} @@ -3000,14 +2994,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.7): - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -3015,7 +3001,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.19.3): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -3043,14 +3028,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.7): - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: @@ -3058,7 +3035,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.19.3): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} @@ -3089,13 +3065,13 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.7): + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 /@babel/plugin-syntax-decorators@7.19.0(@babel/core@7.19.3): @@ -3183,13 +3159,13 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.25.7 - /@babel/plugin-syntax-import-attributes@7.25.7(@babel/core@7.25.7): + /@babel/plugin-syntax-import-attributes@7.25.7(@babel/core@7.26.0): resolution: {integrity: sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.0): @@ -3209,14 +3185,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.7): - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -3224,7 +3192,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.19.3): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} @@ -3252,14 +3219,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.7): - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -3267,7 +3226,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.19.3): resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} @@ -3288,13 +3246,13 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-jsx@7.25.7(@babel/core@7.25.7): + /@babel/plugin-syntax-jsx@7.25.7(@babel/core@7.26.0): resolution: {integrity: sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.19.3): @@ -3323,14 +3281,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.7): - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -3338,7 +3288,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.19.3): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} @@ -3366,14 +3315,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.7): - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -3381,7 +3322,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.19.3): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -3409,14 +3349,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.7): - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -3424,7 +3356,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.19.3): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -3452,14 +3383,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.7): - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -3467,7 +3390,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.19.3): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -3495,14 +3417,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.7): - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -3510,7 +3424,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.19.3): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -3538,14 +3451,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.7): - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -3553,7 +3458,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.19.3): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} @@ -3584,13 +3488,13 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.7): + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.19.3): @@ -3622,15 +3526,6 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.7): - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.25.7 - '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} @@ -3639,7 +3534,6 @@ packages: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.5 - dev: true /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.24.4): resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} @@ -3650,13 +3544,13 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.5 - /@babel/plugin-syntax-typescript@7.25.7(@babel/core@7.25.7): + /@babel/plugin-syntax-typescript@7.25.7(@babel/core@7.26.0): resolution: {integrity: sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.0): @@ -7706,7 +7600,7 @@ packages: resolution: {integrity: sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.1.2 + '@jest/test-result': 29.7.0 graceful-fs: 4.2.11 jest-haste-map: 29.1.2 slash: 3.0.0 @@ -13845,17 +13739,17 @@ packages: transitivePeerDependencies: - supports-color - /babel-jest@29.7.0(@babel/core@7.25.7): + /babel-jest@29.7.0(@babel/core@7.26.0): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.25.7) + babel-preset-jest: 29.6.3(@babel/core@7.26.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -13923,7 +13817,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.25.7 - '@babel/types': 7.25.7 + '@babel/types': 7.26.3 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.18.3 @@ -13931,8 +13825,8 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/template': 7.25.7 - '@babel/types': 7.25.7 + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.18.3 @@ -14078,27 +13972,27 @@ packages: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.4) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.4) - /babel-preset-current-node-syntax@1.1.0(@babel/core@7.25.7): + /babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.7 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.7) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.25.7) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.7) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.7) - '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.25.7) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.7) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.7) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.7) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.7) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.7) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.7) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.7) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.7) + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) /babel-preset-jest@27.5.1(@babel/core@7.24.4): resolution: {integrity: sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==} @@ -14132,15 +14026,15 @@ packages: babel-plugin-jest-hoist: 29.4.2 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.24.4) - /babel-preset-jest@29.6.3(@babel/core@7.25.7): + /babel-preset-jest@29.6.3(@babel/core@7.26.0): resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.7) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -20705,7 +20599,7 @@ packages: resolution: {integrity: sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==} engines: {node: '>=10'} dependencies: - '@babel/core': 7.24.4 + '@babel/core': 7.26.0 '@babel/parser': 7.24.4 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 @@ -20823,7 +20717,7 @@ packages: dependencies: '@jest/environment': 29.7.0 '@jest/expect': 29.6.3 - '@jest/test-result': 29.1.2 + '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 '@types/node': 18.14.6 chalk: 4.1.2 @@ -21077,11 +20971,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 18.14.6 - babel-jest: 29.7.0(@babel/core@7.25.7) + babel-jest: 29.7.0(@babel/core@7.26.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -21118,11 +21012,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 18.14.6 - babel-jest: 29.7.0(@babel/core@7.25.7) + babel-jest: 29.7.0(@babel/core@7.26.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -21158,11 +21052,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.25.7 + '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 20.11.17 - babel-jest: 29.7.0(@babel/core@7.25.7) + babel-jest: 29.7.0(@babel/core@7.26.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -22021,15 +21915,15 @@ packages: resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.25.7 - '@babel/generator': 7.25.7 - '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.7) - '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.25.7) - '@babel/types': 7.25.7 + '@babel/core': 7.26.0 + '@babel/generator': 7.26.3 + '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.26.0) + '@babel/types': 7.26.3 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.7) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 diff --git a/testapps/next-app-intl/.env b/testapps/next-app-intl/.env new file mode 100644 index 0000000000..8e4341bac5 --- /dev/null +++ b/testapps/next-app-intl/.env @@ -0,0 +1,2 @@ +NEXT_PUBLIC_TOLGEE_API_URL=https://app.tolgee.io +NEXT_PUBLIC_TOLGEE_API_KEY= \ No newline at end of file diff --git a/testapps/next-app-intl/src/app/[locale]/layout.tsx b/testapps/next-app-intl/src/app/[locale]/layout.tsx index 2eb829fcaf..e2b39f81b2 100644 --- a/testapps/next-app-intl/src/app/[locale]/layout.tsx +++ b/testapps/next-app-intl/src/app/[locale]/layout.tsx @@ -1,8 +1,8 @@ +import React, { ReactNode } from 'react'; import { notFound } from 'next/navigation'; -import { ReactNode } from 'react'; import { TolgeeNextProvider } from '@/tolgee/client'; -import { ALL_LANGUAGES, getStaticData } from '@/tolgee/shared'; -import React from 'react'; +import { ALL_LANGUAGES } from '@/tolgee/shared'; +import { getTolgee } from '@/tolgee/server'; type Props = { children: ReactNode; @@ -14,15 +14,13 @@ export default async function LocaleLayout({ children, params }: Props) { if (!ALL_LANGUAGES.includes(locale)) { notFound(); } - - // it's important you provide all data which are needed for initial render - // so current language and also fallback languages + necessary namespaces - const staticData = await getStaticData([locale, 'en']); + const tolgee = await getTolgee(); + const records = await tolgee.loadRequired(); return ( - + {children} diff --git a/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx b/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx index 79765ae1e0..9b9c9635e6 100644 --- a/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx +++ b/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx @@ -40,9 +40,7 @@ export const TranslationMethodsClient = () => { i: , key: 'value', }} - > - Hey - + /> diff --git a/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx b/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx index 5f04566ec9..8d859ef464 100644 --- a/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx +++ b/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx @@ -39,9 +39,7 @@ export const TranslationMethodsServer = async () => { i: , key: 'value', }} - > - Hey - + /> diff --git a/testapps/next-app-intl/src/components/LangSelector.tsx b/testapps/next-app-intl/src/components/LangSelector.tsx index 695df6c643..9dded39136 100644 --- a/testapps/next-app-intl/src/components/LangSelector.tsx +++ b/testapps/next-app-intl/src/components/LangSelector.tsx @@ -12,9 +12,9 @@ export const LangSelector: React.FC = () => { const [_, startTransition] = useTransition(); function onSelectChange(event: ChangeEvent) { - const nextLanguage = event.target.value; + const newLocale = event.target.value; startTransition(() => { - router.replace(pathname, { locale: nextLanguage }); + router.replace(pathname, { locale: newLocale }); }); } return ( diff --git a/testapps/next-app-intl/src/navigation.ts b/testapps/next-app-intl/src/navigation.ts index 8af9279326..1674b72c1b 100644 --- a/testapps/next-app-intl/src/navigation.ts +++ b/testapps/next-app-intl/src/navigation.ts @@ -1,7 +1,8 @@ -import { createSharedPathnamesNavigation } from 'next-intl/navigation'; +import { createNavigation } from 'next-intl/navigation'; import { ALL_LANGUAGES } from './tolgee/shared'; // read more about next-intl library // https://next-intl-docs.vercel.app -export const { Link, redirect, usePathname, useRouter } = - createSharedPathnamesNavigation({ locales: ALL_LANGUAGES }); +export const { Link, redirect, usePathname, useRouter } = createNavigation({ + locales: ALL_LANGUAGES, +}); diff --git a/testapps/next-app-intl/src/tolgee/client.tsx b/testapps/next-app-intl/src/tolgee/client.tsx index d2776d8996..915c8f70bd 100644 --- a/testapps/next-app-intl/src/tolgee/client.tsx +++ b/testapps/next-app-intl/src/tolgee/client.tsx @@ -1,12 +1,16 @@ 'use client'; import { useEffect } from 'react'; -import { TolgeeProvider, TolgeeStaticData } from '@tolgee/react'; +import { + CachePublicRecord, + TolgeeProvider, + TolgeeStaticData, +} from '@tolgee/react'; import { useRouter } from 'next/navigation'; import { TolgeeBase } from './shared'; type Props = { - staticData: TolgeeStaticData; + staticData: TolgeeStaticData | CachePublicRecord[]; language: string; children: React.ReactNode; }; @@ -30,7 +34,6 @@ export const TolgeeNextProvider = ({ return ( diff --git a/testapps/next-app-intl/src/tolgee/server.tsx b/testapps/next-app-intl/src/tolgee/server.tsx index 98e69cab02..15401428e6 100644 --- a/testapps/next-app-intl/src/tolgee/server.tsx +++ b/testapps/next-app-intl/src/tolgee/server.tsx @@ -1,20 +1,18 @@ import { getLocale } from 'next-intl/server'; - -import { TolgeeBase, ALL_LANGUAGES, getStaticData } from './shared'; import { createServerInstance } from '@tolgee/react/server'; +import { TolgeeBase } from './shared'; export const { getTolgee, getTranslate, T } = createServerInstance({ getLocale: getLocale, - createTolgee: async (language) => - TolgeeBase().init({ - // including all languages - // on server we are not concerned about bundle size - staticData: await getStaticData(ALL_LANGUAGES), + createTolgee: async (language) => { + const tolgee = TolgeeBase().init({ observerOptions: { fullKeyEncode: true, }, language, - fetch: async (input, init) => - fetch(input, { ...init, next: { revalidate: 0 } }), - }), + }); + // preload all the languages for the server instance + await tolgee.loadRequired(); + return tolgee; + }, }); diff --git a/testapps/next-app-intl/src/tolgee/shared.ts b/testapps/next-app-intl/src/tolgee/shared.ts index 2e57971c8c..f603d38696 100644 --- a/testapps/next-app-intl/src/tolgee/shared.ts +++ b/testapps/next-app-intl/src/tolgee/shared.ts @@ -8,29 +8,19 @@ export const ALL_LANGUAGES = ['en', 'cs', 'de', 'fr']; export const DEFAULT_LANGUAGE = 'en'; -export async function getStaticData( - languages: string[], - namespaces: string[] = [''] -) { - const result: Record = {}; - for (const lang of languages) { - for (const namespace of namespaces) { - if (namespace) { - result[`${lang}:${namespace}`] = ( - await import(`../../messages/${namespace}/${lang}.json`) - ).default; - } else { - result[lang] = (await import(`../../messages/${lang}.json`)).default; - } - } - } - return result; -} - export function TolgeeBase() { - return Tolgee().use(FormatIcu()).use(DevTools()).updateDefaults({ - apiKey, - apiUrl, - fallbackLanguage: 'en', - }); + return Tolgee() + .use(FormatIcu()) + .use(DevTools()) + .updateDefaults({ + apiKey, + apiUrl, + fallbackLanguage: 'en', + staticData: { + en: () => import('../../messages/en.json'), + cs: () => import('../../messages/cs.json'), + de: () => import('../../messages/de.json'), + fr: () => import('../../messages/fr.json'), + }, + }); } diff --git a/testapps/next-app/.env b/testapps/next-app/.env new file mode 100644 index 0000000000..8e4341bac5 --- /dev/null +++ b/testapps/next-app/.env @@ -0,0 +1,2 @@ +NEXT_PUBLIC_TOLGEE_API_URL=https://app.tolgee.io +NEXT_PUBLIC_TOLGEE_API_KEY= \ No newline at end of file diff --git a/testapps/next-app/src/app/layout.tsx b/testapps/next-app/src/app/layout.tsx index 4197172603..d9e3314011 100644 --- a/testapps/next-app/src/app/layout.tsx +++ b/testapps/next-app/src/app/layout.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; import { TolgeeNextProvider } from '@/tolgee/client'; -import { getStaticData } from '@/tolgee/shared'; +import { getTolgee } from '@/tolgee/server'; import { getLanguage } from '@/tolgee/language'; import './style.css'; @@ -9,15 +9,14 @@ type Props = { }; export default async function LocaleLayout({ children }: Props) { - const locale = await getLanguage(); - // it's important you provide all data which are needed for initial render - // so current language and also fallback languages + necessary namespaces - const staticData = await getStaticData([locale, 'en']); + const language = await getLanguage(); + const tolgee = await getTolgee(); + const staticData = await tolgee.loadRequired(); return ( - + - + {children} diff --git a/testapps/next-app/src/tolgee/client.tsx b/testapps/next-app/src/tolgee/client.tsx index 243871f43d..3bc9f5ba74 100644 --- a/testapps/next-app/src/tolgee/client.tsx +++ b/testapps/next-app/src/tolgee/client.tsx @@ -1,13 +1,17 @@ 'use client'; import { TolgeeBase } from './shared'; -import { TolgeeProvider, TolgeeStaticData } from '@tolgee/react'; +import { + CachePublicRecord, + TolgeeProvider, + TolgeeStaticData, +} from '@tolgee/react'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; type Props = { language: string; - staticData: TolgeeStaticData; + staticData: TolgeeStaticData | CachePublicRecord[]; children: React.ReactNode; }; @@ -30,7 +34,6 @@ export const TolgeeNextProvider = ({ return ( - TolgeeBase().init({ - // including all locales - // on server we are not concerned about bundle size - staticData: await getStaticData(ALL_LANGUAGES), + createTolgee: async (language) => { + const tolgee = TolgeeBase().init({ observerOptions: { fullKeyEncode: true, }, - language: locale, - fetch: async (input, init) => - fetch(input, { ...init, next: { revalidate: 0 } }), - }), + language, + }); + await tolgee.loadRequired(); + return tolgee; + }, }); diff --git a/testapps/next-app/src/tolgee/shared.ts b/testapps/next-app/src/tolgee/shared.ts index 2e57971c8c..f603d38696 100644 --- a/testapps/next-app/src/tolgee/shared.ts +++ b/testapps/next-app/src/tolgee/shared.ts @@ -8,29 +8,19 @@ export const ALL_LANGUAGES = ['en', 'cs', 'de', 'fr']; export const DEFAULT_LANGUAGE = 'en'; -export async function getStaticData( - languages: string[], - namespaces: string[] = [''] -) { - const result: Record = {}; - for (const lang of languages) { - for (const namespace of namespaces) { - if (namespace) { - result[`${lang}:${namespace}`] = ( - await import(`../../messages/${namespace}/${lang}.json`) - ).default; - } else { - result[lang] = (await import(`../../messages/${lang}.json`)).default; - } - } - } - return result; -} - export function TolgeeBase() { - return Tolgee().use(FormatIcu()).use(DevTools()).updateDefaults({ - apiKey, - apiUrl, - fallbackLanguage: 'en', - }); + return Tolgee() + .use(FormatIcu()) + .use(DevTools()) + .updateDefaults({ + apiKey, + apiUrl, + fallbackLanguage: 'en', + staticData: { + en: () => import('../../messages/en.json'), + cs: () => import('../../messages/cs.json'), + de: () => import('../../messages/de.json'), + fr: () => import('../../messages/fr.json'), + }, + }); } diff --git a/testapps/next/src/pages/_app.tsx b/testapps/next/src/pages/_app.tsx index faab8c61ae..4108307fd3 100644 --- a/testapps/next/src/pages/_app.tsx +++ b/testapps/next/src/pages/_app.tsx @@ -1,11 +1,11 @@ import type { AppContext, AppInitialProps, AppProps } from 'next/app'; import { useRouter } from 'next/router'; -import { TolgeeProvider, TolgeeStaticData } from '@tolgee/react'; +import { TolgeeProvider, TolgeeStaticDataProp } from '@tolgee/react'; -import { getStaticData, tolgee } from '../tolgee'; +import { tolgee } from '../tolgee'; import App from 'next/app'; -type AppOwnProps = { staticData: TolgeeStaticData }; +type AppOwnProps = { staticData: TolgeeStaticDataProp }; export default function MyApp({ Component, @@ -29,6 +29,8 @@ MyApp.getInitialProps = async ( const ctx = await App.getInitialProps(context); return { ...ctx, - staticData: await getStaticData([context.ctx.locale!], ['', 'namespaced']), + staticData: await tolgee.loadRequired({ + language: context.ctx.locale!, + }), }; }; diff --git a/testapps/next/src/tolgee.ts b/testapps/next/src/tolgee.ts index e5dbfdbed4..b01f6daef4 100644 --- a/testapps/next/src/tolgee.ts +++ b/testapps/next/src/tolgee.ts @@ -1,25 +1,6 @@ import { FormatIcu } from '@tolgee/format-icu'; import { Tolgee, DevTools } from '@tolgee/react'; -export async function getStaticData( - languages: string[], - namespaces: string[] = [''] -) { - const result: Record = {}; - for (const lang of languages) { - for (const namespace of namespaces) { - if (namespace) { - result[`${lang}:${namespace}`] = ( - await import(`../messages/${namespace}/${lang}.json`) - ).default; - } else { - result[lang] = (await import(`../messages/${lang}.json`)).default; - } - } - } - return result; -} - export const tolgee = Tolgee() .use(FormatIcu()) .use(DevTools()) @@ -28,4 +9,14 @@ export const tolgee = Tolgee() defaultLanguage: 'en', apiKey: process.env.NEXT_PUBLIC_TOLGEE_API_KEY, apiUrl: process.env.NEXT_PUBLIC_TOLGEE_API_URL, + staticData: { + en: () => import('../messages/en.json'), + cs: () => import('../messages/cs.json'), + de: () => import('../messages/de.json'), + fr: () => import('../messages/fr.json'), + 'en:namespaced': () => import('../messages/namespaced/en.json'), + 'cs:namespaced': () => import('../messages/namespaced/cs.json'), + 'de:namespaced': () => import('../messages/namespaced/de.json'), + 'fr:namespaced': () => import('../messages/namespaced/fr.json'), + }, }); diff --git a/testapps/react/src/App.tsx b/testapps/react/src/App.tsx index 13a1ff8326..2cb15658fe 100644 --- a/testapps/react/src/App.tsx +++ b/testapps/react/src/App.tsx @@ -21,7 +21,7 @@ export const App = () => { const currentRoute = window.location.pathname; return ( - + {currentRoute === '/translation-methods' ? ( ) : ( diff --git a/testapps/vue-ssr/.gitignore b/testapps/vue-ssr/.gitignore index 2a7e39d037..4b09cd3851 100644 --- a/testapps/vue-ssr/.gitignore +++ b/testapps/vue-ssr/.gitignore @@ -28,3 +28,6 @@ coverage *.sw? tsconfig.tsbuildinfo + +vite.config.ts.* +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/testapps/vue-ssr/pages/+Layout.vue b/testapps/vue-ssr/pages/+Layout.vue index 5a1cdcd594..91e5f46d78 100644 --- a/testapps/vue-ssr/pages/+Layout.vue +++ b/testapps/vue-ssr/pages/+Layout.vue @@ -1,5 +1,5 @@