Skip to content

Commit b18d539

Browse files
committed
feat: implement useLocalizer hook for translation management; update README and tests
1 parent b7dfc18 commit b18d539

File tree

5 files changed

+137
-58
lines changed

5 files changed

+137
-58
lines changed

README.md

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# @devwizard/laravel-localizer-react
22

3-
🌍 React integration for Laravel Localizer with Vite plugin, `useTranslation` hook, and automatic TypeScript generation.
3+
🌍 React integration for Laravel Localizer with Vite plugin, `useLocalizer` hook, and automatic TypeScript generation.
44

55
## Features
66

77
-**Automatic Generation**: Vite plugin watches for language file changes and regenerates TypeScript files
88
-**Type-Safe**: Full TypeScript support with auto-generated types
9-
-**React Hooks**: Intuitive `useTranslation` hook for React components
9+
-**React Hooks**: Intuitive `useLocalizer` hook for React components
10+
-**Customizable Path**: By default reads from `@/lang` folder, customizable via options
1011
-**Laravel-Compatible**: Matches Laravel's translation API (`__`, `trans`, `choice`)
1112
-**Inertia.js Integration**: Seamlessly works with Inertia.js page props
1213
-**RTL Support**: Built-in right-to-left language support
@@ -63,10 +64,10 @@ This creates TypeScript files in `resources/js/lang/` directory.
6364
### Basic Usage
6465

6566
```tsx
66-
import { useTranslation } from '@devwizard/laravel-localizer-react';
67+
import { useLocalizer } from '@devwizard/laravel-localizer-react';
6768

6869
function MyComponent() {
69-
const { __ } = useTranslation();
70+
const { __ } = useLocalizer();
7071

7172
return (
7273
<div>
@@ -77,13 +78,42 @@ function MyComponent() {
7778
}
7879
```
7980

81+
### Custom Translations Path
82+
83+
By default, translations are read from the `@/lang` folder (which maps to `resources/js/lang`). You can customize this path:
84+
85+
```tsx
86+
import { useLocalizer } from '@devwizard/laravel-localizer-react';
87+
88+
function MyComponent() {
89+
// Use custom translations directory
90+
const { __ } = useLocalizer({ langPath: '@/translations' });
91+
92+
return <h1>{__('welcome')}</h1>;
93+
}
94+
```
95+
96+
**Note:** Ensure your Vite config has the corresponding path alias:
97+
98+
```typescript
99+
// vite.config.ts
100+
export default defineConfig({
101+
resolve: {
102+
alias: {
103+
'@': '/resources/js',
104+
'@/translations': '/resources/js/translations',
105+
},
106+
},
107+
});
108+
```
109+
80110
### With Replacements
81111

82112
```tsx
83-
import { useTranslation } from '@devwizard/laravel-localizer-react';
113+
import { useLocalizer } from '@devwizard/laravel-localizer-react';
84114

85115
function Greeting() {
86-
const { __ } = useTranslation();
116+
const { __ } = useLocalizer();
87117

88118
return (
89119
<div>
@@ -97,10 +127,10 @@ function Greeting() {
97127
### Pluralization
98128

99129
```tsx
100-
import { useTranslation } from '@devwizard/laravel-localizer-react';
130+
import { useLocalizer } from '@devwizard/laravel-localizer-react';
101131

