Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/eleven-monkeys-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'gt-react': patch
'gt-next': patch
---

fix: return type for t.obj
2 changes: 1 addition & 1 deletion packages/next/src/provider/GTProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default async function GTProvider({
}

// Merge dictionary with dictionary translations
dictionary = mergeDictionaries(dictionary, dictionaryTranslations);
// dictionary = mergeDictionaries(dictionary, dictionaryTranslations);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Translation Merge Code Commented Out

The dictionary merge operation was commented out, disabling the merging of dictionary translations with the base dictionary. This prevents translations from being applied properly and appears to be accidentally committed debugging code.

Fix in Cursor Fix in Web


// Block until cache check resolves
const translations = await cachedTranslationsPromise;
Expand Down
10 changes: 10 additions & 0 deletions packages/next/src/server-dir/buildtime/getTranslations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: This assumes dictionaryTranslations is not null due to the ! operator, but it could be undefined based on line 67-69 logic

return renderContent(entry, [defaultLocale]);
}

try {
// Translate on demand
I18NConfig.translateIcu({
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion packages/react/rollup.base.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
12 changes: 6 additions & 6 deletions packages/react/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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: [
Expand Down
15 changes: 9 additions & 6 deletions packages/react/src/dictionaries/collectUntranslatedEntries.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,19 +15,19 @@ 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]) => {
const wholeId = id ? `${id}.${key}` : key;
if (isDictionaryEntry(value)) {
const { entry, metadata } = getEntryAndMetadata(value);

if (!get(translationsDictionary, key)) {
if (get(translationsDictionary, key) === undefined) {
untranslatedEntries.push({
source: entry,
metadata: {
Expand All @@ -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
)
);
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/dictionaries/getDictionaryEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Array Validation Fails for Non-String Primitives

The array validation logic in isValidDictionaryEntry and isDictionaryEntry incorrectly allows non-string, non-null primitive types as the first element of a dictionary entry array. The condition typeof value[0] === 'object' && value[0] !== null only rejects non-null objects, contradicting the expected string or null type for the first element.

Additional Locations (1)

Fix in Cursor Fix in Web

return false;
Comment on lines +12 to 13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The validation logic has been inverted - now rejects objects (except null) instead of accepting only strings. This creates inconsistency with isDictionaryEntry function which uses different validation rules for the same data structure.

}
const provisionalMetadata = value?.[1];
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/dictionaries/getEntryAndMetadata.ts
Original file line number Diff line number Diff line change
@@ -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)) {
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/dictionaries/getSubtree.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Dictionary>({
dictionary,
id,
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/dictionaries/indexDict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: This change allows null dictionaries to pass through, but accessing properties on null (line 12: dictionary[id as number] or line 14: dictionary[id as string]) will cause runtime errors. Consider adding explicit null handling.

Suggested change
if (dictionary === undefined) {
if (dictionary === undefined || dictionary === null) {

throw new Error('Cannot index into an undefined dictionary');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Error message should be updated to reflect that only undefined dictionaries are rejected, or the function should handle null dictionaries explicitly.

}
if (Array.isArray(dictionary)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/dictionaries/injectEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,7 +42,7 @@ export function injectEntry(
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,
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/dictionaries/injectFallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/dictionaries/injectHashes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/dictionaries/injectTranslations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/dictionaries/isDictionaryEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Dictionary Validation Fails for Non-Object Primitives

The validation logic in isValidDictionaryEntry and isDictionaryEntry is too permissive. The condition value === null || typeof value !== 'object' incorrectly validates any non-object primitive (e.g., numbers, booleans, undefined) as a DictionaryEntry. This contradicts the Entry type (string | null), creating a type safety mismatch and potential runtime errors.

Additional Locations (2)

Fix in Cursor Fix in Web

return true;
}

Expand All @@ -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;
}

Expand Down
14 changes: 4 additions & 10 deletions packages/react/src/dictionaries/mergeDictionaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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]);
}
Comment on lines +121 to +123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: This type check is redundant since line 59 already handles non-string entries. Consider removing or add a comment explaining why this additional check is necessary.


// Translate Content
registerIcuForTranslation({
source: entry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading