From 742fb24e615ce8c72fcf0f94e8f4adc30ff19379 Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:04:44 +0330 Subject: [PATCH 01/11] chore: update .gitignore to include example directory and fix yarn-error.log entry --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d9a8581..0725d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,6 @@ logs *.log npm-debug.log* yarn-debug.log* -yarn-error.log* \ No newline at end of file +yarn-error.log* + +/example From 564d37d4e21223f513803f5320d5414107af6ccb Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:05:07 +0330 Subject: [PATCH 02/11] docs: enhance README.md with type-safe translation features, update variable injection and component interpolation sections, and clarify usage examples for improved clarity --- README.md | 199 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 130 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 4aaccaa..ba03961 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ A lightweight, professional localization package for React and React Native appl - 🌐 **Simple React Hooks** - Clean, intuitive hooks for translations and language management - 🔄 **Language Switching** - Built-in language switching with persistence -- 📝 **Variable Injection** - Dynamic content insertion in translations -- 🧩 **Component Interpolation** - Embed React components within translations +- 📝 **Type-Safe Variable Injection** - Dynamic content insertion with compile-time parameter validation +- 🧩 **Unified Component Interpolation** - Embed React components within translations using the same `t` function - 🔍 **Translation Validation** - CLI tools to ensure translation consistency - 🔄 **Translation Sync** - Automated synchronization of translation files - 📱 **React Native Support** - Full compatibility with React Native applications -- 🛡️ **Type Safety** - Full TypeScript support with comprehensive type definitions +- 🛡️ **Full Type Safety** - TypeScript support with type-safe keys, parameters, and component interpolation - ⚡ **Performance Optimized** - Lightweight wrapper with minimal overhead ## 📦 Installation @@ -26,27 +26,29 @@ npm install @weprodev/ui-localization #### 1. Create Translation Files -Create a `translations` directory in your project with language files: +Create a `translations` directory in your project with language files. **Important:** Use `as const` to enable type-safe parameter validation: ```typescript // translations/en.ts const en = { common: { - hello: "Hello", - welcome: "Welcome to our app", - goodbye: "Goodbye" + hello: "Hello {{name}}!", + welcome: "Welcome {{name}}!", + goodbye: "Goodbye {{name}}!", + greeting: "Hello, {{name}}!" }, auth: { login: "Login", signup: "Sign Up", - forgotPassword: "Forgot Password" + forgotPassword: "Forgot Password", + welcomeMessage: "Welcome {{name}}! Please sign in to continue." }, dashboard: { title: "Dashboard", summary: "Summary", recentActivity: "Recent Activity" } -}; +} as const; export default en; ``` @@ -55,21 +57,23 @@ export default en; // translations/es.ts const es = { common: { - hello: "Hola", - welcome: "Bienvenido a nuestra aplicación", - goodbye: "Adiós" + hello: "Hola {{name}}!", + welcome: "Bienvenido {{name}}!", + goodbye: "Adiós {{name}}!", + greeting: "¡Hola, {{name}}!" }, auth: { login: "Iniciar sesión", signup: "Registrarse", - forgotPassword: "Contraseña olvidada" + forgotPassword: "Contraseña olvidada", + welcomeMessage: "Bienvenido {{name}}! Por favor inicia sesión para continuar." }, dashboard: { title: "Panel de control", summary: "Resumen", recentActivity: "Actividad reciente" } -}; +} as const; export default es; ``` @@ -133,14 +137,17 @@ import React from 'react'; import { useTranslation } from '@weprodev/ui-localization'; import en from '../translations/en'; -const Welcome: React.FC = () => { - // Type-safe translation hook with intellisense - const t = useTranslation(en); +const Welcome: React.FC<{ name: string }> = ({ name }) => { + // Type-safe translation hook with path-based keys + const { t } = useTranslation(); return (
-

{t.common.welcome}

-

{t.common.hello}

+ {/* TypeScript requires 'name' parameter because translation has {{name}} placeholder */} +

{t('common.welcome', { name })}

+

{t('common.hello', { name })}

+ {/* TypeScript will error if you use invalid keys like t('common.invalid') */} + {/* TypeScript will error if you forget required parameters */}
); }; @@ -156,25 +163,28 @@ import { useTranslation } from '@weprodev/ui-localization'; import en from '../translations/en'; export const useAppTranslation = () => { - return useTranslation(en); + return useTranslation(); }; // Usage in components import { useAppTranslation } from '../hooks/useAppTranslation'; const Welcome: React.FC = () => { - const t = useAppTranslation(); + const { t } = useAppTranslation(); return (
-

{t.common.welcome}

{/* Full intellisense support */} -

{t.common.hello}

+

{t('common.welcome')}

{/* Full intellisense and type safety */} +

{t('common.hello')}