102132
function ItemCount({ count }: { count: number }) {
103-
const { choice } = useTranslation();
133+
const { choice } = useLocalizer();
104134

105135
return <p>{choice('items', count)}</p>;
106136
}
@@ -109,10 +139,10 @@ function ItemCount({ count }: { count: number }) {
109139
### RTL Support
110140

111141
```tsx
112-
import { useTranslation } from '@devwizard/laravel-localizer-react';
142+
import { useLocalizer } from '@devwizard/laravel-localizer-react';
113143

114144
function App() {
115-
const { dir, locale } = useTranslation();
145+
const { dir, locale } = useLocalizer();
116146

117147
return (
118148
<div dir={dir} lang={locale}>
@@ -125,10 +155,10 @@ function App() {
125155
### Check Translation Exists
126156

127157
```tsx
128-
import { useTranslation } from '@devwizard/laravel-localizer-react';
158+
import { useLocalizer } from '@devwizard/laravel-localizer-react';
129159

130160
function ConditionalMessage() {
131-
const { __, has } = useTranslation();
161+
const { __, has } = useLocalizer();
132162

133163
if (!has('special.message')) {
134164
return null;
@@ -140,9 +170,17 @@ function ConditionalMessage() {
140170

141171
## API Reference
142172

143-
### `useTranslation()`
173+
### `useLocalizer(options?)`
174+
175+
Main hook for accessing translations and locale information.
176+
177+
**Options:**
178+
179+
| Option | Type | Default | Description |
180+
| ---------- | -------- | ---------- | -------------------------------------------- |
181+
| `langPath` | `string` | `'@/lang'` | Custom path to translations directory |
144182

145-
Returns an object with the following properties and methods:
183+
**Returns:**
146184

147185
| Property | Type | Description |
148186
| ------------------ | ------------------------------------------- | ----------------------------------- |
Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { renderHook } from '@testing-library/react';
21
import { usePage } from '@inertiajs/react';
3-
import { useTranslation } from '../hooks/useTranslation';
2+
import { renderHook } from '@testing-library/react';
3+
import { useLocalizer } from '../hooks/useLocalizer';
44

55
// Mock usePage
66
const mockUsePage = usePage as jest.MockedFunction<typeof usePage>;
77

8-
describe('useTranslation', () => {
8+
describe('useLocalizer', () => {
99
beforeEach(() => {
1010
// Setup default mock
1111
mockUsePage.mockReturnValue({
@@ -48,68 +48,68 @@ describe('useTranslation', () => {
4848

4949
describe('Basic Translation', () => {
5050
it('should return translated string for a valid key', () => {
51-
const { result } = renderHook(() => useTranslation());
51+
const { result } = renderHook(() => useLocalizer());
5252

5353
expect(result.current.__('welcome')).toBe('Welcome');
5454
});
5555

5656
it('should return the key if translation is not found', () => {
57-
const { result } = renderHook(() => useTranslation());
57+
const { result } = renderHook(() => useLocalizer());
5858

5959
expect(result.current.__('missing.key')).toBe('missing.key');
6060
});
6161

6262
it('should support nested keys with dot notation', () => {
63-
const { result } = renderHook(() => useTranslation());
63+
const { result } = renderHook(() => useLocalizer());
6464

6565
expect(result.current.__('validation.required')).toBe('This field is required');
6666
});
6767
});
6868

6969
describe('Placeholder Replacement', () => {
7070
it('should replace placeholders with :placeholder format', () => {
71-
const { result } = renderHook(() => useTranslation());
71+
const { result } = renderHook(() => useLocalizer());
7272

7373
expect(result.current.__('greeting.hello', { name: 'John' })).toBe('Hello John!');
7474
});
7575

7676
it('should replace multiple placeholders', () => {
77-
const { result } = renderHook(() => useTranslation());
77+
const { result } = renderHook(() => useLocalizer());
7878

7979
expect(result.current.__('items.count', { count: 5 })).toBe('You have 5 items');
8080
});
8181

8282
it('should handle numeric replacements', () => {
83-
const { result } = renderHook(() => useTranslation());
83+
const { result } = renderHook(() => useLocalizer());
8484

8585
expect(result.current.__('items.count', { count: 0 })).toBe('You have 0 items');
8686
});
8787
});
8888

8989
describe('Fallback', () => {
9090
it('should use fallback if translation key is missing', () => {
91-
const { result } = renderHook(() => useTranslation());
91+
const { result } = renderHook(() => useLocalizer());
9292

9393
expect(result.current.__('missing.key', {}, 'Default text')).toBe('Default text');
9494
});
9595

9696
it('should not use fallback if translation exists', () => {
97-
const { result } = renderHook(() => useTranslation());
97+
const { result } = renderHook(() => useLocalizer());
9898

9999
expect(result.current.__('welcome', {}, 'Fallback')).toBe('Welcome');
100100
});
101101
});
102102

103103
describe('Aliases', () => {
104104
it('trans should work as alias for __', () => {
105-
const { result } = renderHook(() => useTranslation());
105+
const { result } = renderHook(() => useLocalizer());
106106

107107
expect(result.current.trans('welcome')).toBe('Welcome');
108108
expect(result.current.trans('welcome')).toBe(result.current.__('welcome'));
109109
});
110110

111111
it('lang should work as alias for __', () => {
112-
const { result } = renderHook(() => useTranslation());
112+
const { result } = renderHook(() => useLocalizer());
113113

114114
expect(result.current.lang('welcome')).toBe('Welcome');
115115
expect(result.current.lang('welcome')).toBe(result.current.__('welcome'));
@@ -118,22 +118,22 @@ describe('useTranslation', () => {
118118

119119
describe('has() method', () => {
120120
it('should return true for existing keys', () => {
121-
const { result } = renderHook(() => useTranslation());
121+
const { result } = renderHook(() => useLocalizer());
122122

123123
expect(result.current.has('welcome')).toBe(true);
124124
expect(result.current.has('validation.required')).toBe(true);
125125
});
126126

127127
it('should return false for missing keys', () => {
128-
const { result } = renderHook(() => useTranslation());
128+
const { result } = renderHook(() => useLocalizer());
129129

130130
expect(result.current.has('missing.key')).toBe(false);
131131
});
132132
});
133133

134134
describe('choice() method', () => {
135135
it('should include count in replacements', () => {
136-
const { result } = renderHook(() => useTranslation());
136+
const { result } = renderHook(() => useLocalizer());
137137

138138
expect(result.current.choice('items.count', 5)).toBe('You have 5 items');
139139
});
@@ -142,21 +142,21 @@ describe('useTranslation', () => {
142142
(window as any).__LARAVEL_LOCALIZER_TRANSLATIONS__.en['user.items'] =
143143
':name has :count items';
144144

145-
const { result } = renderHook(() => useTranslation());
145+
const { result } = renderHook(() => useLocalizer());
146146

147147
expect(result.current.choice('user.items', 3, { name: 'Alice' })).toBe('Alice has 3 items');
148148
});
149149
});
150150

151151
describe('Locale Information', () => {
152152
it('should return current locale', () => {
153-
const { result } = renderHook(() => useTranslation());
153+
const { result } = renderHook(() => useLocalizer());
154154

155155
expect(result.current.locale).toBe('en');
156156
});
157157

158158
it('should return text direction', () => {
159-
const { result } = renderHook(() => useTranslation());
159+
const { result } = renderHook(() => useLocalizer());
160160

161161
expect(result.current.dir).toBe('ltr');
162162
});
@@ -177,13 +177,13 @@ describe('useTranslation', () => {
177177
version: null,
178178
});
179179

180-
const { result } = renderHook(() => useTranslation());
180+
const { result } = renderHook(() => useLocalizer());
181181

182182
expect(result.current.dir).toBe('rtl');
183183
});
184184

185185
it('should return available locales', () => {
186-
const { result } = renderHook(() => useTranslation());
186+
const { result } = renderHook(() => useLocalizer());
187187

188188
expect(result.current.availableLocales).toEqual({
189189
en: { label: 'English', flag: '🇺🇸', dir: 'ltr' },
@@ -194,7 +194,7 @@ describe('useTranslation', () => {
194194

195195
describe('getLocales() method', () => {
196196
it('should return array of locale codes', () => {
197-
const { result } = renderHook(() => useTranslation());
197+
const { result } = renderHook(() => useLocalizer());
198198

199199
expect(result.current.getLocales()).toEqual(['en', 'bn']);
200200
});
@@ -214,15 +214,15 @@ describe('useTranslation', () => {
214214
version: null,
215215
});
216216

217-
const { result } = renderHook(() => useTranslation());
217+
const { result } = renderHook(() => useLocalizer());
218218

219219
expect(result.current.getLocales()).toEqual([]);
220220
});
221221
});
222222

223223
describe('Translations object', () => {
224224
it('should expose all translations', () => {
225-
const { result } = renderHook(() => useTranslation());
225+
const { result } = renderHook(() => useLocalizer());
226226

227227
expect(result.current.translations).toEqual({
228228
welcome: 'Welcome',
@@ -244,7 +244,7 @@ describe('useTranslation', () => {
244244
version: null,
245245
});
246246

247-
const { result } = renderHook(() => useTranslation());
247+
const { result } = renderHook(() => useLocalizer());
248248

249249
expect(result.current.locale).toBe('en'); // Default
250250
expect(result.current.dir).toBe('ltr'); // Default
@@ -253,7 +253,7 @@ describe('useTranslation', () => {
253253
it('should handle missing translations gracefully', () => {
254254
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
255255

256-
const { result } = renderHook(() => useTranslation());
256+
const { result } = renderHook(() => useLocalizer());
257257

258258
// Since translations exist in the test setup, verify basic functionality works
259259
expect(result.current.__('test.key')).toBe('test.key');
@@ -263,21 +263,21 @@ describe('useTranslation', () => {
263263
});
264264

265265
it('should handle empty string replacements', () => {
266-
const { result } = renderHook(() => useTranslation());
266+
const { result } = renderHook(() => useLocalizer());
267267

268268
expect(result.current.__('greeting.hello', { name: '' })).toBe('Hello !');
269269
});
270270

271271
it('should handle undefined replacements', () => {
272-
const { result } = renderHook(() => useTranslation());
272+
const { result } = renderHook(() => useLocalizer());
273273

274274
expect(result.current.__('greeting.hello', undefined)).toBe('Hello :name!');
275275
});
276276
});
277277

278278
describe('Reactivity', () => {
279279
it('should update when locale changes', () => {
280-
const { result, rerender } = renderHook(() => useTranslation());
280+
const { result, rerender } = renderHook(() => useLocalizer());
281281

282282
expect(result.current.__('welcome')).toBe('Welcome');
283283

0 commit comments

Comments
 (0)