@hua-labs/i18n-core-zustand 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +245 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/index.ts +347 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 HUA Labs Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# @hua-labs/i18n-core-zustand
|
|
2
|
+
|
|
3
|
+
Type-safe adapter package for integrating Zustand state management with `@hua-labs/i18n-core`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @hua-labs/i18n-core-zustand zustand
|
|
9
|
+
# or
|
|
10
|
+
npm install @hua-labs/i18n-core-zustand zustand
|
|
11
|
+
# or
|
|
12
|
+
yarn add @hua-labs/i18n-core-zustand zustand
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Your Zustand store must have `language: string` and `setLanguage: (lang: string) => void`.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### 1. Basic Usage (Create Provider)
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// lib/i18n-config.ts
|
|
25
|
+
import { createZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
26
|
+
import { useAppStore } from '../store/useAppStore';
|
|
27
|
+
|
|
28
|
+
export const I18nProvider = createZustandI18n(useAppStore, {
|
|
29
|
+
fallbackLanguage: 'en',
|
|
30
|
+
namespaces: ['common', 'navigation', 'footer'],
|
|
31
|
+
translationLoader: 'api',
|
|
32
|
+
translationApiPath: '/api/translations',
|
|
33
|
+
defaultLanguage: 'ko', // SSR initial language (prevents hydration errors)
|
|
34
|
+
debug: process.env.NODE_ENV === 'development'
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// app/layout.tsx
|
|
40
|
+
import { I18nProvider } from './lib/i18n-config';
|
|
41
|
+
|
|
42
|
+
export default function RootLayout({ children }) {
|
|
43
|
+
return (
|
|
44
|
+
<html>
|
|
45
|
+
<body>
|
|
46
|
+
<I18nProvider>{children}</I18nProvider>
|
|
47
|
+
</body>
|
|
48
|
+
</html>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Zustand Store Example
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// store/useAppStore.ts
|
|
57
|
+
import { create } from 'zustand';
|
|
58
|
+
import { persist } from 'zustand/middleware';
|
|
59
|
+
|
|
60
|
+
interface AppState {
|
|
61
|
+
language: 'ko' | 'en';
|
|
62
|
+
setLanguage: (lang: 'ko' | 'en') => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const useAppStore = create<AppState>()(
|
|
66
|
+
persist(
|
|
67
|
+
(set) => ({
|
|
68
|
+
language: 'ko',
|
|
69
|
+
setLanguage: (lang) => set({ language: lang }),
|
|
70
|
+
}),
|
|
71
|
+
{
|
|
72
|
+
name: 'app-storage',
|
|
73
|
+
partialize: (state) => ({ language: state.language }),
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Using Translations
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// components/MyComponent.tsx
|
|
83
|
+
import { useTranslation } from '@hua-labs/i18n-core';
|
|
84
|
+
import { useAppStore } from '../store/useAppStore';
|
|
85
|
+
|
|
86
|
+
export default function MyComponent() {
|
|
87
|
+
const { t } = useTranslation();
|
|
88
|
+
const { language, setLanguage } = useAppStore();
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<h1>{t('common:welcome')}</h1>
|
|
93
|
+
<button onClick={() => setLanguage(language === 'ko' ? 'en' : 'ko')}>
|
|
94
|
+
{language === 'ko' ? 'English' : '한국어'}
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 4. Using with SSR
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
// app/layout.tsx (Server Component)
|
|
105
|
+
import { loadSSRTranslations } from './lib/ssr-translations';
|
|
106
|
+
import { createZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
107
|
+
import { useAppStore } from './store/useAppStore';
|
|
108
|
+
|
|
109
|
+
export default async function RootLayout({ children }) {
|
|
110
|
+
// Load translations from SSR
|
|
111
|
+
const ssrTranslations = await loadSSRTranslations('ko');
|
|
112
|
+
|
|
113
|
+
const I18nProvider = createZustandI18n(useAppStore, {
|
|
114
|
+
fallbackLanguage: 'en',
|
|
115
|
+
namespaces: ['common', 'navigation', 'footer'],
|
|
116
|
+
translationLoader: 'api',
|
|
117
|
+
translationApiPath: '/api/translations',
|
|
118
|
+
defaultLanguage: 'ko', // Same initial language as SSR
|
|
119
|
+
initialTranslations: ssrTranslations, // Pass SSR translations
|
|
120
|
+
debug: process.env.NODE_ENV === 'development'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<html lang="ko">
|
|
125
|
+
<body>
|
|
126
|
+
<I18nProvider>{children}</I18nProvider>
|
|
127
|
+
</body>
|
|
128
|
+
</html>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## API
|
|
134
|
+
|
|
135
|
+
### `createZustandI18n(store, config)`
|
|
136
|
+
|
|
137
|
+
Creates a Provider that integrates Zustand store with i18n-core.
|
|
138
|
+
|
|
139
|
+
**Parameters:**
|
|
140
|
+
- `store`: Zustand store (must have `language` and `setLanguage` methods)
|
|
141
|
+
- `config`: i18n configuration (same as `I18nConfig` except `defaultLanguage`)
|
|
142
|
+
|
|
143
|
+
**Returns:**
|
|
144
|
+
- React Provider component
|
|
145
|
+
|
|
146
|
+
**Configuration Options:**
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
interface ZustandI18nConfig {
|
|
150
|
+
// Initial language to match SSR (prevents hydration errors)
|
|
151
|
+
defaultLanguage?: string;
|
|
152
|
+
|
|
153
|
+
// Fallback language
|
|
154
|
+
fallbackLanguage?: string;
|
|
155
|
+
|
|
156
|
+
// Namespace list
|
|
157
|
+
namespaces?: string[];
|
|
158
|
+
|
|
159
|
+
// Debug mode
|
|
160
|
+
debug?: boolean;
|
|
161
|
+
|
|
162
|
+
// Custom translation loader
|
|
163
|
+
loadTranslations?: (language: string, namespace: string) => Promise<Record<string, string>>;
|
|
164
|
+
|
|
165
|
+
// Translation loader type
|
|
166
|
+
translationLoader?: 'api' | 'static' | 'custom';
|
|
167
|
+
|
|
168
|
+
// API path (when translationLoader is 'api')
|
|
169
|
+
translationApiPath?: string;
|
|
170
|
+
|
|
171
|
+
// SSR initial translation data
|
|
172
|
+
initialTranslations?: Record<string, Record<string, Record<string, string>>>;
|
|
173
|
+
|
|
174
|
+
// Auto language sync (always false in this package)
|
|
175
|
+
autoLanguageSync?: boolean;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### `useZustandI18n(store)`
|
|
180
|
+
|
|
181
|
+
Provides i18n hook integrated with Zustand store.
|
|
182
|
+
|
|
183
|
+
**Parameters:**
|
|
184
|
+
- `store`: Zustand store
|
|
185
|
+
|
|
186
|
+
**Returns:**
|
|
187
|
+
- `{ language, setLanguage }`: Language state and change function
|
|
188
|
+
|
|
189
|
+
**Note:** Use `useTranslation` hook for actual translations:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { useTranslation } from '@hua-labs/i18n-core';
|
|
193
|
+
import { useZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
194
|
+
import { useAppStore } from './store/useAppStore';
|
|
195
|
+
|
|
196
|
+
function MyComponent() {
|
|
197
|
+
const { t } = useTranslation(); // Translation function
|
|
198
|
+
const { language, setLanguage } = useZustandI18n(useAppStore); // Language state
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<p>{t('common:welcome')}</p>
|
|
203
|
+
<button onClick={() => setLanguage('en')}>English</button>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Features
|
|
210
|
+
|
|
211
|
+
- **Type-safe**: Full TypeScript support
|
|
212
|
+
- **Minimal dependencies**: Only Zustand as peer dependency
|
|
213
|
+
- **Auto synchronization**: Automatically syncs Zustand store changes to i18n
|
|
214
|
+
- **Unidirectional data flow**: Zustand store is the source of truth
|
|
215
|
+
- **SSR compatible**: Language syncs after hydration completes (prevents hydration errors)
|
|
216
|
+
- **Circular reference prevention**: Safe language synchronization mechanism
|
|
217
|
+
|
|
218
|
+
## How It Works
|
|
219
|
+
|
|
220
|
+
1. **Initialization**: `createZustandI18n` wraps `createCoreI18n` to create the base Provider.
|
|
221
|
+
2. **Language synchronization**: Detects Zustand store language changes and automatically syncs to i18n.
|
|
222
|
+
3. **Hydration**: Only syncs language after hydration completes to prevent SSR/client hydration errors.
|
|
223
|
+
4. **Circular reference prevention**: Uses `useRef` to prevent infinite loops and maintains unidirectional data flow.
|
|
224
|
+
|
|
225
|
+
## Caveats
|
|
226
|
+
|
|
227
|
+
1. **Zustand store structure**: Store must have `language` and `setLanguage`.
|
|
228
|
+
2. **autoLanguageSync**: This package automatically disables `autoLanguageSync` (Zustand adapter handles it directly).
|
|
229
|
+
3. **Language changes**: Language changes must be made through Zustand store's `setLanguage`.
|
|
230
|
+
4. **SSR initial language**: Use `defaultLanguage` option to set the same initial language as SSR (prevents hydration errors).
|
|
231
|
+
5. **Hydration**: Zustand store language only syncs to i18n after hydration completes.
|
|
232
|
+
|
|
233
|
+
## Examples
|
|
234
|
+
|
|
235
|
+
- **[CodeSandbox Template](../../examples/codesandbox-template/)** - Quick start with Zustand
|
|
236
|
+
- **[Next.js Example](../../examples/next-app-router-example/)** - Complete Next.js example
|
|
237
|
+
|
|
238
|
+
## Documentation
|
|
239
|
+
|
|
240
|
+
- [Hydration Guide](./docs/HYDRATION.md) - Hydration process and troubleshooting
|
|
241
|
+
|
|
242
|
+
## Related Packages
|
|
243
|
+
|
|
244
|
+
- `@hua-labs/i18n-core`: Core i18n library
|
|
245
|
+
- `@hua-labs/i18n-loaders`: Production-ready loaders and caching utilities
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hua-labs/i18n-core-zustand - Zustand 어댑터
|
|
3
|
+
*
|
|
4
|
+
* Zustand 상태관리와 i18n-core를 타입 안전하게 통합하는 어댑터입니다.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { createZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
9
|
+
* import { useAppStore } from './store/useAppStore';
|
|
10
|
+
*
|
|
11
|
+
* // Zustand 스토어에 language와 setLanguage가 있어야 함
|
|
12
|
+
* const I18nProvider = createZustandI18n(useAppStore, {
|
|
13
|
+
* fallbackLanguage: 'en',
|
|
14
|
+
* namespaces: ['common', 'navigation']
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* export default function Layout({ children }) {
|
|
18
|
+
* return <I18nProvider>{children}</I18nProvider>;
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import React from 'react';
|
|
23
|
+
import type { StoreApi, UseBoundStore } from 'zustand';
|
|
24
|
+
/**
|
|
25
|
+
* Zustand 스토어에서 언어 관련 상태를 가져오는 인터페이스
|
|
26
|
+
*/
|
|
27
|
+
export interface ZustandLanguageStore {
|
|
28
|
+
language: string | 'ko' | 'en';
|
|
29
|
+
setLanguage: (lang: string | 'ko' | 'en') => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Zustand 스토어 어댑터 인터페이스
|
|
33
|
+
*/
|
|
34
|
+
export interface ZustandI18nAdapter {
|
|
35
|
+
getLanguage: () => string;
|
|
36
|
+
setLanguage: (lang: string) => void;
|
|
37
|
+
subscribe: (callback: (lang: string) => void) => () => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Zustand 스토어와 i18n-core를 통합하는 Provider 생성
|
|
41
|
+
*
|
|
42
|
+
* @param store - Zustand 스토어 (language와 setLanguage 메서드 필요)
|
|
43
|
+
* @param config - i18n 설정 (defaultLanguage는 스토어에서 가져옴)
|
|
44
|
+
* @returns I18nProvider 컴포넌트
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* import { createZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
49
|
+
* import { useAppStore } from './store/useAppStore';
|
|
50
|
+
*
|
|
51
|
+
* const I18nProvider = createZustandI18n(useAppStore, {
|
|
52
|
+
* fallbackLanguage: 'en',
|
|
53
|
+
* namespaces: ['common', 'navigation', 'footer'],
|
|
54
|
+
* translationLoader: 'api',
|
|
55
|
+
* debug: process.env.NODE_ENV === 'development'
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* export default function RootLayout({ children }) {
|
|
59
|
+
* return <I18nProvider>{children}</I18nProvider>;
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export interface ZustandI18nConfig {
|
|
64
|
+
defaultLanguage?: string;
|
|
65
|
+
fallbackLanguage?: string;
|
|
66
|
+
namespaces?: string[];
|
|
67
|
+
debug?: boolean;
|
|
68
|
+
loadTranslations?: (language: string, namespace: string) => Promise<Record<string, string>>;
|
|
69
|
+
translationLoader?: 'api' | 'static' | 'custom';
|
|
70
|
+
translationApiPath?: string;
|
|
71
|
+
initialTranslations?: Record<string, Record<string, Record<string, string>>>;
|
|
72
|
+
autoLanguageSync?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export declare function createZustandI18n(store: UseBoundStore<StoreApi<ZustandLanguageStore>>, config?: ZustandI18nConfig): React.ComponentType<{
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* Zustand 스토어와 i18n-core를 통합하는 Hook
|
|
79
|
+
*
|
|
80
|
+
* @param store - Zustand 스토어
|
|
81
|
+
* @returns { language, setLanguage, t } - i18n 훅과 동일한 인터페이스
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* import { useZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
86
|
+
* import { useAppStore } from './store/useAppStore';
|
|
87
|
+
*
|
|
88
|
+
* function MyComponent() {
|
|
89
|
+
* const { language, setLanguage, t } = useZustandI18n(useAppStore);
|
|
90
|
+
*
|
|
91
|
+
* return (
|
|
92
|
+
* <div>
|
|
93
|
+
* <p>{t('common:welcome')}</p>
|
|
94
|
+
* <button onClick={() => setLanguage('en')}>English</button>
|
|
95
|
+
* </div>
|
|
96
|
+
* );
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function useZustandI18n(store: UseBoundStore<StoreApi<ZustandLanguageStore>>): {
|
|
101
|
+
language: string;
|
|
102
|
+
setLanguage: (lang: string) => void;
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,MAAM,CAAC;IAC1B,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CAC7D;AA+BD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,iBAAiB;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5F,iBAAiB,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAChD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAC7E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC,EACpD,MAAM,CAAC,EAAE,iBAAiB,GACzB,KAAK,CAAC,aAAa,CAAC;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,CAAC,CAyLpD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;;wBAS3C,MAAM;EAYhB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hua-labs/i18n-core-zustand - Zustand 어댑터
|
|
3
|
+
*
|
|
4
|
+
* Zustand 상태관리와 i18n-core를 타입 안전하게 통합하는 어댑터입니다.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { createZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
9
|
+
* import { useAppStore } from './store/useAppStore';
|
|
10
|
+
*
|
|
11
|
+
* // Zustand 스토어에 language와 setLanguage가 있어야 함
|
|
12
|
+
* const I18nProvider = createZustandI18n(useAppStore, {
|
|
13
|
+
* fallbackLanguage: 'en',
|
|
14
|
+
* namespaces: ['common', 'navigation']
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* export default function Layout({ children }) {
|
|
18
|
+
* return <I18nProvider>{children}</I18nProvider>;
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import React from 'react';
|
|
23
|
+
import { createCoreI18n, useTranslation } from '@hua-labs/i18n-core';
|
|
24
|
+
/**
|
|
25
|
+
* Zustand 스토어에서 어댑터 생성
|
|
26
|
+
*/
|
|
27
|
+
function createZustandAdapter(store) {
|
|
28
|
+
return {
|
|
29
|
+
getLanguage: () => store.getState().language,
|
|
30
|
+
setLanguage: (lang) => {
|
|
31
|
+
const currentLang = store.getState().language;
|
|
32
|
+
if (currentLang !== lang) {
|
|
33
|
+
store.getState().setLanguage(lang);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
subscribe: (callback) => {
|
|
37
|
+
// Zustand의 subscribe를 사용하여 언어 변경 감지
|
|
38
|
+
let prevLanguage = store.getState().language;
|
|
39
|
+
return store.subscribe((state) => {
|
|
40
|
+
const currentLanguage = state.language;
|
|
41
|
+
if (currentLanguage !== prevLanguage) {
|
|
42
|
+
prevLanguage = currentLanguage;
|
|
43
|
+
callback(currentLanguage);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function createZustandI18n(store, config) {
|
|
50
|
+
const adapter = createZustandAdapter(store);
|
|
51
|
+
// 하이드레이션 에러 방지: SSR과 동일한 초기 언어 사용
|
|
52
|
+
// config에 defaultLanguage가 있으면 사용, 없으면 'ko' (SSR 기본값과 일치)
|
|
53
|
+
// 하이드레이션 완료 후 저장된 언어로 자동 동기화됨
|
|
54
|
+
const initialLanguage = config?.defaultLanguage || 'ko';
|
|
55
|
+
const storeLanguage = adapter.getLanguage();
|
|
56
|
+
// createCoreI18n으로 기본 Provider 생성
|
|
57
|
+
const BaseI18nProvider = createCoreI18n({
|
|
58
|
+
...config,
|
|
59
|
+
defaultLanguage: initialLanguage, // SSR과 동일한 초기 언어 사용
|
|
60
|
+
// Zustand 어댑터가 직접 언어 동기화 처리하므로 autoLanguageSync 비활성화
|
|
61
|
+
autoLanguageSync: false
|
|
62
|
+
});
|
|
63
|
+
// 언어 동기화 래퍼 컴포넌트 (Provider 내부에서만 사용)
|
|
64
|
+
// BaseI18nProvider가 I18nProvider를 렌더링하므로, 그 자식으로 들어가면 useTranslation 사용 가능
|
|
65
|
+
function LanguageSyncWrapper({ children: innerChildren }) {
|
|
66
|
+
const debug = config?.debug ?? false;
|
|
67
|
+
// useTranslation은 I18nProvider 내부에서만 사용 가능
|
|
68
|
+
// BaseI18nProvider가 I18nProvider를 렌더링하므로 여기서 사용 가능
|
|
69
|
+
const { setLanguage: setI18nLanguage, currentLanguage, isInitialized } = useTranslation();
|
|
70
|
+
const hydrationStateRef = React.useRef({
|
|
71
|
+
isComplete: false,
|
|
72
|
+
isInitialized: false,
|
|
73
|
+
previousStoreLanguage: null,
|
|
74
|
+
currentI18nLanguage: currentLanguage,
|
|
75
|
+
});
|
|
76
|
+
// currentLanguage가 변경되면 상태 업데이트
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
hydrationStateRef.current.currentI18nLanguage = currentLanguage;
|
|
79
|
+
}, [currentLanguage]);
|
|
80
|
+
// 하이드레이션 완료 감지 및 언어 동기화
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
if (typeof window === 'undefined' || hydrationStateRef.current.isComplete) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const checkHydration = () => {
|
|
86
|
+
if (hydrationStateRef.current.isComplete) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
hydrationStateRef.current.isComplete = true;
|
|
90
|
+
hydrationStateRef.current.isInitialized = isInitialized;
|
|
91
|
+
if (debug) {
|
|
92
|
+
console.log(`✅ [ZUSTAND-I18N] Hydration complete`);
|
|
93
|
+
}
|
|
94
|
+
// 하이드레이션 완료 후 저장된 언어로 동기화
|
|
95
|
+
if (isInitialized) {
|
|
96
|
+
const storeLanguage = store.getState().language;
|
|
97
|
+
const state = hydrationStateRef.current;
|
|
98
|
+
// initialLanguage와 다르고, 현재 i18n 언어와도 다를 때만 동기화
|
|
99
|
+
if (storeLanguage !== initialLanguage && storeLanguage !== state.currentI18nLanguage) {
|
|
100
|
+
if (debug) {
|
|
101
|
+
console.log(`🔄 [ZUSTAND-I18N] Hydration complete, syncing language: ${state.currentI18nLanguage} -> ${storeLanguage}`);
|
|
102
|
+
}
|
|
103
|
+
setI18nLanguage(storeLanguage);
|
|
104
|
+
state.previousStoreLanguage = storeLanguage;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (debug) {
|
|
108
|
+
console.log(`⏭️ [ZUSTAND-I18N] Hydration complete, no sync needed (store: ${storeLanguage}, initial: ${initialLanguage}, current: ${state.currentI18nLanguage})`);
|
|
109
|
+
}
|
|
110
|
+
state.previousStoreLanguage = storeLanguage;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
// 브라우저가 준비되면 하이드레이션 완료로 간주
|
|
115
|
+
const timeoutId = setTimeout(() => {
|
|
116
|
+
requestAnimationFrame(checkHydration);
|
|
117
|
+
}, 0);
|
|
118
|
+
return () => clearTimeout(timeoutId);
|
|
119
|
+
}, [isInitialized, setI18nLanguage, initialLanguage, debug]);
|
|
120
|
+
// 언어 동기화 함수 (재사용)
|
|
121
|
+
const syncLanguageFromStore = React.useCallback(() => {
|
|
122
|
+
const state = hydrationStateRef.current;
|
|
123
|
+
if (!state.isInitialized || !state.isComplete) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const storeLanguage = store.getState().language;
|
|
127
|
+
if (storeLanguage !== state.currentI18nLanguage && storeLanguage !== initialLanguage) {
|
|
128
|
+
if (debug) {
|
|
129
|
+
console.log(`🔄 [ZUSTAND-I18N] Syncing language from store: ${state.currentI18nLanguage} -> ${storeLanguage}`);
|
|
130
|
+
}
|
|
131
|
+
setI18nLanguage(storeLanguage);
|
|
132
|
+
state.previousStoreLanguage = storeLanguage;
|
|
133
|
+
}
|
|
134
|
+
}, [setI18nLanguage, initialLanguage, debug]);
|
|
135
|
+
// 언어 변경 구독 설정
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
// Translator가 초기화된 후에만 동기화
|
|
138
|
+
if (!isInitialized) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const state = hydrationStateRef.current;
|
|
142
|
+
state.isInitialized = true;
|
|
143
|
+
// 초기 스토어 언어 설정
|
|
144
|
+
if (state.previousStoreLanguage === null) {
|
|
145
|
+
state.previousStoreLanguage = store.getState().language;
|
|
146
|
+
}
|
|
147
|
+
// Zustand 스토어 변경 감지
|
|
148
|
+
const unsubscribe = adapter.subscribe((newLanguage) => {
|
|
149
|
+
// 이전 언어와 다를 때만 처리
|
|
150
|
+
if (newLanguage !== state.previousStoreLanguage) {
|
|
151
|
+
state.previousStoreLanguage = newLanguage;
|
|
152
|
+
// 하이드레이션 완료 후에만 동기화
|
|
153
|
+
if (state.isComplete) {
|
|
154
|
+
// 현재 i18n 언어와 다를 때만 동기화 (무한 루프 방지)
|
|
155
|
+
if (newLanguage !== state.currentI18nLanguage) {
|
|
156
|
+
if (debug) {
|
|
157
|
+
console.log(`🔄 [ZUSTAND-I18N] Store language changed, syncing to i18n: ${state.currentI18nLanguage} -> ${newLanguage}`);
|
|
158
|
+
}
|
|
159
|
+
setI18nLanguage(newLanguage);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
if (debug) {
|
|
163
|
+
console.log(`⏭️ [ZUSTAND-I18N] Store language changed but i18n already synced: ${newLanguage}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// 하이드레이션 완료 전에는 로그만 출력
|
|
169
|
+
if (debug) {
|
|
170
|
+
console.log(`⏳ [ZUSTAND-I18N] Store language changed but hydration not complete yet: ${newLanguage}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// 하이드레이션이 이미 완료되었다면 즉시 동기화
|
|
176
|
+
if (state.isComplete) {
|
|
177
|
+
const storeLanguage = store.getState().language;
|
|
178
|
+
// initialLanguage와 다르고, 현재 i18n 언어와도 다를 때만 동기화
|
|
179
|
+
if (storeLanguage !== initialLanguage && storeLanguage !== state.currentI18nLanguage) {
|
|
180
|
+
if (debug) {
|
|
181
|
+
console.log(`🔄 [ZUSTAND-I18N] Already hydrated, syncing language: ${state.currentI18nLanguage} -> ${storeLanguage}`);
|
|
182
|
+
}
|
|
183
|
+
setI18nLanguage(storeLanguage);
|
|
184
|
+
state.previousStoreLanguage = storeLanguage;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
if (debug) {
|
|
188
|
+
console.log(`⏭️ [ZUSTAND-I18N] Already hydrated, no sync needed (store: ${storeLanguage}, initial: ${initialLanguage}, current: ${state.currentI18nLanguage})`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return unsubscribe;
|
|
193
|
+
}, [isInitialized, setI18nLanguage, initialLanguage, debug]);
|
|
194
|
+
// 하이드레이션 완료 후 언어 동기화를 위한 별도 useEffect
|
|
195
|
+
// hydratedRef는 ref이므로 의존성으로 사용할 수 없음
|
|
196
|
+
// 대신 하이드레이션 완료 시점에 직접 syncLanguageFromStore 호출
|
|
197
|
+
return React.createElement(React.Fragment, null, innerChildren);
|
|
198
|
+
}
|
|
199
|
+
// Zustand 스토어 구독을 포함한 래퍼 Provider
|
|
200
|
+
return function ZustandI18nProvider({ children }) {
|
|
201
|
+
return React.createElement(BaseI18nProvider, {
|
|
202
|
+
children: React.createElement(LanguageSyncWrapper, { children })
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Zustand 스토어와 i18n-core를 통합하는 Hook
|
|
208
|
+
*
|
|
209
|
+
* @param store - Zustand 스토어
|
|
210
|
+
* @returns { language, setLanguage, t } - i18n 훅과 동일한 인터페이스
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```tsx
|
|
214
|
+
* import { useZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
215
|
+
* import { useAppStore } from './store/useAppStore';
|
|
216
|
+
*
|
|
217
|
+
* function MyComponent() {
|
|
218
|
+
* const { language, setLanguage, t } = useZustandI18n(useAppStore);
|
|
219
|
+
*
|
|
220
|
+
* return (
|
|
221
|
+
* <div>
|
|
222
|
+
* <p>{t('common:welcome')}</p>
|
|
223
|
+
* <button onClick={() => setLanguage('en')}>English</button>
|
|
224
|
+
* </div>
|
|
225
|
+
* );
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export function useZustandI18n(store) {
|
|
230
|
+
const adapter = React.useMemo(() => createZustandAdapter(store), [store]);
|
|
231
|
+
// 스토어의 언어 상태 구독
|
|
232
|
+
const language = store((state) => state.language);
|
|
233
|
+
// 언어 변경 함수
|
|
234
|
+
const setLanguage = React.useCallback((lang) => {
|
|
235
|
+
adapter.setLanguage(lang);
|
|
236
|
+
}, [adapter]);
|
|
237
|
+
return {
|
|
238
|
+
language,
|
|
239
|
+
setLanguage,
|
|
240
|
+
// useTranslation 훅은 별도로 import해서 사용
|
|
241
|
+
// 이 함수는 Zustand 스토어와의 통합만 제공
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// 타입은 이미 위에서 export되었으므로 중복 export 제거
|
|
245
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hua-labs/i18n-core-zustand",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zustand adapter for @hua-labs/i18n-core - Type-safe state management integration",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@hua-labs/i18n-core": "1.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"zustand": "^4.0.0",
|
|
24
|
+
"react": ">=16.8.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.0.0",
|
|
28
|
+
"@types/react": "^19.2.7",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"zustand": "^4.0.0"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"i18n",
|
|
34
|
+
"internationalization",
|
|
35
|
+
"zustand",
|
|
36
|
+
"state-management",
|
|
37
|
+
"adapter"
|
|
38
|
+
],
|
|
39
|
+
"author": "HUA Labs",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/HUA-Labs/HUA-Labs-public.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/HUA-Labs/HUA-Labs-public/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/HUA-Labs/HUA-Labs-public#readme",
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc",
|
|
51
|
+
"dev": "tsc --watch",
|
|
52
|
+
"clean": "rm -rf dist",
|
|
53
|
+
"test": "echo \"No tests defined\"",
|
|
54
|
+
"lint": "echo 'Skipping lint for hua-i18n-core-zustand'",
|
|
55
|
+
"type-check": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hua-labs/i18n-core-zustand - Zustand 어댑터
|
|
3
|
+
*
|
|
4
|
+
* Zustand 상태관리와 i18n-core를 타입 안전하게 통합하는 어댑터입니다.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { createZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
9
|
+
* import { useAppStore } from './store/useAppStore';
|
|
10
|
+
*
|
|
11
|
+
* // Zustand 스토어에 language와 setLanguage가 있어야 함
|
|
12
|
+
* const I18nProvider = createZustandI18n(useAppStore, {
|
|
13
|
+
* fallbackLanguage: 'en',
|
|
14
|
+
* namespaces: ['common', 'navigation']
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* export default function Layout({ children }) {
|
|
18
|
+
* return <I18nProvider>{children}</I18nProvider>;
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import { createCoreI18n, useTranslation } from '@hua-labs/i18n-core';
|
|
25
|
+
import type { StoreApi, UseBoundStore } from 'zustand';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Zustand 스토어에서 언어 관련 상태를 가져오는 인터페이스
|
|
29
|
+
*/
|
|
30
|
+
export interface ZustandLanguageStore {
|
|
31
|
+
language: string | 'ko' | 'en';
|
|
32
|
+
setLanguage: (lang: string | 'ko' | 'en') => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Zustand 스토어 어댑터 인터페이스
|
|
37
|
+
*/
|
|
38
|
+
export interface ZustandI18nAdapter {
|
|
39
|
+
getLanguage: () => string;
|
|
40
|
+
setLanguage: (lang: string) => void;
|
|
41
|
+
subscribe: (callback: (lang: string) => void) => () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Zustand 스토어에서 어댑터 생성
|
|
46
|
+
*/
|
|
47
|
+
function createZustandAdapter(
|
|
48
|
+
store: UseBoundStore<StoreApi<ZustandLanguageStore>>
|
|
49
|
+
): ZustandI18nAdapter {
|
|
50
|
+
return {
|
|
51
|
+
getLanguage: () => store.getState().language,
|
|
52
|
+
setLanguage: (lang: string) => {
|
|
53
|
+
const currentLang = store.getState().language;
|
|
54
|
+
if (currentLang !== lang) {
|
|
55
|
+
store.getState().setLanguage(lang);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
subscribe: (callback: (lang: string) => void) => {
|
|
59
|
+
// Zustand의 subscribe를 사용하여 언어 변경 감지
|
|
60
|
+
let prevLanguage = store.getState().language;
|
|
61
|
+
|
|
62
|
+
return store.subscribe((state) => {
|
|
63
|
+
const currentLanguage = state.language;
|
|
64
|
+
if (currentLanguage !== prevLanguage) {
|
|
65
|
+
prevLanguage = currentLanguage;
|
|
66
|
+
callback(currentLanguage);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Zustand 스토어와 i18n-core를 통합하는 Provider 생성
|
|
75
|
+
*
|
|
76
|
+
* @param store - Zustand 스토어 (language와 setLanguage 메서드 필요)
|
|
77
|
+
* @param config - i18n 설정 (defaultLanguage는 스토어에서 가져옴)
|
|
78
|
+
* @returns I18nProvider 컴포넌트
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* import { createZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
83
|
+
* import { useAppStore } from './store/useAppStore';
|
|
84
|
+
*
|
|
85
|
+
* const I18nProvider = createZustandI18n(useAppStore, {
|
|
86
|
+
* fallbackLanguage: 'en',
|
|
87
|
+
* namespaces: ['common', 'navigation', 'footer'],
|
|
88
|
+
* translationLoader: 'api',
|
|
89
|
+
* debug: process.env.NODE_ENV === 'development'
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* export default function RootLayout({ children }) {
|
|
93
|
+
* return <I18nProvider>{children}</I18nProvider>;
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export interface ZustandI18nConfig {
|
|
98
|
+
defaultLanguage?: string; // SSR과 일치시키기 위한 초기 언어 (하이드레이션 에러 방지)
|
|
99
|
+
fallbackLanguage?: string;
|
|
100
|
+
namespaces?: string[];
|
|
101
|
+
debug?: boolean;
|
|
102
|
+
loadTranslations?: (language: string, namespace: string) => Promise<Record<string, string>>;
|
|
103
|
+
translationLoader?: 'api' | 'static' | 'custom';
|
|
104
|
+
translationApiPath?: string;
|
|
105
|
+
initialTranslations?: Record<string, Record<string, Record<string, string>>>;
|
|
106
|
+
autoLanguageSync?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createZustandI18n(
|
|
110
|
+
store: UseBoundStore<StoreApi<ZustandLanguageStore>>,
|
|
111
|
+
config?: ZustandI18nConfig
|
|
112
|
+
): React.ComponentType<{ children: React.ReactNode }> {
|
|
113
|
+
const adapter = createZustandAdapter(store);
|
|
114
|
+
|
|
115
|
+
// 하이드레이션 에러 방지: SSR과 동일한 초기 언어 사용
|
|
116
|
+
// config에 defaultLanguage가 있으면 사용, 없으면 'ko' (SSR 기본값과 일치)
|
|
117
|
+
// 하이드레이션 완료 후 저장된 언어로 자동 동기화됨
|
|
118
|
+
const initialLanguage = config?.defaultLanguage || 'ko';
|
|
119
|
+
const storeLanguage = adapter.getLanguage();
|
|
120
|
+
|
|
121
|
+
// createCoreI18n으로 기본 Provider 생성
|
|
122
|
+
const BaseI18nProvider = createCoreI18n({
|
|
123
|
+
...config,
|
|
124
|
+
defaultLanguage: initialLanguage, // SSR과 동일한 초기 언어 사용
|
|
125
|
+
// Zustand 어댑터가 직접 언어 동기화 처리하므로 autoLanguageSync 비활성화
|
|
126
|
+
autoLanguageSync: false
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 언어 동기화 래퍼 컴포넌트 (Provider 내부에서만 사용)
|
|
130
|
+
// BaseI18nProvider가 I18nProvider를 렌더링하므로, 그 자식으로 들어가면 useTranslation 사용 가능
|
|
131
|
+
function LanguageSyncWrapper({ children: innerChildren }: { children: React.ReactNode }) {
|
|
132
|
+
const debug = config?.debug ?? false;
|
|
133
|
+
// useTranslation은 I18nProvider 내부에서만 사용 가능
|
|
134
|
+
// BaseI18nProvider가 I18nProvider를 렌더링하므로 여기서 사용 가능
|
|
135
|
+
const { setLanguage: setI18nLanguage, currentLanguage, isInitialized } = useTranslation();
|
|
136
|
+
|
|
137
|
+
// 하이드레이션 상태를 하나의 객체로 관리
|
|
138
|
+
interface HydrationState {
|
|
139
|
+
isComplete: boolean;
|
|
140
|
+
isInitialized: boolean;
|
|
141
|
+
previousStoreLanguage: string | null;
|
|
142
|
+
currentI18nLanguage: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const hydrationStateRef = React.useRef<HydrationState>({
|
|
146
|
+
isComplete: false,
|
|
147
|
+
isInitialized: false,
|
|
148
|
+
previousStoreLanguage: null,
|
|
149
|
+
currentI18nLanguage: currentLanguage,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// currentLanguage가 변경되면 상태 업데이트
|
|
153
|
+
React.useEffect(() => {
|
|
154
|
+
hydrationStateRef.current.currentI18nLanguage = currentLanguage;
|
|
155
|
+
}, [currentLanguage]);
|
|
156
|
+
|
|
157
|
+
// 하이드레이션 완료 감지 및 언어 동기화
|
|
158
|
+
React.useEffect(() => {
|
|
159
|
+
if (typeof window === 'undefined' || hydrationStateRef.current.isComplete) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const checkHydration = () => {
|
|
164
|
+
if (hydrationStateRef.current.isComplete) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
hydrationStateRef.current.isComplete = true;
|
|
169
|
+
hydrationStateRef.current.isInitialized = isInitialized;
|
|
170
|
+
|
|
171
|
+
if (debug) {
|
|
172
|
+
console.log(`✅ [ZUSTAND-I18N] Hydration complete`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 하이드레이션 완료 후 저장된 언어로 동기화
|
|
176
|
+
if (isInitialized) {
|
|
177
|
+
const storeLanguage = store.getState().language;
|
|
178
|
+
const state = hydrationStateRef.current;
|
|
179
|
+
|
|
180
|
+
// initialLanguage와 다르고, 현재 i18n 언어와도 다를 때만 동기화
|
|
181
|
+
if (storeLanguage !== initialLanguage && storeLanguage !== state.currentI18nLanguage) {
|
|
182
|
+
if (debug) {
|
|
183
|
+
console.log(`🔄 [ZUSTAND-I18N] Hydration complete, syncing language: ${state.currentI18nLanguage} -> ${storeLanguage}`);
|
|
184
|
+
}
|
|
185
|
+
setI18nLanguage(storeLanguage);
|
|
186
|
+
state.previousStoreLanguage = storeLanguage;
|
|
187
|
+
} else {
|
|
188
|
+
if (debug) {
|
|
189
|
+
console.log(`⏭️ [ZUSTAND-I18N] Hydration complete, no sync needed (store: ${storeLanguage}, initial: ${initialLanguage}, current: ${state.currentI18nLanguage})`);
|
|
190
|
+
}
|
|
191
|
+
state.previousStoreLanguage = storeLanguage;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// 브라우저가 준비되면 하이드레이션 완료로 간주
|
|
197
|
+
const timeoutId = setTimeout(() => {
|
|
198
|
+
requestAnimationFrame(checkHydration);
|
|
199
|
+
}, 0);
|
|
200
|
+
|
|
201
|
+
return () => clearTimeout(timeoutId);
|
|
202
|
+
}, [isInitialized, setI18nLanguage, initialLanguage, debug]);
|
|
203
|
+
|
|
204
|
+
// 언어 동기화 함수 (재사용)
|
|
205
|
+
const syncLanguageFromStore = React.useCallback(() => {
|
|
206
|
+
const state = hydrationStateRef.current;
|
|
207
|
+
if (!state.isInitialized || !state.isComplete) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const storeLanguage = store.getState().language;
|
|
212
|
+
if (storeLanguage !== state.currentI18nLanguage && storeLanguage !== initialLanguage) {
|
|
213
|
+
if (debug) {
|
|
214
|
+
console.log(`🔄 [ZUSTAND-I18N] Syncing language from store: ${state.currentI18nLanguage} -> ${storeLanguage}`);
|
|
215
|
+
}
|
|
216
|
+
setI18nLanguage(storeLanguage);
|
|
217
|
+
state.previousStoreLanguage = storeLanguage;
|
|
218
|
+
}
|
|
219
|
+
}, [setI18nLanguage, initialLanguage, debug]);
|
|
220
|
+
|
|
221
|
+
// 언어 변경 구독 설정
|
|
222
|
+
React.useEffect(() => {
|
|
223
|
+
// Translator가 초기화된 후에만 동기화
|
|
224
|
+
if (!isInitialized) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const state = hydrationStateRef.current;
|
|
229
|
+
state.isInitialized = true;
|
|
230
|
+
|
|
231
|
+
// 초기 스토어 언어 설정
|
|
232
|
+
if (state.previousStoreLanguage === null) {
|
|
233
|
+
state.previousStoreLanguage = store.getState().language;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Zustand 스토어 변경 감지
|
|
237
|
+
const unsubscribe = adapter.subscribe((newLanguage) => {
|
|
238
|
+
// 이전 언어와 다를 때만 처리
|
|
239
|
+
if (newLanguage !== state.previousStoreLanguage) {
|
|
240
|
+
state.previousStoreLanguage = newLanguage;
|
|
241
|
+
|
|
242
|
+
// 하이드레이션 완료 후에만 동기화
|
|
243
|
+
if (state.isComplete) {
|
|
244
|
+
// 현재 i18n 언어와 다를 때만 동기화 (무한 루프 방지)
|
|
245
|
+
if (newLanguage !== state.currentI18nLanguage) {
|
|
246
|
+
if (debug) {
|
|
247
|
+
console.log(`🔄 [ZUSTAND-I18N] Store language changed, syncing to i18n: ${state.currentI18nLanguage} -> ${newLanguage}`);
|
|
248
|
+
}
|
|
249
|
+
setI18nLanguage(newLanguage);
|
|
250
|
+
} else {
|
|
251
|
+
if (debug) {
|
|
252
|
+
console.log(`⏭️ [ZUSTAND-I18N] Store language changed but i18n already synced: ${newLanguage}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
// 하이드레이션 완료 전에는 로그만 출력
|
|
257
|
+
if (debug) {
|
|
258
|
+
console.log(`⏳ [ZUSTAND-I18N] Store language changed but hydration not complete yet: ${newLanguage}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// 하이드레이션이 이미 완료되었다면 즉시 동기화
|
|
265
|
+
if (state.isComplete) {
|
|
266
|
+
const storeLanguage = store.getState().language;
|
|
267
|
+
// initialLanguage와 다르고, 현재 i18n 언어와도 다를 때만 동기화
|
|
268
|
+
if (storeLanguage !== initialLanguage && storeLanguage !== state.currentI18nLanguage) {
|
|
269
|
+
if (debug) {
|
|
270
|
+
console.log(`🔄 [ZUSTAND-I18N] Already hydrated, syncing language: ${state.currentI18nLanguage} -> ${storeLanguage}`);
|
|
271
|
+
}
|
|
272
|
+
setI18nLanguage(storeLanguage);
|
|
273
|
+
state.previousStoreLanguage = storeLanguage;
|
|
274
|
+
} else {
|
|
275
|
+
if (debug) {
|
|
276
|
+
console.log(`⏭️ [ZUSTAND-I18N] Already hydrated, no sync needed (store: ${storeLanguage}, initial: ${initialLanguage}, current: ${state.currentI18nLanguage})`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return unsubscribe;
|
|
282
|
+
}, [isInitialized, setI18nLanguage, initialLanguage, debug]);
|
|
283
|
+
|
|
284
|
+
// 하이드레이션 완료 후 언어 동기화를 위한 별도 useEffect
|
|
285
|
+
// hydratedRef는 ref이므로 의존성으로 사용할 수 없음
|
|
286
|
+
// 대신 하이드레이션 완료 시점에 직접 syncLanguageFromStore 호출
|
|
287
|
+
|
|
288
|
+
return React.createElement(React.Fragment, null, innerChildren);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Zustand 스토어 구독을 포함한 래퍼 Provider
|
|
292
|
+
return function ZustandI18nProvider({ children }: { children: React.ReactNode }) {
|
|
293
|
+
return React.createElement(BaseI18nProvider, {
|
|
294
|
+
children: React.createElement(LanguageSyncWrapper, { children })
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Zustand 스토어와 i18n-core를 통합하는 Hook
|
|
301
|
+
*
|
|
302
|
+
* @param store - Zustand 스토어
|
|
303
|
+
* @returns { language, setLanguage, t } - i18n 훅과 동일한 인터페이스
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```tsx
|
|
307
|
+
* import { useZustandI18n } from '@hua-labs/i18n-core-zustand';
|
|
308
|
+
* import { useAppStore } from './store/useAppStore';
|
|
309
|
+
*
|
|
310
|
+
* function MyComponent() {
|
|
311
|
+
* const { language, setLanguage, t } = useZustandI18n(useAppStore);
|
|
312
|
+
*
|
|
313
|
+
* return (
|
|
314
|
+
* <div>
|
|
315
|
+
* <p>{t('common:welcome')}</p>
|
|
316
|
+
* <button onClick={() => setLanguage('en')}>English</button>
|
|
317
|
+
* </div>
|
|
318
|
+
* );
|
|
319
|
+
* }
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
export function useZustandI18n(
|
|
323
|
+
store: UseBoundStore<StoreApi<ZustandLanguageStore>>
|
|
324
|
+
) {
|
|
325
|
+
const adapter = React.useMemo(() => createZustandAdapter(store), [store]);
|
|
326
|
+
|
|
327
|
+
// 스토어의 언어 상태 구독
|
|
328
|
+
const language = store((state) => state.language);
|
|
329
|
+
|
|
330
|
+
// 언어 변경 함수
|
|
331
|
+
const setLanguage = React.useCallback(
|
|
332
|
+
(lang: string) => {
|
|
333
|
+
adapter.setLanguage(lang);
|
|
334
|
+
},
|
|
335
|
+
[adapter]
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
language,
|
|
340
|
+
setLanguage,
|
|
341
|
+
// useTranslation 훅은 별도로 import해서 사용
|
|
342
|
+
// 이 함수는 Zustand 스토어와의 통합만 제공
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 타입은 이미 위에서 export되었으므로 중복 export 제거
|
|
347
|
+
|