+ {/* t('common.invalid') will show TypeScript error */}
); }; ``` -**Troubleshooting: If you encounter TypeScript errors with the type-safe hook, you can use `useTranslationFallback()` as an escape hatch. See the API Reference for details.** +**Important:** For type-safe parameter validation to work, your translation files must use `as const`. This ensures TypeScript preserves literal string types, allowing the system to extract parameter names from placeholders like `{{name}}`. + +**Troubleshooting:** If you encounter TypeScript errors with the type-safe hook, you can use `useTranslationFallback()` as an escape hatch. See the API Reference for details. #### 5. Language Switching @@ -292,14 +302,14 @@ import { View, Text, StyleSheet } from 'react-native'; import { useTranslation } from '@weprodev/ui-localization'; import en from '../translations/en'; -const Welcome: React.FC = () => { - // Type-safe translation hook with intellisense - const t = useTranslation(en); +const Welcome: React.FC<{ name: string }> = ({ name }) => { + // Type-safe translation hook with path-based keys + const { t } = useTranslation(); return ( - {t.common.welcome} - {t.common.hello} + {t('common.welcome', { name })} + {t('common.hello', { name })} ); }; @@ -400,44 +410,71 @@ export default LanguageSwitcher; ## 🔧 Advanced Usage -### Translation with Variables +### Translation with Variables (Type-Safe Parameters) + +The `t` function enforces type-safe parameters based on placeholders in your translation strings. Parameters are **required** when placeholders exist, and **optional** when they don't. ```typescript -import { useTranslation, useTranslationInjection } from '@weprodev/ui-localization'; +import { useTranslation } from '@weprodev/ui-localization'; import en from '../translations/en'; const Greeting: React.FC<{ name: string }> = ({ name }) => { - const t = useTranslation(en); + const { t } = useTranslation(); + + // Translation: "common.greeting": "Hello, {{name}}!" + // ✅ TypeScript requires the 'name' parameter + const greeting = t('common.greeting', { name }); + + // ❌ TypeScript error: missing required parameter 'name' + // const greeting = t('common.greeting'); - // Translation key: "greeting": "Hello, {{name}}!" - const greeting = useTranslationInjection(t.common.greeting, { name }); + // ❌ TypeScript error: wrong parameter name + // const greeting = t('common.greeting', { wrongName: name }); + + // ✅ No parameters needed for translations without placeholders + const title = t('dashboard.title'); return

{greeting}

; }; ``` -### Translation with Components +### Translation with Component Interpolation + +The unified `t` function supports both string and component interpolation. When you pass a `components` object as the third argument, it returns a React element instead of a string. ```typescript -import { useTranslation, useTranslationWithInterpolation } from '@weprodev/ui-localization'; +import { useTranslation } from '@weprodev/ui-localization'; import en from '../translations/en'; -const TermsAgreement: React.FC<{ name: string }> = ({ name }) => { - const t = useTranslation(en); +const WelcomeMessage: React.FC<{ name: string }> = ({ name }) => { + const { t } = useTranslation(); - // Translation key: "welcome": "Welcome {{name}}" - const welcomeElement = useTranslationWithInterpolation( - t.common.welcome, - { name }, + // Translation: "auth.welcomeMessage": "Welcome {{name}}! Please sign in to continue." + // The translation string must contain matching HTML-like tags (, , etc.) + const welcomeElement = t( + 'auth.welcomeMessage', + { name }, { - strong: + strong: , + link: (props: { children?: React.ReactNode }) => ( + + {props.children} + + ) } ); + // Returns JSX.Element when components are provided return
{welcomeElement}
; }; ``` +**Important Notes:** +- Component interpolation only works when the translation string contains matching HTML-like tags (e.g., ``, ``) +- The component keys in your `components` object must match the tag names in the translation +- For self-closing tags like ``, use self-closing components +- For tags with content like `text`, use function components that accept `props.children` + ## 🛠️ Translation Management Tools @@ -516,21 +553,48 @@ We recommend running `translation:validate` as part of your CI pipeline to ensur ### Hooks -#### `useTranslation(translationLanguage: object)` -Returns a type-safe translation proxy object with intellisense support. +#### `useTranslation()` +Returns a type-safe translation function with intellisense support and type-safe parameter validation. **Parameters:** -- `translationLanguage`: The translation object to provide type safety for +- None (the translation object type `T` is provided as a generic parameter for type safety) + +**Returns:** Object with `t` function that accepts type-safe translation keys -**Returns:** Type-safe translation proxy object +**The `t` function supports two modes:** +1. **String interpolation** (returns `string`): ```typescript import en from '../translations/en'; -const t = useTranslation(en); -const translatedText = t.common.hello; // Type-safe with intellisense +const { t } = useTranslation(); + +// No parameters needed for translations without placeholders +const title = t('dashboard.title'); // ✅ Returns string + +// Parameters required when placeholders exist +const greeting = t('common.greeting', { name: 'John' }); // ✅ Returns string +const greeting = t('common.greeting'); // ❌ TypeScript error: missing required parameter ``` +2. **Component interpolation** (returns `JSX.Element`): +```typescript +// Pass components as third argument +const welcomeElement = t( + 'auth.welcomeMessage', + { name: 'Alice' }, + { + strong: , + link: (props) => {props.children} + } +); // ✅ Returns JSX.Element +``` + +**Type-Safe Parameters:** +- Parameters are **required** when the translation string contains placeholders like `{{name}}` +- Parameters are **optional** when the translation has no placeholders +- TypeScript validates parameter names match the placeholders in the translation + **Note:** For better reusability, consider creating a custom hook: ```typescript @@ -539,8 +603,12 @@ import { useTranslation } from '@weprodev/ui-localization'; import en from '../translations/en'; export const useAppTranslation = () => { - return useTranslation(en); + return useTranslation(); }; + +// Usage +const { t } = useAppTranslation(); +const text = t('common.welcome', { name: 'User' }); // Full type safety and intellisense ``` #### `useLanguage()` @@ -564,24 +632,6 @@ const withVars = t('greeting', { name: 'John' }); **Note:** The main `useTranslation` hook should be preferred in 99% of cases. -#### `useTranslationInjection(key, variables)` -Injects variables into translation strings. - -```typescript -const result = useTranslationInjection('greeting', { name: 'John' }); -``` - -#### `useTranslationWithInterpolation(key, variables, components)` -Interpolates React components into translations. - -```typescript -const element = useTranslationWithInterpolation( - 'welcome', - { name: 'John' }, - { strong: } -); -``` - ### Core Functions #### `initLocalization(config)` @@ -622,6 +672,17 @@ interface LocalizationConfig { } ``` +#### `ComponentMap` +Type for component interpolation map. Supports both React elements and function components. + +```typescript +type ComponentMap = { + [key: string]: + | React.ReactElement + | ((props: { children?: React.ReactNode; [key: string]: any }) => React.ReactElement) +} +``` + ## 🆘 Support @@ -638,4 +699,4 @@ This project is licensed under the MIT License. --- -**@weprodev/ui-localization** - Professional localization solution by WeProDev \ No newline at end of file +**@weprodev/ui-localization** - Professional localization solution by WeProDev From 93c792b132bc5a6b8a05fec08ab8c97227518575 Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:05:17 +0330 Subject: [PATCH 03/11] refactor: streamline exports in index.ts for improved type management and clarity --- src/index.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index a43797c..002c72a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,9 @@ -export type { - LanguageStore, - LocalizationConfig, -} from "./core/types"; -export { initLocalization } from "./core/initLocalization"; +export type { LanguageStore, LocalizationConfig, Path, PathValue, TranslateFunction, NestedRecord, Primitive } from './core/types' +export { initLocalization } from './core/initLocalization' -export { useLanguage } from "./hooks/useLanguage"; -export { useTranslation } from "./hooks/useTranslation"; -export { useTranslationFallback } from "./hooks/useTranslationFallback"; -export { useTranslationWithInterpolation } from "./hooks/useTranslationWithInterpolation"; -export { useTranslationInjection } from "./hooks/useTranslationInjection"; +export { useLanguage } from './hooks/useLanguage' +export { useTranslation } from './hooks/useTranslation' +export type { UseTranslationReturn } from './hooks/useTranslation' +export { useTranslationFallback } from './hooks/useTranslationFallback' +export { createTranslation } from './utils/createTranslation' +export type { ComponentMap } from './core/types' From 44f8ef2ecffc625015b50986216b4339b8b8a14a Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:05:24 +0330 Subject: [PATCH 04/11] test: enhance useTranslation tests with type-safe t function, support for interpolation, and component rendering --- src/__tests__/hooks/useTranslation.test.tsx | 160 ++++++++++++++------ 1 file changed, 113 insertions(+), 47 deletions(-) diff --git a/src/__tests__/hooks/useTranslation.test.tsx b/src/__tests__/hooks/useTranslation.test.tsx index fcc994b..eea10f6 100644 --- a/src/__tests__/hooks/useTranslation.test.tsx +++ b/src/__tests__/hooks/useTranslation.test.tsx @@ -1,13 +1,20 @@ +import React from 'react'; import { renderHook } from '@testing-library/react'; import { useTranslation } from '../../hooks/useTranslation'; const mockUseTranslation = jest.fn(); jest.mock('react-i18next', () => ({ - useTranslation: () => mockUseTranslation() + useTranslation: () => mockUseTranslation(), + Trans: ({ i18nKey, values, components }: any) => + React.createElement('span', { 'data-testid': 'trans', 'data-key': i18nKey }, + `Translated: ${i18nKey}`, + values && JSON.stringify(values), + components && ' [with components]' + ) })); -// Mock translation object for testing +// Mock translation object for testing type safety const mockTranslationObject = { common: { hello: 'Hello', @@ -34,63 +41,51 @@ describe('useTranslation', () => { }); }); - it('should return a proxy object with type-safe access', () => { - const { result } = renderHook(() => useTranslation(mockTranslationObject)); + it('should return an object with t function', () => { + const { result } = renderHook(() => useTranslation()); expect(typeof result.current).toBe('object'); expect(result.current).not.toBeNull(); + expect(typeof result.current.t).toBe('function'); }); - it('should translate nested keys correctly using dot notation access', () => { + it('should translate nested keys correctly using type-safe t function', () => { const tSpy = jest.fn((key) => `translated_${key}`); mockUseTranslation.mockReturnValue({ t: tSpy }); - const { result } = renderHook(() => useTranslation(mockTranslationObject)); + const { result } = renderHook(() => useTranslation()); - // Access nested properties - const hello = result.current.common.hello; - const welcome = result.current.common.welcome; - const login = result.current.auth.login; + // Use type-safe t function + const hello = result.current.t('common.hello'); + const welcome = result.current.t('common.welcome'); + const login = result.current.t('auth.login'); expect(hello).toBe('translated_common.hello'); expect(welcome).toBe('translated_common.welcome'); expect(login).toBe('translated_auth.login'); - expect(tSpy).toHaveBeenCalledWith('common.hello'); - expect(tSpy).toHaveBeenCalledWith('common.welcome'); - expect(tSpy).toHaveBeenCalledWith('auth.login'); + expect(tSpy).toHaveBeenCalledWith('common.hello', undefined); + expect(tSpy).toHaveBeenCalledWith('common.welcome', undefined); + expect(tSpy).toHaveBeenCalledWith('auth.login', undefined); }); - it('should handle deeply nested objects', () => { + it('should handle deeply nested objects with type-safe t function', () => { const tSpy = jest.fn((key) => `translated_${key}`); mockUseTranslation.mockReturnValue({ t: tSpy }); - const { result } = renderHook(() => useTranslation(mockTranslationObject)); + const { result } = renderHook(() => useTranslation()); - const deepValue = result.current.nested.deep.value; + const deepValue = result.current.t('nested.deep.value'); expect(deepValue).toBe('translated_nested.deep.value'); - expect(tSpy).toHaveBeenCalledWith('nested.deep.value'); + expect(tSpy).toHaveBeenCalledWith('nested.deep.value', undefined); }); - it('should return nested proxy objects for object properties', () => { - const { result } = renderHook(() => useTranslation(mockTranslationObject)); - - // Accessing an object property should return another proxy - const commonProxy = result.current.common; - const authProxy = result.current.auth; - - expect(typeof commonProxy).toBe('object'); - expect(typeof authProxy).toBe('object'); - expect(commonProxy).not.toBeNull(); - expect(authProxy).not.toBeNull(); - }); - - it('should handle translation errors gracefully', () => { + it('should handle translation errors gracefully with t function', () => { const errorT = jest.fn().mockImplementation(() => { throw new Error('Translation error'); }); @@ -99,12 +94,12 @@ describe('useTranslation', () => { t: errorT }); - const { result } = renderHook(() => useTranslation(mockTranslationObject)); + const { result } = renderHook(() => useTranslation()); - expect(() => result.current.common.hello).toThrow('Translation error'); + expect(() => result.current.t('common.hello')).toThrow('Translation error'); }); - it('should work with different translation object structures', () => { + it('should work with different translation object structures using t function', () => { const customTranslationObject = { buttons: { save: 'Save', @@ -121,32 +116,103 @@ describe('useTranslation', () => { t: tSpy }); - const { result } = renderHook(() => useTranslation(customTranslationObject)); + const { result } = renderHook(() => useTranslation()); - const saveButton = result.current.buttons.save; - const errorMessage = result.current.messages.error; + const saveButton = result.current.t('buttons.save'); + const errorMessage = result.current.t('messages.error'); expect(saveButton).toBe('custom_buttons.save'); expect(errorMessage).toBe('custom_messages.error'); - expect(tSpy).toHaveBeenCalledWith('buttons.save'); - expect(tSpy).toHaveBeenCalledWith('messages.error'); + expect(tSpy).toHaveBeenCalledWith('buttons.save', undefined); + expect(tSpy).toHaveBeenCalledWith('messages.error', undefined); }); - it('should handle empty translation objects', () => { - const emptyTranslationObject = {}; + + it('should support interpolation with t function', () => { + const tSpy = jest.fn((key, params) => { + if (params) { + return `translated_${key}_${JSON.stringify(params)}`; + } + return `translated_${key}`; + }); + mockUseTranslation.mockReturnValue({ + t: tSpy + }); + + const { result } = renderHook(() => useTranslation()); + + const greeting = result.current.t('common.hello', { name: 'John' }); - const tSpy = jest.fn((key) => `empty_${key}`); + expect(greeting).toContain('translated_common.hello'); + expect(tSpy).toHaveBeenCalledWith('common.hello', { name: 'John' }); + }); + + it('should support type-safe params for translations with placeholders', () => { + const translationWithParams = { + user: { + greeting: 'Hello {{name}}!', + points: 'You have {{count}} points', + message: 'Hello {{name}}, you have {{count}} points' + }, + common: { + welcome: 'Welcome' + } + }; + + const tSpy = jest.fn((key, params) => { + if (params) { + return `translated_${key}_${JSON.stringify(params)}`; + } + return `translated_${key}`; + }); mockUseTranslation.mockReturnValue({ t: tSpy }); - const { result } = renderHook(() => useTranslation(emptyTranslationObject)); + const { result } = renderHook(() => useTranslation()); + + // Test single param + const greeting = result.current.t('user.greeting', { name: 'John' }); + expect(greeting).toContain('translated_user.greeting'); + expect(tSpy).toHaveBeenCalledWith('user.greeting', { name: 'John' }); + + // Test number param + const points = result.current.t('user.points', { count: 42 }); + expect(points).toContain('translated_user.points'); + expect(tSpy).toHaveBeenCalledWith('user.points', { count: 42 }); + + // Test multiple params + const message = result.current.t('user.message', { name: 'Alice', count: 10 }); + expect(message).toContain('translated_user.message'); + expect(tSpy).toHaveBeenCalledWith('user.message', { name: 'Alice', count: 10 }); + + // Test translation without params (should work without params argument) + const welcome = result.current.t('common.welcome'); + expect(welcome).toBe('translated_common.welcome'); + expect(tSpy).toHaveBeenCalledWith('common.welcome', undefined); + }); + + it('should support component interpolation with t function', () => { + const translationWithComponents = { + user: { + greeting: 'Hello {{name}}!', + welcome: 'Welcome {{name}}!' + } + }; + + mockUseTranslation.mockReturnValue({ + t: jest.fn((key) => `translated_${key}`) + }); + + const { result } = renderHook(() => useTranslation()); + + const components = { + strong: React.createElement('strong', { className: 'highlight' }) + }; - // Should still work even with empty objects - accessing any key should return translation - const someKey = (result.current as any).nonexistent; + const greeting = result.current.t('user.greeting', { name: 'John' }, components); - expect(someKey).toBe('empty_nonexistent'); - expect(tSpy).toHaveBeenCalledWith('nonexistent'); + expect(React.isValidElement(greeting)).toBe(true); }); }); From 56861c644dd04768beb950f609ef55ee108575b7 Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:05:36 +0330 Subject: [PATCH 05/11] refactor: remove unused translation hooks and their associated tests to streamline codebase --- .../hooks/useTranslationInjection.test.tsx | 96 ------------------- .../useTranslationWithInterpolation.test.tsx | 63 ------------ src/hooks/useTranslationInjection.tsx | 18 ---- src/hooks/useTranslationWithInterpolation.tsx | 24 ----- 4 files changed, 201 deletions(-) delete mode 100644 src/__tests__/hooks/useTranslationInjection.test.tsx delete mode 100644 src/__tests__/hooks/useTranslationWithInterpolation.test.tsx delete mode 100644 src/hooks/useTranslationInjection.tsx delete mode 100644 src/hooks/useTranslationWithInterpolation.tsx diff --git a/src/__tests__/hooks/useTranslationInjection.test.tsx b/src/__tests__/hooks/useTranslationInjection.test.tsx deleted file mode 100644 index 714a629..0000000 --- a/src/__tests__/hooks/useTranslationInjection.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { useTranslationInjection } from '../../hooks/useTranslationInjection'; - -const mockUseTranslation = jest.fn(); - -jest.mock('react-i18next', () => ({ - useTranslation: () => mockUseTranslation() -})); - -describe('useTranslationInjection', () => { - beforeEach(() => { - jest.clearAllMocks(); - - mockUseTranslation.mockReturnValue({ - t: jest.fn((key, variables) => { - if (Array.isArray(key)) { - return `translated_${key.join(',')}`; - } - - if (!variables) return `translated_${key}`; - - let result = `translated_${key}`; - Object.entries(variables).forEach(([varKey, varValue]) => { - if (typeof varValue === 'string') { - result = result.replace(new RegExp(`{{${varKey}}}`, 'g'), varValue); - } - }); - return result; - }) - }); - }); - - it('should translate a simple key', () => { - const { result } = renderHook(() => - useTranslationInjection('greeting', {}) - ); - - expect(result.current).toBe('translated_greeting'); - }); - - it('should translate with variables', () => { - const { result } = renderHook(() => - useTranslationInjection('welcome {{name}}', { name: 'John' }) - ); - - expect(result.current).toBe('translated_welcome John'); - }); - - it('should handle array of keys', () => { - const { result } = renderHook(() => - useTranslationInjection(['greeting', 'fallback'], {}) - ); - - expect(result.current).toBe('translated_greeting,fallback'); - }); - - it('should pass all variables to the t function', () => { - const tSpy = jest.fn().mockReturnValue('translated_with_context'); - mockUseTranslation.mockReturnValueOnce({ - t: tSpy - }); - - const variables = { - name: 'John', - count: 5, - context: { role: 'admin' }, - interpolation: { escapeValue: false } - }; - - renderHook(() => useTranslationInjection('complex', variables)); - - expect(tSpy).toHaveBeenCalledWith('complex', variables); - }); - - it('should handle empty variables object', () => { - const tSpy = jest.fn().mockReturnValue('translated_empty'); - mockUseTranslation.mockReturnValueOnce({ - t: tSpy - }); - - renderHook(() => useTranslationInjection('empty', {})); - - expect(tSpy).toHaveBeenCalledWith('empty', {}); - }); - - it('should handle undefined variables gracefully', () => { - const tSpy = jest.fn().mockReturnValue('translated_no_vars'); - mockUseTranslation.mockReturnValueOnce({ - t: tSpy - }); - - renderHook(() => useTranslationInjection('no_vars', undefined as any)); - - expect(tSpy).toHaveBeenCalledWith('no_vars', undefined); - }); -}); diff --git a/src/__tests__/hooks/useTranslationWithInterpolation.test.tsx b/src/__tests__/hooks/useTranslationWithInterpolation.test.tsx deleted file mode 100644 index 29cff12..0000000 --- a/src/__tests__/hooks/useTranslationWithInterpolation.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { renderHook } from '@testing-library/react'; -import { useTranslationWithInterpolation } from '../../hooks/useTranslationWithInterpolation'; - -jest.mock('react-i18next', () => ({ - Trans: ({ i18nKey, ...props }: any) => - React.createElement('span', props, `Translated: ${i18nKey}`) -})); - -describe('useTranslationWithInterpolation', () => { - it('should create a memoized component', () => { - const { result } = renderHook(() => - useTranslationWithInterpolation('welcome', {}, {}) - ); - - expect(result.current).toBeDefined(); - expect(React.isValidElement(result.current)).toBe(true); - }); - - it('should pass correct props to Trans component', () => { - const variables = { name: 'John' }; - const components = { strong: }; - - const { result } = renderHook(() => - useTranslationWithInterpolation('welcome', variables, components) - ); - - expect(result.current).toBeDefined(); - expect(React.isValidElement(result.current)).toBe(true); - }); - - it('should handle default parameters', () => { - const { result } = renderHook(() => - useTranslationWithInterpolation('simple') - ); - - expect(result.current).toBeDefined(); - expect(React.isValidElement(result.current)).toBe(true); - }); - - it('should memoize the result based on dependencies', () => { - const variables = { name: 'John' }; - const components = { strong: }; - - const { result, rerender } = renderHook( - ({ key, vars, comps }) => - useTranslationWithInterpolation(key, vars, comps), - { - initialProps: { key: 'welcome', vars: variables, comps: components } - } - ); - - const firstResult = result.current; - - rerender({ key: 'welcome', vars: variables, comps: components }); - - expect(result.current).toBe(firstResult); - - rerender({ key: 'goodbye', vars: variables, comps: components }); - - expect(result.current).not.toBe(firstResult); - }); -}); \ No newline at end of file diff --git a/src/hooks/useTranslationInjection.tsx b/src/hooks/useTranslationInjection.tsx deleted file mode 100644 index 4785699..0000000 --- a/src/hooks/useTranslationInjection.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { $Dictionary } from 'i18next/typescript/helpers'; -import { TOptionsBase } from 'i18next/typescript/options'; -import { useTranslation } from 'react-i18next'; - -/** - * Hook for translation with variable injection. - * - * @param key - Translation key or array of keys - * @param variables - Variables to inject into the translation - * @returns Translated string with variables injected - */ -export const useTranslationInjection = ( - key: string | string[], - variables: TOptionsBase & $Dictionary -): string => { - const { t } = useTranslation(); - return t(key, variables); -}; \ No newline at end of file diff --git a/src/hooks/useTranslationWithInterpolation.tsx b/src/hooks/useTranslationWithInterpolation.tsx deleted file mode 100644 index 4df04a6..0000000 --- a/src/hooks/useTranslationWithInterpolation.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { useMemo, ReactElement, JSX } from 'react'; -import { Trans } from 'react-i18next'; - -type Variables = { [key: string]: any }; -type Components = { [key: string]: ReactElement }; - -/** - * Hook for translation with component interpolation. - * - * @param key - Translation key - * @param variables - Variables to inject into the translation - * @param components - React components to interpolate - * @returns JSX element with translated content and interpolated components - */ -export const useTranslationWithInterpolation = ( - key: string, - variables: Variables = {}, - components: Components = {} -): JSX.Element => { - return useMemo( - () => , - [key, variables, components] - ); -}; \ No newline at end of file From 3f23c0aec02906d24fda655e6bdbb747079c03ae Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:07:10 +0330 Subject: [PATCH 06/11] feat: add type-safe translation utilities and React component support to enhance localization capabilities --- src/core/types.ts | 187 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 176 insertions(+), 11 deletions(-) diff --git a/src/core/types.ts b/src/core/types.ts index 7c91c4b..45cf27a 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,4 +1,5 @@ -import { Resource } from "i18next"; +import { Resource } from 'i18next' +import React from 'react' /** * Interface for language storage operations. @@ -8,24 +9,188 @@ export interface LanguageStore { * Gets the currently stored language. * @returns The stored language code or null if none is stored */ - getLanguage(): string | null; - + getLanguage(): string | null + /** * Sets the language to be stored. * @param language - The language code to store */ - setLanguage(language: string): void; + setLanguage(language: string): void } /** * Configuration object for localization initialization. */ export interface LocalizationConfig { - resources: Resource; - fallbackLng?: string; - compatibilityJSON?: "v4"; + resources: Resource + fallbackLng?: string + compatibilityJSON?: 'v4' interpolation?: { - escapeValue?: boolean; - }; - languageStore?: LanguageStore; -} \ No newline at end of file + escapeValue?: boolean + } + languageStore?: LanguageStore +} + +/** + * Type-safe translation path types + * These types enable compile-time checking of translation keys + */ + +/** + * Primitive types that can be leaf values in translation objects + */ +export type Primitive = string | number | boolean | null | undefined + +/** + * Nested record type that allows deep nesting of translation objects + */ +export type NestedRecord = { [key: string]: Primitive | NestedRecord } + +/** + * Recursively generates all possible paths through a nested object structure. + * Returns a union of string literal types representing dot-notation paths. + * + * @template T - The translation object type + * + * @example + * ```typescript + * type Translations = { + * common: { welcome: string; goodbye: string }; + * user: { greeting: string }; + * }; + * + * type Paths = Path; + * // Result: "common" | "common.welcome" | "common.goodbye" | "user" | "user.greeting" + * ``` + */ +export type PathImpl = K extends string | number + ? T[K] extends Record + ? T[K] extends Primitive + ? `${K}` + : `${K}` | `${K}.${PathImpl}` + : `${K}` + : never + +export type Path = T extends Record ? PathImpl : never + +/** + * Extracts the value type at a given path in a nested object structure. + * + * @template T - The translation object type + * @template P - The path string (e.g., "common.welcome") + * + * @example + * ```typescript + * type Translations = { + * common: { welcome: string; count: number }; + * }; + * + * type WelcomeType = PathValue; + * // Result: string + * + * type CountType = PathValue; + * // Result: number + * ``` + */ +export type PathValue = P extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? Rest extends Path + ? PathValue + : never + : never + : P extends keyof T + ? T[P] + : never + +/** + * Extracts parameter names from a string literal type that contains interpolation placeholders. + * Supports the default format: {{paramName}} + * + * @template S - The string literal type + * @template Prefix - The interpolation prefix (default: "{{") + * @template Suffix - The interpolation suffix (default: "}}") + * + * @example + * ```typescript + * type Params1 = ExtractParams<"Hello {{name}}!">; + * // Result: { name: string | number } + * + * type Params2 = ExtractParams<"You have {{count}} points">; + * // Result: { count: string | number } + * + * type Params3 = ExtractParams<"Hello {{name}}, you have {{count}} points">; + * // Result: { name: string | number; count: string | number } + * + * type Params4 = ExtractParams<"No params here">; + * // Result: {} + * ``` + */ +type ExtractParams = S extends `${string}${Prefix}${infer Param}${Suffix}${infer Rest}` + ? Param extends `${infer ParamName}` + ? ParamName extends '' + ? ExtractParams + : { + [K in ParamName]: string | number + } & ExtractParams + : ExtractParams + : {} + +/** + * Type for React component interpolation map + * Supports both React elements and function components for react-i18next Trans + */ +export type ComponentMap = { + [key: string]: React.ReactElement | ((props: { children?: React.ReactNode; [key: string]: any }) => React.ReactElement) +} + +/** + * Type-safe translation function that accepts only valid paths from the translation structure + * and enforces type-safe parameters based on the translation string content. + * Supports both string interpolation and React component interpolation. + * + * @template T - The translation object type + * + * @example + * ```typescript + * const translations = { + * common: { welcome: "Welcome" }, + * user: { greeting: "Hello, {{name}}!" } + * }; + * + * type TranslateFn = TranslateFunction; + * + * const t: TranslateFn = (key, params, components) => { ... }; + * + * // String interpolation + * t("common.welcome"); // ✅ Valid (no params needed) -> returns string + * t("user.greeting", { name: "John" }); // ✅ Valid -> returns string + * t("user.greeting", { wrong: "John" }); // ❌ TypeScript error + * t("user.greeting"); // ❌ TypeScript error (name is required) + * + * // Component interpolation + * t("user.greeting", { name: "John" }, { name: John }); // ✅ Valid -> returns JSX.Element + * t("invalid.key"); // ❌ TypeScript error + * ``` + */ +export type TranslateFunction = { + // Overload 1: With components -> returns JSX.Element + & string>( + key: K, + params: PathValue extends string + ? ExtractParams> extends Record + ? Record | undefined + : ExtractParams> + : Record | undefined, + components: ComponentMap + ): React.JSX.Element + + // Overload 2: Without components -> returns string + & string>( + key: K, + ...args: PathValue extends string + ? ExtractParams> extends Record + ? [params?: Record] + : [params: ExtractParams>] + : [params?: Record] + ): string +} From e86c9d1d643069b821480adeebf101b44406a1bb Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:07:25 +0330 Subject: [PATCH 07/11] refactor: simplify useTranslation hook to improve type safety and support for React components --- src/hooks/useTranslation.ts | 85 ++++++++++++------------------------- 1 file changed, 28 insertions(+), 57 deletions(-) diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts index 719f13d..ebc2a4a 100644 --- a/src/hooks/useTranslation.ts +++ b/src/hooks/useTranslation.ts @@ -1,65 +1,36 @@ -import { useTranslation as useTranslationI18next } from 'react-i18next'; +import React from 'react' +import { useTranslation as useTranslationI18next, Trans } from 'react-i18next' +import { Path, TranslateFunction, NestedRecord, ComponentMap } from '../core/types' /** - * Type utility that recursively maps translation object structure to provide type safety. - * Converts nested objects to the same structure but with string values for leaf nodes. + * Return type for useTranslation hook with type-safe t function */ -type TranslationKeys = { - [K in keyof T]: T[K] extends object ? TranslationKeys : string; -}; +export interface UseTranslationReturn { + t: TranslateFunction +} -/** - * Type alias for translation structure with type safety. - */ -type Translation = TranslationKeys; - -/** - * Type-safe translation hook that provides intellisense and type checking for translation keys. - * - * @template T - The type of the translation object structure - * @param translationLanguage - The translation object to provide type safety for - * @returns Type-safe translation proxy object - * - * @example - * ```typescript - * import en from '../translations/en'; - * - * const t = useTranslation(en); - * const welcome = t.common.welcome; // Type-safe access with intellisense - * ``` - */ -export const useTranslation = ( - translationLanguage: object -): Translation => { - const { t } = useTranslationI18next(); +export const useTranslation = (): UseTranslationReturn => { + const { t: tI18next } = useTranslationI18next() - const createTranslationProxy = (prefix = ''): any => { - return new Proxy( - {}, - { - get(_target, prop: string) { - const key = prefix ? `${prefix}.${prop}` : prop; + const t = ( & string>(key: K, ...args: any[]): string | React.JSX.Element => { + const params = args[0] as Record | undefined + const components = args[1] as ComponentMap | undefined - // Check if this key exists in the original structure - const originalValue = prefix - ? prefix - .split('.') - .reduce((obj, k) => obj?.[k], translationLanguage as any)?.[ - prop - ] - : (translationLanguage as any)[prop]; + // If components are provided, use Trans component for interpolation + if (components !== undefined) { + return React.createElement(Trans, { + i18nKey: key, + values: params, + components: components as { [tagName: string]: React.ReactElement }, + }) + } - if (typeof originalValue === 'object' && originalValue !== null) { - // Return a new proxy for nested objects - return createTranslationProxy(key); - } else { - // For leaf nodes or non-existent keys, return the translated string - return t(key); - } - }, - } - ); - }; + // Otherwise, use regular translation + const result = tI18next(key, params) + return typeof result === 'string' ? result : String(result) + }) as TranslateFunction - return createTranslationProxy() as Translation; -}; + return { + t, + } +} From e4fb3ac8e4e7213fe38e8ad5a7d99c6b7644303d Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:07:31 +0330 Subject: [PATCH 08/11] feat: add createTranslation utility for type-safe translation function to enhance localization support --- src/utils/createTranslation.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/utils/createTranslation.ts diff --git a/src/utils/createTranslation.ts b/src/utils/createTranslation.ts new file mode 100644 index 0000000..b6b91b3 --- /dev/null +++ b/src/utils/createTranslation.ts @@ -0,0 +1,12 @@ +import i18n from 'i18next' +import { TranslateFunction, NestedRecord, Path } from '../core/types' + +export const createTranslation = (_translationLanguage: T): TranslateFunction => { + const t = ( & string>(key: K, ...args: any[]): string => { + const params = args[0] + const result = i18n.t(key, params) + return typeof result === 'string' ? result : String(result) + }) as TranslateFunction + + return t +} From 50274b833ee91fe40ab7287fd93365f89a98ad0d Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:12:19 +0330 Subject: [PATCH 09/11] chore: bump version to 2.0.0 for enhanced localization features --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14e80ff..ccb059a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@weprodev/ui-localization", - "version": "1.0.0", + "version": "2.0.0", "description": "Localization package for React and React Native applications", "main": "dist/index.js", "module": "dist/index.esm.js", From 278c2a97c9aa8d0f92a85127aa0c6d1dbc0167bd Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:12:25 +0330 Subject: [PATCH 10/11] docs: add CHANGELOG.md to document notable changes and migration details for version 2.0.0 --- CHANGELOG.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5a836d0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.0.0] - 2025-11-20 + +### 🚨 Breaking Changes + +#### Removed Hooks +- **Removed `useTranslationWithInterpolation` hook** - Functionality merged into `useTranslation` +- **Removed `useTranslationInjection` hook** - Functionality merged into `useTranslation` + +#### `useTranslation` API Changes +The `useTranslation` hook has been completely refactored with a new API: + +**Before (v1.0.0):** +```typescript +const t = useTranslation(en); +const welcome = t.common.welcome; // Proxy-based access +``` + +**After (v2.0.0):** +```typescript +const { t } = useTranslation(); +const welcome = t('common.welcome'); // Function-based access +``` + +**Migration Guide:** +- Remove the translation object parameter from `useTranslation` +- Change from property access (`t.common.welcome`) to function calls (`t('common.welcome')`) +- The hook now returns an object with a `t` function instead of a proxy object +- Variable injection and component interpolation are now handled through the same `t` function + +### ✨ Added + +- **`createTranslation` utility** - New utility function for creating type-safe translation functions outside of React components +- **Enhanced type safety** - Improved TypeScript types with `Path`, `PathValue`, `TranslateFunction`, and `ComponentMap` +- **Unified API** - Single `t` function now handles both variable injection and component interpolation +- **React component support** - Built-in support for embedding React components within translations using the `Trans` component + +### 🔄 Changed + +- **Simplified `useTranslation` hook** - Reduced from 65 lines to 36 lines, improved maintainability +- **Streamlined exports** - Cleaner export structure in `index.ts` +- **Enhanced type definitions** - Expanded `types.ts` with comprehensive type utilities for better type safety + +### 📝 Documentation + +- Updated README.md with new API examples and usage patterns +- Added comprehensive examples for type-safe translations +- Clarified variable injection and component interpolation sections + +## [1.0.0] - Previous Release + +Initial stable release with: +- Basic translation hooks +- Language switching functionality +- Translation validation and sync tools +- React and React Native support + From 69a271f65606ce7e0e5f0955b940ac5b63630229 Mon Sep 17 00:00:00 2001 From: Amanj Date: Thu, 20 Nov 2025 10:23:31 +0330 Subject: [PATCH 11/11] feat: enhance createTranslation function to support React components and improve interpolation handling --- src/utils/createTranslation.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/utils/createTranslation.ts b/src/utils/createTranslation.ts index b6b91b3..3507b6c 100644 --- a/src/utils/createTranslation.ts +++ b/src/utils/createTranslation.ts @@ -1,9 +1,23 @@ +import React from 'react' import i18n from 'i18next' -import { TranslateFunction, NestedRecord, Path } from '../core/types' +import { Trans } from 'react-i18next' +import { TranslateFunction, NestedRecord, Path, ComponentMap } from '../core/types' export const createTranslation = (_translationLanguage: T): TranslateFunction => { - const t = ( & string>(key: K, ...args: any[]): string => { - const params = args[0] + const t = ( & string>(key: K, ...args: any[]): string | React.JSX.Element => { + const params = args[0] as Record | undefined + const components = args[1] as ComponentMap | undefined + + // If components are provided, use Trans component for interpolation + if (components !== undefined) { + return React.createElement(Trans, { + i18nKey: key, + values: params, + components: components as { [tagName: string]: React.ReactElement }, + }) + } + + // Otherwise, use regular translation const result = i18n.t(key, params) return typeof result === 'string' ? result : String(result) }) as TranslateFunction