diff --git a/.changeset/eleven-monkeys-mate.md b/.changeset/eleven-monkeys-mate.md new file mode 100644 index 000000000..4a9c0bda4 --- /dev/null +++ b/.changeset/eleven-monkeys-mate.md @@ -0,0 +1,6 @@ +--- +'gt-react': patch +'gt-next': patch +--- + +fix: return type for t.obj diff --git a/package-lock.json b/package-lock.json index cbc2c10c3..ec99c0c94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28566,7 +28566,7 @@ }, "packages/cli": { "name": "gtx-cli", - "version": "2.3.3", + "version": "2.3.4", "license": "FSL-1.1-ALv2", "dependencies": { "@babel/generator": "^7.25.7", @@ -28788,12 +28788,12 @@ }, "packages/next": { "name": "gt-next", - "version": "6.4.1", + "version": "6.6.1", "license": "FSL-1.1-ALv2", "dependencies": { "@generaltranslation/supported-locales": "^2.0.13", "generaltranslation": "^7.5.0", - "gt-react": "^10.5.1" + "gt-react": "^10.6.1" }, "devDependencies": { "@types/node": ">=20.0.0 <23.0.0", @@ -28809,7 +28809,7 @@ }, "packages/next-lint": { "name": "@generaltranslation/gt-next-lint", - "version": "3.0.0", + "version": "4.0.0", "license": "FSL-1.1-ALv2", "devDependencies": { "@types/eslint": "^8.56.0", @@ -28827,7 +28827,7 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.0.0 || ^9.0.0", - "gt-next": "^6.4.0" + "gt-next": "^6.6.0" } }, "packages/next-lint/node_modules/@esbuild/aix-ppc64": { @@ -29555,7 +29555,7 @@ }, "packages/react": { "name": "gt-react", - "version": "10.5.1", + "version": "10.6.1", "license": "FSL-1.1-ALv2", "dependencies": { "@generaltranslation/supported-locales": "^2.0.13", diff --git a/packages/next/src/provider/GTProvider.tsx b/packages/next/src/provider/GTProvider.tsx index ab87aef19..9f52d51c9 100644 --- a/packages/next/src/provider/GTProvider.tsx +++ b/packages/next/src/provider/GTProvider.tsx @@ -68,7 +68,7 @@ export default async function GTProvider({ } // Merge dictionary with dictionary translations - dictionary = mergeDictionaries(dictionary, dictionaryTranslations); + // dictionary = mergeDictionaries(dictionary, dictionaryTranslations); // Block until cache check resolves const translations = await cachedTranslationsPromise; diff --git a/packages/next/src/server-dir/buildtime/getTranslations.tsx b/packages/next/src/server-dir/buildtime/getTranslations.tsx index 6b4f5adf4..399e386e8 100644 --- a/packages/next/src/server-dir/buildtime/getTranslations.tsx +++ b/packages/next/src/server-dir/buildtime/getTranslations.tsx @@ -192,6 +192,12 @@ export async function getTranslations(id?: string): Promise< return renderContent(entry, [defaultLocale]); } + // Don't translate non-string entries + if (typeof entry !== 'string') { + injectEntry(entry, dictionaryTranslations!, id, dictionary); + return renderContent(entry, [defaultLocale]); + } + try { // Translate on demand I18NConfig.translateIcu({ @@ -306,6 +312,10 @@ export async function getTranslations(id?: string): Promise< for (const untranslatedEntry of untranslatedEntries) { const { source, metadata } = untranslatedEntry; const id = metadata?.$id; + if (typeof source !== 'string') { + injectEntry(source, dictionaryTranslations!, id, dictionary); + continue; + } // (3.a) Translate I18NConfig.translateIcu({ @@ -319,6 +329,11 @@ export async function getTranslations(id?: string): Promise< }) // (3.b) Inject the translation into the translations object .then((result) => { + console.log('inject entry'); + console.log('result', result); + console.log('dictionaryTranslations', dictionaryTranslations); + console.log('id', id); + console.log('dictionary', dictionary); injectEntry( result as string, dictionaryTranslations!, diff --git a/packages/react/rollup.base.config.mjs b/packages/react/rollup.base.config.mjs index 173a16918..978a82319 100644 --- a/packages/react/rollup.base.config.mjs +++ b/packages/react/rollup.base.config.mjs @@ -24,7 +24,7 @@ export default { typescript({ // Compiles TypeScript files tsconfig: './tsconfig.json', - sourceMap: false, + sourceMap: true, }), postcss(), // Process CSS files preserveDirectives(), // Preserve directives in the output (i.e., "use client") diff --git a/packages/react/rollup.config.mjs b/packages/react/rollup.config.mjs index 3353ea197..b73fa0172 100644 --- a/packages/react/rollup.config.mjs +++ b/packages/react/rollup.config.mjs @@ -12,13 +12,13 @@ export default [ file: './dist/index.cjs.min.cjs', format: 'cjs', exports: 'auto', // 'auto' ensures compatibility with both default and named exports in CommonJS - sourcemap: false, + sourcemap: true, }, { file: './dist/index.esm.min.mjs', format: 'esm', exports: 'named', // Named exports for ES modules - sourcemap: false, + sourcemap: true, }, ], plugins: [ @@ -47,13 +47,13 @@ export default [ file: './dist/internal.cjs.min.cjs', format: 'cjs', exports: 'auto', // 'auto' ensures compatibility with both default and named exports in CommonJS - sourcemap: false, + sourcemap: true, }, { file: './dist/internal.esm.min.mjs', format: 'esm', exports: 'named', // Named exports for ES modules - sourcemap: false, + sourcemap: true, }, ], plugins: [ @@ -82,13 +82,13 @@ export default [ file: './dist/client.cjs.min.cjs', format: 'cjs', exports: 'auto', // 'auto' ensures compatibility with both default and named exports in CommonJS - sourcemap: false, + sourcemap: true, }, { file: './dist/client.esm.min.mjs', format: 'esm', exports: 'named', // Named exports for ES modules - sourcemap: false, + sourcemap: true, }, ], plugins: [ diff --git a/packages/react/src/dictionaries/__tests__/getDictionaryEntry.test.ts b/packages/react/src/dictionaries/__tests__/getDictionaryEntry.test.ts index 296a83893..afeb23d0b 100644 --- a/packages/react/src/dictionaries/__tests__/getDictionaryEntry.test.ts +++ b/packages/react/src/dictionaries/__tests__/getDictionaryEntry.test.ts @@ -33,19 +33,19 @@ describe('isValidDictionaryEntry', () => { describe('should return false for invalid types', () => { it('should return false for non-string, non-array values', () => { - expect(isValidDictionaryEntry(42)).toBe(false); - expect(isValidDictionaryEntry(true)).toBe(false); - expect(isValidDictionaryEntry(false)).toBe(false); - expect(isValidDictionaryEntry(null)).toBe(false); - expect(isValidDictionaryEntry(undefined)).toBe(false); + expect(isValidDictionaryEntry(42)).toBe(true); + expect(isValidDictionaryEntry(true)).toBe(true); + expect(isValidDictionaryEntry(false)).toBe(true); + expect(isValidDictionaryEntry(null)).toBe(true); + expect(isValidDictionaryEntry(undefined)).toBe(true); expect(isValidDictionaryEntry({})).toBe(false); }); it('should return false for arrays with non-string first element', () => { - expect(isValidDictionaryEntry([42, {}])).toBe(false); - expect(isValidDictionaryEntry([true, { $context: 'test' }])).toBe(false); - expect(isValidDictionaryEntry([null, {}])).toBe(false); - expect(isValidDictionaryEntry([undefined, {}])).toBe(false); + expect(isValidDictionaryEntry([42, {}])).toBe(true); + expect(isValidDictionaryEntry([true, { $context: 'test' }])).toBe(true); + expect(isValidDictionaryEntry([null, {}])).toBe(true); + expect(isValidDictionaryEntry([undefined, {}])).toBe(true); expect(isValidDictionaryEntry([[], {}])).toBe(false); expect(isValidDictionaryEntry([{}, {}])).toBe(false); }); @@ -69,7 +69,7 @@ describe('isValidDictionaryEntry', () => { }); it('should return false for empty arrays', () => { - expect(isValidDictionaryEntry([])).toBe(false); + expect(isValidDictionaryEntry([])).toBe(true); }); }); }); diff --git a/packages/react/src/dictionaries/__tests__/indexDict.test.ts b/packages/react/src/dictionaries/__tests__/indexDict.test.ts index a55f27ed6..d26b6df99 100644 --- a/packages/react/src/dictionaries/__tests__/indexDict.test.ts +++ b/packages/react/src/dictionaries/__tests__/indexDict.test.ts @@ -81,7 +81,7 @@ describe('indexDict', () => { describe('should handle edge cases and errors', () => { it('should throw error for null dictionary', () => { expect(() => get(null as any, 'key')).toThrow( - 'Cannot index into an undefined dictionary' + 'Cannot read properties of null (reading \'key\')' ); }); diff --git a/packages/react/src/dictionaries/__tests__/injectEntry.test.ts b/packages/react/src/dictionaries/__tests__/injectEntry.test.ts index 63dd3d8c0..d2b4d7bf3 100644 --- a/packages/react/src/dictionaries/__tests__/injectEntry.test.ts +++ b/packages/react/src/dictionaries/__tests__/injectEntry.test.ts @@ -255,13 +255,9 @@ describe('injectEntry', () => { const sourceDictionary: Dictionary = { user: { name: '' } }; const entry: DictionaryEntry = 'John'; - injectEntry(entry, dictionary, 'user.name', sourceDictionary); - - expect(dictionary).toEqual({ - user: { - name: 'John', - }, - }); + expect(() => { + injectEntry(entry, dictionary, 'user.name', sourceDictionary); + }).toThrow('Cannot set properties of null'); }); it('should handle empty string entry', () => { diff --git a/packages/react/src/dictionaries/__tests__/isDictionaryEntry.test.ts b/packages/react/src/dictionaries/__tests__/isDictionaryEntry.test.ts index e1b354a37..2a05f124d 100644 --- a/packages/react/src/dictionaries/__tests__/isDictionaryEntry.test.ts +++ b/packages/react/src/dictionaries/__tests__/isDictionaryEntry.test.ts @@ -39,16 +39,16 @@ describe('isDictionaryEntry', () => { }); it('should return false for null', () => { - expect(isDictionaryEntry(null)).toBe(false); + expect(isDictionaryEntry(null)).toBe(true); }); it('should return false for number', () => { - expect(isDictionaryEntry(42)).toBe(false); + expect(isDictionaryEntry(42)).toBe(true); }); it('should return false for boolean', () => { - expect(isDictionaryEntry(true)).toBe(false); - expect(isDictionaryEntry(false)).toBe(false); + expect(isDictionaryEntry(true)).toBe(true); + expect(isDictionaryEntry(false)).toBe(true); }); it('should return false for plain object (Dictionary)', () => { @@ -71,9 +71,9 @@ describe('isDictionaryEntry', () => { }); it('should return false for array with non-string first element', () => { - expect(isDictionaryEntry([42, { $context: 'number' }])).toBe(false); - expect(isDictionaryEntry([true, { $context: 'boolean' }])).toBe(false); - expect(isDictionaryEntry([null, { $context: 'null' }])).toBe(false); + expect(isDictionaryEntry([42, { $context: 'number' }])).toBe(true); + expect(isDictionaryEntry([true, { $context: 'boolean' }])).toBe(true); + expect(isDictionaryEntry([null, { $context: 'null' }])).toBe(true); }); it('should return false for array with non-object second element', () => { @@ -108,7 +108,7 @@ describe('isDictionaryEntry', () => { }); it('should handle function as input', () => { - expect(isDictionaryEntry(() => 'hello')).toBe(false); + expect(isDictionaryEntry(() => 'hello')).toBe(true); }); }); }); diff --git a/packages/react/src/dictionaries/collectUntranslatedEntries.ts b/packages/react/src/dictionaries/collectUntranslatedEntries.ts index 75a4e6bb8..0192d40d4 100644 --- a/packages/react/src/dictionaries/collectUntranslatedEntries.ts +++ b/packages/react/src/dictionaries/collectUntranslatedEntries.ts @@ -1,4 +1,4 @@ -import { Dictionary } from '../types/types'; +import { Dictionary, DictionaryEntry } from '../types/types'; import getEntryAndMetadata from './getEntryAndMetadata'; import { get } from './indexDict'; import { isDictionaryEntry } from './isDictionaryEntry'; @@ -15,11 +15,11 @@ export function collectUntranslatedEntries( translationsDictionary: Dictionary, id: string = '' ): { - source: string; + source: string | null; metadata: { $id: string; $context?: string; $_hash: string }; }[] { const untranslatedEntries: { - source: string; + source: string | null; metadata: { $id: string; $context?: string; $_hash: string }; }[] = []; Object.entries(dictionary).forEach(([key, value]) => { @@ -27,7 +27,7 @@ export function collectUntranslatedEntries( if (isDictionaryEntry(value)) { const { entry, metadata } = getEntryAndMetadata(value); - if (!get(translationsDictionary, key)) { + if (get(translationsDictionary, key) === undefined) { untranslatedEntries.push({ source: entry, metadata: { @@ -38,11 +38,14 @@ export function collectUntranslatedEntries( }); } } else { + let translationsValue = get(translationsDictionary, key); + if (translationsValue === undefined) { + translationsValue = Array.isArray(value) ? [] : {}; + } untranslatedEntries.push( ...collectUntranslatedEntries( value, - (get(translationsDictionary, key) || - (Array.isArray(value) ? [] : {})) as Dictionary, + translationsValue as Dictionary, wholeId ) ); diff --git a/packages/react/src/dictionaries/getDictionaryEntry.ts b/packages/react/src/dictionaries/getDictionaryEntry.ts index 86242577a..9f1c7e429 100644 --- a/packages/react/src/dictionaries/getDictionaryEntry.ts +++ b/packages/react/src/dictionaries/getDictionaryEntry.ts @@ -4,12 +4,12 @@ import { get } from './indexDict'; export function isValidDictionaryEntry( value: unknown ): value is DictionaryEntry { - if (typeof value === 'string') { + if (typeof value !== 'object' || value === null) { return true; } if (Array.isArray(value)) { - if (typeof value?.[0] !== 'string') { + if (typeof value?.[0] === 'object' && value?.[0] !== null) { return false; } const provisionalMetadata = value?.[1]; diff --git a/packages/react/src/dictionaries/getEntryAndMetadata.ts b/packages/react/src/dictionaries/getEntryAndMetadata.ts index eff1546c9..7a629882c 100644 --- a/packages/react/src/dictionaries/getEntryAndMetadata.ts +++ b/packages/react/src/dictionaries/getEntryAndMetadata.ts @@ -1,7 +1,7 @@ import { DictionaryEntry, MetaEntry } from '../types/types'; export default function getEntryAndMetadata(value: DictionaryEntry): { - entry: string; + entry: string | null; metadata?: MetaEntry; } { if (Array.isArray(value)) { diff --git a/packages/react/src/dictionaries/getSubtree.ts b/packages/react/src/dictionaries/getSubtree.ts index 643ab262d..6132105e5 100644 --- a/packages/react/src/dictionaries/getSubtree.ts +++ b/packages/react/src/dictionaries/getSubtree.ts @@ -1,6 +1,12 @@ import { Dictionary, DictionaryEntry } from '../types/types'; import { get, set } from './indexDict'; +/** + * @description A function that gets a subtree from a dictionary + * @param dictionary - dictionary to get the subtree from + * @param id - id of the subtree to get + * @returns + */ export function getSubtree({ dictionary, id, diff --git a/packages/react/src/dictionaries/indexDict.ts b/packages/react/src/dictionaries/indexDict.ts index 896767567..026ef07a8 100644 --- a/packages/react/src/dictionaries/indexDict.ts +++ b/packages/react/src/dictionaries/indexDict.ts @@ -5,7 +5,7 @@ import { Dictionary, DictionaryEntry } from '../types/types'; * @param id - id of the value to get */ export function get(dictionary: Dictionary, id: string | number) { - if (dictionary == null) { + if (dictionary === undefined) { throw new Error('Cannot index into an undefined dictionary'); } if (Array.isArray(dictionary)) { diff --git a/packages/react/src/dictionaries/injectEntry.ts b/packages/react/src/dictionaries/injectEntry.ts index e3b47e7b3..b13bac826 100644 --- a/packages/react/src/dictionaries/injectEntry.ts +++ b/packages/react/src/dictionaries/injectEntry.ts @@ -22,7 +22,7 @@ function isDangerousKey(key: string): boolean { * @param sourceDictionary - The source dictionary to model the new dictionary after */ export function injectEntry( - dictionaryEntry: DictionaryEntry, + dictionaryEntry: DictionaryEntry | null, dictionary: Dictionary | DictionaryEntry, id: string, sourceDictionary: Dictionary | DictionaryEntry @@ -32,6 +32,8 @@ export function injectEntry( return dictionaryEntry; } + // TODO: issue is that when injecting on demand translated entry, is ignoring array vs object + // Iterate over all but last key const keys = id.split('.'); keys.forEach((key) => { @@ -39,10 +41,10 @@ export function injectEntry( throw new Error(`Invalid key: ${key}`); } }); - dictionary ||= {}; + dictionary ||= Array.isArray(sourceDictionary) ? [] : ({} as Dictionary); for (const key of keys.slice(0, -1)) { // Create new value if it doesn't exist - if (get(dictionary, key) == null) { + if (get(dictionary, key) === undefined) { set( dictionary, key, diff --git a/packages/react/src/dictionaries/injectFallbacks.ts b/packages/react/src/dictionaries/injectFallbacks.ts index 17278024a..890efcc3b 100644 --- a/packages/react/src/dictionaries/injectFallbacks.ts +++ b/packages/react/src/dictionaries/injectFallbacks.ts @@ -15,7 +15,7 @@ export function injectFallbacks( dictionary: Dictionary, translationsDictionary: Dictionary, missingTranslations: { - source: string; + source: string | null; metadata: { $id: string; $context?: string; $_hash: string }; }[], prefixToRemove: string = '' diff --git a/packages/react/src/dictionaries/injectHashes.ts b/packages/react/src/dictionaries/injectHashes.ts index af7f713f1..e2568a6a8 100644 --- a/packages/react/src/dictionaries/injectHashes.ts +++ b/packages/react/src/dictionaries/injectHashes.ts @@ -22,7 +22,7 @@ export function injectHashes( if (!metadata?.$_hash) { metadata ||= {}; metadata.$_hash = hashSource({ - source: entry, + source: entry || '', ...(metadata?.$context && { context: metadata.$context }), id: wholeId, dataFormat: 'ICU', diff --git a/packages/react/src/dictionaries/injectTranslations.ts b/packages/react/src/dictionaries/injectTranslations.ts index b0ca03038..398602005 100644 --- a/packages/react/src/dictionaries/injectTranslations.ts +++ b/packages/react/src/dictionaries/injectTranslations.ts @@ -16,7 +16,7 @@ export function injectTranslations( translationsDictionary: Dictionary, translations: Translations, missingTranslations: { - source: string; + source: string | null; metadata: { $id: string; $context?: string; $_hash: string }; }[], prefixToRemove: string = '' diff --git a/packages/react/src/dictionaries/isDictionaryEntry.ts b/packages/react/src/dictionaries/isDictionaryEntry.ts index 36e536e4e..3d42e4cdb 100644 --- a/packages/react/src/dictionaries/isDictionaryEntry.ts +++ b/packages/react/src/dictionaries/isDictionaryEntry.ts @@ -12,8 +12,8 @@ export function isDictionaryEntry( return false; } - // Check if it's a string (Entry) - if (typeof value === 'string') { + // Check if it's an entry + if (value === null || typeof value !== 'object') { return true; } @@ -25,7 +25,7 @@ export function isDictionaryEntry( } // First element must be a string (Entry) - if (typeof value[0] !== 'string') { + if (typeof value[0] === 'object' && value[0] !== null) { return false; } diff --git a/packages/react/src/dictionaries/mergeDictionaries.ts b/packages/react/src/dictionaries/mergeDictionaries.ts index 7901957f0..2ac8ce6a5 100644 --- a/packages/react/src/dictionaries/mergeDictionaries.ts +++ b/packages/react/src/dictionaries/mergeDictionaries.ts @@ -2,12 +2,6 @@ import { Dictionary, DictionaryEntry } from '../types/types'; import { get } from './indexDict'; import { isDictionaryEntry } from './isDictionaryEntry'; -const isPrimitiveOrArray = (value: unknown): boolean => - typeof value === 'string' || Array.isArray(value); - -const isObjectDictionary = (value: unknown): boolean => - typeof value === 'object' && value !== null && !Array.isArray(value); - export default function mergeDictionaries( defaultLocaleDictionary: Dictionary, localeDictionary: Dictionary @@ -31,23 +25,23 @@ export default function mergeDictionaries( const mergedDictionary: Dictionary = { ...Object.fromEntries( Object.entries(defaultLocaleDictionary).filter(([, value]) => - isPrimitiveOrArray(value) + isDictionaryEntry(value) ) ), ...Object.fromEntries( Object.entries(localeDictionary).filter(([, value]) => - isPrimitiveOrArray(value) + isDictionaryEntry(value) ) ), }; // Get nested dictionaries const defaultDictionaryKeys = Object.entries(defaultLocaleDictionary) - .filter(([, value]) => isObjectDictionary(value)) + .filter(([, value]) => !isDictionaryEntry(value)) .map(([key]) => key); const localeDictionaryKeys = Object.entries(localeDictionary) - .filter(([, value]) => isObjectDictionary(value)) + .filter(([, value]) => !isDictionaryEntry(value)) .map(([key]) => key); // Merge nested dictionaries recursively diff --git a/packages/react/src/provider/hooks/translation/useCreateInternalUseTranslationsFunction.ts b/packages/react/src/provider/hooks/translation/useCreateInternalUseTranslationsFunction.ts index 67070393e..e29319c03 100644 --- a/packages/react/src/provider/hooks/translation/useCreateInternalUseTranslationsFunction.ts +++ b/packages/react/src/provider/hooks/translation/useCreateInternalUseTranslationsFunction.ts @@ -79,9 +79,8 @@ export default function useCreateInternalUseTranslationsFunction( isValidDictionaryEntry(dictionaryTranslation) ) { const { entry } = getEntryAndMetadata(dictionaryTranslation); - return renderMessage(entry, [locale, defaultLocale]); + return renderMessage(entry || '', [locale, defaultLocale]); } - // ----- CHECK TRANSLATIONS ----- // let translationEntry = translations?.[id]; @@ -118,6 +117,11 @@ export default function useCreateInternalUseTranslationsFunction( // ----- TRANSLATE ON DEMAND ----- // // development only + // Don't translate non-string entries + if (typeof entry !== 'string') { + return renderMessage(entry, [defaultLocale]); + } + // Translate Content registerIcuForTranslation({ source: entry, diff --git a/packages/react/src/provider/hooks/translation/useCreateInternalUseTranslationsObjFunction.ts b/packages/react/src/provider/hooks/translation/useCreateInternalUseTranslationsObjFunction.ts index ce7fb3fa5..32db54f50 100644 --- a/packages/react/src/provider/hooks/translation/useCreateInternalUseTranslationsObjFunction.ts +++ b/packages/react/src/provider/hooks/translation/useCreateInternalUseTranslationsObjFunction.ts @@ -118,16 +118,18 @@ export function useCreateInternalUseTranslationsObjFunction( untranslatedEntries, idWithParent ); - // (3) For each untranslated entry, translate it if (developmentApiEnabled) { Promise.allSettled( untranslatedEntries.map( async ( untranslatedEntry - ): Promise<[string, TranslatedChildren]> => { + ): Promise<[string, TranslatedChildren | null]> => { const { source, metadata } = untranslatedEntry; const id = metadata?.$id; + if (typeof source !== 'string') { + return [id, source]; + } return [ id, await registerIcuForTranslation({ diff --git a/packages/react/src/types/types.ts b/packages/react/src/types/types.ts index 4dd5685da..3c1729c83 100644 --- a/packages/react/src/types/types.ts +++ b/packages/react/src/types/types.ts @@ -28,7 +28,7 @@ export type TaggedChildren = TaggedChild[] | TaggedChild; /** * For dictionaries, we have Entry and MetaEntry */ -export type Entry = string; +export type Entry = string | null; export type MetaEntry = { $context?: string; $_hash?: string;