@hua-labs/i18n-core-zustand 1.0.0 → 1.1.0-alpha.0.2

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/src/index.ts CHANGED
@@ -1,347 +1,399 @@
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
-
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
+ * 지원되는 언어 코드 타입
29
+ * ISO 639-1 표준 언어 코드
30
+ */
31
+ export type SupportedLanguage = 'ko' | 'en' | 'ja';
32
+
33
+ /**
34
+ * Zustand 스토어에서 언어 관련 상태를 가져오는 인터페이스
35
+ *
36
+ * @template L - 언어 코드 타입 (기본값: SupportedLanguage | string)
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * // 기본 사용 (모든 언어 코드 허용)
41
+ * interface MyStore extends ZustandLanguageStore {}
42
+ *
43
+ * // 특정 언어만 허용
44
+ * interface MyStore extends ZustandLanguageStore<'ko' | 'en'> {}
45
+ * ```
46
+ */
47
+ export interface ZustandLanguageStore<L extends string = SupportedLanguage | string> {
48
+ language: L;
49
+ setLanguage: (lang: L) => void;
50
+ }
51
+
52
+ /**
53
+ * Zustand 스토어 어댑터 인터페이스
54
+ */
55
+ export interface ZustandI18nAdapter {
56
+ getLanguage: () => string;
57
+ setLanguage: (lang: string) => void;
58
+ subscribe: (callback: (lang: string) => void) => () => void;
59
+ }
60
+
61
+ /**
62
+ * Zustand 스토어에서 어댑터 생성
63
+ *
64
+ * @template L - 언어 코드 타입
65
+ */
66
+ function createZustandAdapter<L extends string = SupportedLanguage | string>(
67
+ store: UseBoundStore<StoreApi<ZustandLanguageStore<L>>>
68
+ ): ZustandI18nAdapter {
69
+ return {
70
+ getLanguage: () => store.getState().language,
71
+ setLanguage: (lang: string) => {
72
+ const currentLang = store.getState().language;
73
+ if (currentLang !== lang) {
74
+ // 어댑터는 string을 받지만, 스토어는 L 타입을 기대하므로 타입 단언 필요
75
+ store.getState().setLanguage(lang as L);
76
+ }
77
+ },
78
+ subscribe: (callback: (lang: string) => void) => {
79
+ // Zustand의 subscribe를 사용하여 언어 변경 감지
80
+ let prevLanguage = store.getState().language;
81
+
82
+ return store.subscribe((state) => {
83
+ const currentLanguage = state.language;
84
+ if (currentLanguage !== prevLanguage) {
85
+ prevLanguage = currentLanguage;
86
+ callback(currentLanguage);
87
+ }
88
+ });
89
+ }
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Zustand 스토어와 i18n-core를 통합하는 Provider 생성
95
+ *
96
+ * @param store - Zustand 스토어 (language와 setLanguage 메서드 필요)
97
+ * @param config - i18n 설정 (defaultLanguage는 스토어에서 가져옴)
98
+ * @returns I18nProvider 컴포넌트
99
+ *
100
+ * @example
101
+ * ```tsx
102
+ * import { createZustandI18n } from '@hua-labs/i18n-core-zustand';
103
+ * import { useAppStore } from './store/useAppStore';
104
+ *
105
+ * const I18nProvider = createZustandI18n(useAppStore, {
106
+ * fallbackLanguage: 'en',
107
+ * namespaces: ['common', 'navigation', 'footer'],
108
+ * translationLoader: 'api',
109
+ * debug: process.env.NODE_ENV === 'development'
110
+ * });
111
+ *
112
+ * export default function RootLayout({ children }) {
113
+ * return <I18nProvider>{children}</I18nProvider>;
114
+ * }
115
+ * ```
116
+ */
117
+ export interface ZustandI18nConfig {
118
+ defaultLanguage?: string; // SSR과 일치시키기 위한 초기 언어 (하이드레이션 에러 방지)
119
+ fallbackLanguage?: string;
120
+ namespaces?: string[];
121
+ debug?: boolean;
122
+ loadTranslations?: (language: string, namespace: string) => Promise<Record<string, string>>;
123
+ translationLoader?: 'api' | 'static' | 'custom';
124
+ translationApiPath?: string;
125
+ initialTranslations?: Record<string, Record<string, Record<string, string>>>;
126
+ autoLanguageSync?: boolean;
127
+ /**
128
+ * document.documentElement.lang 자동 업데이트 여부
129
+ * 기본값: false (사용자가 직접 관리)
130
+ * true로 설정하면 언어 변경 자동으로 html[lang] 속성 업데이트
131
+ */
132
+ autoUpdateHtmlLang?: boolean;
133
+ }
134
+
135
+ /**
136
+ * Zustand 스토어와 i18n-core를 통합하는 Provider 생성
137
+ *
138
+ * @template L - 언어 코드 타입
139
+ * @param store - Zustand 스토어 (language와 setLanguage 메서드 필요)
140
+ * @param config - i18n 설정
141
+ * @returns I18nProvider 컴포넌트
142
+ */
143
+ export function createZustandI18n<L extends string = SupportedLanguage | string>(
144
+ store: UseBoundStore<StoreApi<ZustandLanguageStore<L>>>,
145
+ config?: ZustandI18nConfig
146
+ ): React.ComponentType<{ children: React.ReactNode }> {
147
+ const adapter = createZustandAdapter(store);
148
+
149
+ // 하이드레이션 에러 방지: SSR과 동일한 초기 언어 사용
150
+ // config에 defaultLanguage가 있으면 사용, 없으면 'ko' (SSR 기본값과 일치)
151
+ // 하이드레이션 완료 후 저장된 언어로 자동 동기화됨
152
+ const initialLanguage = config?.defaultLanguage || 'ko';
153
+ const storeLanguage = adapter.getLanguage();
154
+
155
+ // createCoreI18n으로 기본 Provider 생성
156
+ const BaseI18nProvider = createCoreI18n({
157
+ ...config,
158
+ defaultLanguage: initialLanguage, // SSR과 동일한 초기 언어 사용
159
+ // Zustand 어댑터가 직접 언어 동기화 처리하므로 autoLanguageSync 비활성화
160
+ autoLanguageSync: false
161
+ });
162
+
163
+ // 언어 동기화 래퍼 컴포넌트 (Provider 내부에서만 사용)
164
+ // BaseI18nProvider가 I18nProvider를 렌더링하므로, 그 자식으로 들어가면 useTranslation 사용 가능
165
+ function LanguageSyncWrapper({ children: innerChildren }: { children: React.ReactNode }) {
166
+ const debug = config?.debug ?? false;
167
+ const autoUpdateHtmlLang = config?.autoUpdateHtmlLang ?? false;
168
+ // useTranslation은 I18nProvider 내부에서만 사용 가능
169
+ // BaseI18nProvider가 I18nProvider를 렌더링하므로 여기서 사용 가능
170
+ const { setLanguage: setI18nLanguage, currentLanguage, isInitialized } = useTranslation();
171
+
172
+ // document.documentElement.lang 자동 업데이트
173
+ React.useEffect(() => {
174
+ if (autoUpdateHtmlLang && typeof document !== 'undefined') {
175
+ document.documentElement.lang = currentLanguage;
176
+ if (debug) {
177
+ console.log(`[ZUSTAND-I18N] Updated html[lang] to: ${currentLanguage}`);
178
+ }
179
+ }
180
+ }, [currentLanguage, autoUpdateHtmlLang, debug]);
181
+
182
+ // 하이드레이션 상태를 하나의 객체로 관리
183
+ interface HydrationState {
184
+ isComplete: boolean;
185
+ isInitialized: boolean;
186
+ previousStoreLanguage: string | null;
187
+ currentI18nLanguage: string;
188
+ }
189
+
190
+ const hydrationStateRef = React.useRef<HydrationState>({
191
+ isComplete: false,
192
+ isInitialized: false,
193
+ previousStoreLanguage: null,
194
+ currentI18nLanguage: currentLanguage,
195
+ });
196
+
197
+ // currentLanguage가 변경되면 상태 업데이트
198
+ React.useEffect(() => {
199
+ hydrationStateRef.current.currentI18nLanguage = currentLanguage;
200
+ }, [currentLanguage]);
201
+
202
+ // 하이드레이션 완료 감지 및 언어 동기화
203
+ React.useEffect(() => {
204
+ if (typeof window === 'undefined' || hydrationStateRef.current.isComplete) {
205
+ return;
206
+ }
207
+
208
+ const checkHydration = () => {
209
+ if (hydrationStateRef.current.isComplete) {
210
+ return;
211
+ }
212
+
213
+ hydrationStateRef.current.isComplete = true;
214
+ hydrationStateRef.current.isInitialized = isInitialized;
215
+
216
+ if (debug) {
217
+ console.log(`✅ [ZUSTAND-I18N] Hydration complete`);
218
+ }
219
+
220
+ // 하이드레이션 완료 후 저장된 언어로 동기화
221
+ if (isInitialized) {
222
+ const storeLanguage = store.getState().language;
223
+ const state = hydrationStateRef.current;
224
+
225
+ // initialLanguage와 다르고, 현재 i18n 언어와도 다를 때만 동기화
226
+ if (storeLanguage !== initialLanguage && storeLanguage !== state.currentI18nLanguage) {
227
+ if (debug) {
228
+ console.log(`🔄 [ZUSTAND-I18N] Hydration complete, syncing language: ${state.currentI18nLanguage} -> ${storeLanguage}`);
229
+ }
230
+ setI18nLanguage(storeLanguage);
231
+ state.previousStoreLanguage = storeLanguage;
232
+ } else {
233
+ if (debug) {
234
+ console.log(`⏭️ [ZUSTAND-I18N] Hydration complete, no sync needed (store: ${storeLanguage}, initial: ${initialLanguage}, current: ${state.currentI18nLanguage})`);
235
+ }
236
+ state.previousStoreLanguage = storeLanguage;
237
+ }
238
+ }
239
+ };
240
+
241
+ // 브라우저가 준비되면 하이드레이션 완료로 간주
242
+ const timeoutId = setTimeout(() => {
243
+ requestAnimationFrame(checkHydration);
244
+ }, 0);
245
+
246
+ return () => clearTimeout(timeoutId);
247
+ }, [isInitialized, setI18nLanguage, initialLanguage, debug]);
248
+
249
+ // 언어 동기화 함수 (재사용)
250
+ const syncLanguageFromStore = React.useCallback(() => {
251
+ const state = hydrationStateRef.current;
252
+ if (!state.isInitialized || !state.isComplete) {
253
+ return;
254
+ }
255
+
256
+ const storeLanguage = store.getState().language;
257
+ if (storeLanguage !== state.currentI18nLanguage && storeLanguage !== initialLanguage) {
258
+ if (debug) {
259
+ console.log(`🔄 [ZUSTAND-I18N] Syncing language from store: ${state.currentI18nLanguage} -> ${storeLanguage}`);
260
+ }
261
+ setI18nLanguage(storeLanguage);
262
+ state.previousStoreLanguage = storeLanguage;
263
+ }
264
+ }, [setI18nLanguage, initialLanguage, debug]);
265
+
266
+ // 언어 변경 구독 설정
267
+ React.useEffect(() => {
268
+ // Translator가 초기화된 후에만 동기화
269
+ if (!isInitialized) {
270
+ return;
271
+ }
272
+
273
+ const state = hydrationStateRef.current;
274
+ state.isInitialized = true;
275
+
276
+ // 초기 스토어 언어 설정
277
+ if (state.previousStoreLanguage === null) {
278
+ state.previousStoreLanguage = store.getState().language;
279
+ }
280
+
281
+ // Zustand 스토어 변경 감지
282
+ const unsubscribe = adapter.subscribe((newLanguage) => {
283
+ // 이전 언어와 다를 때만 처리
284
+ if (newLanguage !== state.previousStoreLanguage) {
285
+ state.previousStoreLanguage = newLanguage;
286
+
287
+ // 하이드레이션 완료 후에만 동기화
288
+ if (state.isComplete) {
289
+ // 현재 i18n 언어와 다를 때만 동기화 (무한 루프 방지)
290
+ if (newLanguage !== state.currentI18nLanguage) {
291
+ if (debug) {
292
+ console.log(`🔄 [ZUSTAND-I18N] Store language changed, syncing to i18n: ${state.currentI18nLanguage} -> ${newLanguage}`);
293
+ }
294
+ setI18nLanguage(newLanguage);
295
+ } else {
296
+ if (debug) {
297
+ console.log(`⏭️ [ZUSTAND-I18N] Store language changed but i18n already synced: ${newLanguage}`);
298
+ }
299
+ }
300
+ } else {
301
+ // 하이드레이션 완료 전에는 로그만 출력
302
+ if (debug) {
303
+ console.log(`⏳ [ZUSTAND-I18N] Store language changed but hydration not complete yet: ${newLanguage}`);
304
+ }
305
+ }
306
+ }
307
+ });
308
+
309
+ // 하이드레이션이 이미 완료되었다면 즉시 동기화
310
+ if (state.isComplete) {
311
+ const storeLanguage = store.getState().language;
312
+ // initialLanguage와 다르고, 현재 i18n 언어와도 다를 때만 동기화
313
+ if (storeLanguage !== initialLanguage && storeLanguage !== state.currentI18nLanguage) {
314
+ if (debug) {
315
+ console.log(`🔄 [ZUSTAND-I18N] Already hydrated, syncing language: ${state.currentI18nLanguage} -> ${storeLanguage}`);
316
+ }
317
+ setI18nLanguage(storeLanguage);
318
+ state.previousStoreLanguage = storeLanguage;
319
+ } else {
320
+ if (debug) {
321
+ console.log(`⏭️ [ZUSTAND-I18N] Already hydrated, no sync needed (store: ${storeLanguage}, initial: ${initialLanguage}, current: ${state.currentI18nLanguage})`);
322
+ }
323
+ }
324
+ }
325
+
326
+ return unsubscribe;
327
+ }, [isInitialized, setI18nLanguage, initialLanguage, debug]);
328
+
329
+ // 하이드레이션 완료 후 언어 동기화를 위한 별도 useEffect
330
+ // hydratedRef는 ref이므로 의존성으로 사용할 수 없음
331
+ // 대신 하이드레이션 완료 시점에 직접 syncLanguageFromStore 호출
332
+
333
+ return React.createElement(React.Fragment, null, innerChildren);
334
+ }
335
+
336
+ // Zustand 스토어 구독을 포함한 래퍼 Provider
337
+ return function ZustandI18nProvider({ children }: { children: React.ReactNode }) {
338
+ return React.createElement(BaseI18nProvider, {
339
+ children: React.createElement(LanguageSyncWrapper, { children })
340
+ });
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Zustand 스토어와 i18n-core를 통합하는 Hook
346
+ *
347
+ * @param store - Zustand 스토어
348
+ * @returns { language, setLanguage, t } - i18n 훅과 동일한 인터페이스
349
+ *
350
+ * @example
351
+ * ```tsx
352
+ * import { useZustandI18n } from '@hua-labs/i18n-core-zustand';
353
+ * import { useAppStore } from './store/useAppStore';
354
+ *
355
+ * function MyComponent() {
356
+ * const { language, setLanguage, t } = useZustandI18n(useAppStore);
357
+ *
358
+ * return (
359
+ * <div>
360
+ * <p>{t('common:welcome')}</p>
361
+ * <button onClick={() => setLanguage('en')}>English</button>
362
+ * </div>
363
+ * );
364
+ * }
365
+ * ```
366
+ */
367
+ /**
368
+ * Zustand 스토어와 i18n-core를 통합하는 Hook
369
+ *
370
+ * @template L - 언어 코드 타입
371
+ * @param store - Zustand 스토어
372
+ * @returns { language, setLanguage } - 언어 상태 및 변경 함수
373
+ */
374
+ export function useZustandI18n<L extends string = SupportedLanguage | string>(
375
+ store: UseBoundStore<StoreApi<ZustandLanguageStore<L>>>
376
+ ) {
377
+ const adapter = React.useMemo(() => createZustandAdapter(store), [store]);
378
+
379
+ // 스토어의 언어 상태 구독
380
+ const language = store((state) => state.language);
381
+
382
+ // 언어 변경 함수
383
+ const setLanguage = React.useCallback(
384
+ (lang: string) => {
385
+ adapter.setLanguage(lang);
386
+ },
387
+ [adapter]
388
+ );
389
+
390
+ return {
391
+ language,
392
+ setLanguage,
393
+ // useTranslation 훅은 별도로 import해서 사용
394
+ // 이 함수는 Zustand 스토어와의 통합만 제공
395
+ };
396
+ }
397
+
398
+ // 타입은 이미 위에서 export되었으므로 중복 export 제거
399
+