@fluojs/i18n 1.0.0-beta.1
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.ko.md +537 -0
- package/README.md +537 -0
- package/dist/adapters.d.ts +180 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +266 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/http.d.ts +120 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +179 -0
- package/dist/icu.d.ts +59 -0
- package/dist/icu.d.ts.map +1 -0
- package/dist/icu.js +142 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/loaders/fs.d.ts +43 -0
- package/dist/loaders/fs.d.ts.map +1 -0
- package/dist/loaders/fs.js +79 -0
- package/dist/loaders/remote.d.ts +146 -0
- package/dist/loaders/remote.d.ts.map +1 -0
- package/dist/loaders/remote.js +268 -0
- package/dist/loaders/shared.d.ts +54 -0
- package/dist/loaders/shared.d.ts.map +1 -0
- package/dist/loaders/shared.js +89 -0
- package/dist/locale-resolution.d.ts +86 -0
- package/dist/locale-resolution.d.ts.map +1 -0
- package/dist/locale-resolution.js +201 -0
- package/dist/module.d.ts +22 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +60 -0
- package/dist/options.d.ts +9 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +169 -0
- package/dist/service.d.ts +104 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +348 -0
- package/dist/typegen.d.ts +60 -0
- package/dist/typegen.d.ts.map +1 -0
- package/dist/typegen.js +215 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +74 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +123 -0
- package/package.json +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fluo contributors
|
|
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.ko.md
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# @fluojs/i18n
|
|
2
|
+
|
|
3
|
+
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
|
+
|
|
5
|
+
fluo 애플리케이션을 위한 프레임워크 비종속 국제화 코어 표면입니다.
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
- [설치](#설치)
|
|
10
|
+
- [사용 시점](#사용-시점)
|
|
11
|
+
- [빠른 시작](#빠른-시작)
|
|
12
|
+
- [코어 번역](#코어-번역)
|
|
13
|
+
- [포맷팅](#포맷팅)
|
|
14
|
+
- [ICU MessageFormat](#icu-messageformat)
|
|
15
|
+
- [HTTP Locale Context Adapter](#http-locale-context-adapter)
|
|
16
|
+
- [Non-HTTP Locale Adapters](#non-http-locale-adapters)
|
|
17
|
+
- [Validation Error Localization](#validation-error-localization)
|
|
18
|
+
- [Node Filesystem Loader](#node-filesystem-loader)
|
|
19
|
+
- [Remote Catalog Loader](#remote-catalog-loader)
|
|
20
|
+
- [Catalog Type Generation](#catalog-type-generation)
|
|
21
|
+
- [공개 API](#공개-api)
|
|
22
|
+
- [Ecosystem Bridge Evaluation](#ecosystem-bridge-evaluation)
|
|
23
|
+
- [Post-MVP 로드맵](#post-mvp-로드맵)
|
|
24
|
+
- [관련 패키지](#관련-패키지)
|
|
25
|
+
- [예제 소스](#예제-소스)
|
|
26
|
+
|
|
27
|
+
## 설치
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @fluojs/i18n
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Root entry point는 `@fluojs/core`에만 의존합니다. Optional subpath는 integration dependency를 optional peer로 유지합니다. `@fluojs/i18n/icu`를 opt-in하면 `intl-messageformat`, `@fluojs/i18n/http`를 opt-in하면 `@fluojs/http`, `@fluojs/i18n/validation`을 opt-in하면 `@fluojs/validation`을 함께 설치하세요. 기존 subpath 사용자는 이 dependency boundary 변경이 포함된 릴리스로 업그레이드하기 전에 해당 peer dependency를 application 또는 package manifest에 추가해야 합니다.
|
|
34
|
+
|
|
35
|
+
## 사용 시점
|
|
36
|
+
|
|
37
|
+
i18n 작업을 위한 안정적인 fluo-native 패키지 경계가 필요할 때 이 패키지를 사용하세요.
|
|
38
|
+
|
|
39
|
+
- `I18nModule.forRoot(...)`를 통한 애플리케이션 수준 모듈 등록.
|
|
40
|
+
- 명시적 로케일 번역 조회를 위한 프레임워크 비종속 `I18nService`.
|
|
41
|
+
- 모듈 없이 사용하는 독립형 `createI18n(...)` 진입점.
|
|
42
|
+
- 로케일 범위 메시지 카탈로그, 결정론적 폴백 처리, 보간법 및 누락된 메시지 훅.
|
|
43
|
+
- `@fluojs/i18n/icu`를 통한 선택적 ICU MessageFormat 복수형/select 포맷팅.
|
|
44
|
+
- 명시적 로케일을 사용하는 표준 `Intl` 포맷팅 헬퍼.
|
|
45
|
+
- `@fluojs/i18n/http`를 통한 명시적 HTTP `RequestContext` 로케일 헬퍼.
|
|
46
|
+
- `@fluojs/i18n/adapters`를 통한 WebSocket, gRPC, CLI, local storage, server-request abstraction용 opt-in non-HTTP locale adapter.
|
|
47
|
+
- `@fluojs/i18n/validation`을 통한 opt-in `@fluojs/validation` issue localization.
|
|
48
|
+
- `@fluojs/i18n/loaders/remote`를 통한 provider-backed remote catalog loading 및 opt-in cache wrapper.
|
|
49
|
+
- `@fluojs/i18n/typegen`을 통한 opt-in catalog key declaration generation 및 typed translation helper declaration.
|
|
50
|
+
- 공유 옵션, 카탈로그, 로케일, 번역 키 및 에러 타입.
|
|
51
|
+
|
|
52
|
+
`@fluojs/i18n`은 의도적으로 NestJS i18n, i18next, next-intl와 결합하지 않습니다. Root entry point는 TC39 `Intl` 기준에 가까운 표준 지향적 대안을 제공하고, ICU MessageFormat 지원은 dedicated `@fluojs/i18n/icu` subpath에 격리됩니다.
|
|
53
|
+
|
|
54
|
+
## 빠른 시작
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { Module } from '@fluojs/core';
|
|
58
|
+
import { I18nModule } from '@fluojs/i18n';
|
|
59
|
+
|
|
60
|
+
@Module({
|
|
61
|
+
imports: [
|
|
62
|
+
I18nModule.forRoot({
|
|
63
|
+
defaultLocale: 'en',
|
|
64
|
+
supportedLocales: ['en', 'ko'],
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
class AppModule {}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 코어 번역
|
|
72
|
+
|
|
73
|
+
`I18nService`는 결정론적인 번역 조회를 제공합니다.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { createI18n } from '@fluojs/i18n';
|
|
77
|
+
|
|
78
|
+
const i18n = createI18n({
|
|
79
|
+
defaultLocale: 'en',
|
|
80
|
+
supportedLocales: ['en', 'ko'],
|
|
81
|
+
fallbackLocales: { ko: ['en'] },
|
|
82
|
+
catalogs: {
|
|
83
|
+
en: { app: { title: 'Hello {{ name }}' } },
|
|
84
|
+
ko: { app: { title: '안녕하세요 {{ name }}' } },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 보간법을 사용한 번역
|
|
89
|
+
const title = i18n.translate('app.title', {
|
|
90
|
+
locale: 'ko',
|
|
91
|
+
values: { name: 'fluo' },
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 폴백 동작
|
|
96
|
+
|
|
97
|
+
번역 조회는 엄격한 순서를 따릅니다.
|
|
98
|
+
|
|
99
|
+
1. 호출 시 명시된 개별 로케일.
|
|
100
|
+
2. 해당 로케일에 구성된 폴백 (`fallbackLocales` 맵 또는 글로벌 폴백 배열 중 하나).
|
|
101
|
+
3. 구성된 `defaultLocale`.
|
|
102
|
+
4. 호출 시 명시된 `defaultValue`.
|
|
103
|
+
5. 구성된 `missingMessage` 훅.
|
|
104
|
+
|
|
105
|
+
메시지를 찾을 수 없으면 `I18N_MISSING_MESSAGE` 코드와 함께 `I18nError`가 발생합니다.
|
|
106
|
+
|
|
107
|
+
## 포맷팅
|
|
108
|
+
|
|
109
|
+
포맷팅 헬퍼는 호스트 환경의 표준 `Intl` 구현에 직접 위임합니다. 로케일은 모든 포맷팅 호출에서 명시적이며, 명명된 포맷터 옵션은 서비스 소유의 불변 스냅샷으로 캡처됩니다.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { createI18n } from '@fluojs/i18n';
|
|
113
|
+
|
|
114
|
+
const i18n = createI18n({
|
|
115
|
+
defaultLocale: 'en-US',
|
|
116
|
+
formats: {
|
|
117
|
+
dateTime: {
|
|
118
|
+
invoice: { dateStyle: 'medium', timeZone: 'UTC' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
i18n.formatDateTime(new Date(), {
|
|
124
|
+
format: 'invoice',
|
|
125
|
+
locale: 'en-US',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
i18n.formatCurrency(12900, {
|
|
129
|
+
currency: 'KRW',
|
|
130
|
+
locale: 'ko-KR',
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## ICU MessageFormat
|
|
135
|
+
|
|
136
|
+
ICU MessageFormat 지원은 `@fluojs/i18n/icu`에 있습니다. 따라서 root `@fluojs/i18n` entry point는 프레임워크 비종속 simple interpolation contract를 유지합니다. ICU service는 먼저 core `I18nService`를 통해 메시지를 resolve하므로 locale fallback, 호출별 `defaultValue`, missing-message hook, 호환되는 primitive 값의 `{{ name }}` interpolation을 보존합니다. 이후 resolve된 메시지를 ICU plural, select, nested MessageFormat 규칙으로 포맷합니다.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { createIcuI18n } from '@fluojs/i18n/icu';
|
|
140
|
+
|
|
141
|
+
const i18n = createIcuI18n({
|
|
142
|
+
defaultLocale: 'en',
|
|
143
|
+
supportedLocales: ['en', 'ko'],
|
|
144
|
+
fallbackLocales: { ko: ['en'] },
|
|
145
|
+
catalogs: {
|
|
146
|
+
en: {
|
|
147
|
+
inbox: 'Hello {{ name }}. {count, plural, =0 {No messages} one {One message} other {# messages}}.',
|
|
148
|
+
invite:
|
|
149
|
+
'{gender, select, female {{host} invited {count, plural, one {one guest} other {# guests}}} other {{host} invited {count, plural, one {one guest} other {# guests}}}}',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
i18n.translate('inbox', {
|
|
155
|
+
locale: 'ko',
|
|
156
|
+
values: { count: 3, name: 'Mina' },
|
|
157
|
+
});
|
|
158
|
+
// "Hello Mina. 3 messages."
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Invalid ICU pattern, 누락된 ICU value, string이 아닌 rich formatting result는 `I18N_INVALID_MESSAGE_FORMAT` 코드의 `I18nError`로 보고됩니다. 이 subpath는 `intl-messageformat`이 사용하는 host `Intl.NumberFormat`, `Intl.DateTimeFormat`, `Intl.PluralRules` 구현에 의존합니다.
|
|
162
|
+
|
|
163
|
+
## HTTP Locale Context Adapter
|
|
164
|
+
|
|
165
|
+
HTTP request locale helper는 `@fluojs/i18n/http` subpath에서만 제공됩니다. 따라서 root `@fluojs/i18n` entry point는 프레임워크 비종속으로 유지되며 `@fluojs/http`를 import하지 않습니다.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { createI18n } from '@fluojs/i18n';
|
|
169
|
+
import { createAcceptLanguageLocaleResolver, getHttpLocale, resolveHttpLocale } from '@fluojs/i18n/http';
|
|
170
|
+
import type { RequestContext } from '@fluojs/http';
|
|
171
|
+
|
|
172
|
+
const i18n = createI18n({
|
|
173
|
+
defaultLocale: 'en',
|
|
174
|
+
supportedLocales: ['en', 'ko'],
|
|
175
|
+
catalogs: {
|
|
176
|
+
en: { app: { title: 'Welcome' } },
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const acceptLanguage = createAcceptLanguageLocaleResolver();
|
|
181
|
+
|
|
182
|
+
async function bindRequestLocale(ctx: RequestContext) {
|
|
183
|
+
return resolveHttpLocale(ctx, {
|
|
184
|
+
defaultLocale: 'en',
|
|
185
|
+
supportedLocales: ['en', 'ko'],
|
|
186
|
+
resolvers: [acceptLanguage],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handler(ctx: RequestContext) {
|
|
191
|
+
const locale = getHttpLocale(ctx)?.locale ?? 'en';
|
|
192
|
+
// resolve된 로케일로 서비스 사용
|
|
193
|
+
return i18n.translate('app.title', { locale, defaultValue: 'Welcome' });
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Adapter는 의도적으로 explicit합니다:
|
|
198
|
+
|
|
199
|
+
- `setHttpLocale(ctx, locale, metadata)`는 `createContextKey(...)`를 사용해 현재 `RequestContext`에 locale metadata를 저장합니다.
|
|
200
|
+
- `getHttpLocale(ctx)`는 global fallback 없이 metadata를 읽습니다.
|
|
201
|
+
- `parseAcceptLanguage(header)`는 q-value 순서로 valid `Accept-Language` range를 parse하고 invalid 또는 q=0 entry를 무시합니다.
|
|
202
|
+
- `createAcceptLanguageLocaleResolver(...)`는 request header에서 첫 번째 supported locale을 선택합니다.
|
|
203
|
+
- `createAcceptLanguageLocalePolicyResolver(...)`는 opt-in이며, `en-US` 같은 regional range를 supported `en`으로 normalize하거나 explicit supported range를 모두 확인한 뒤 wildcard fallback을 선택할 수 있습니다.
|
|
204
|
+
- `resolveHttpLocale(ctx, options)`는 application-provided resolver를 배열 순서대로 실행하고 invalid 또는 unsupported resolver output을 무시하며, 아무 resolver도 match하지 않으면 `defaultLocale`을 source `default`로 저장합니다.
|
|
205
|
+
|
|
206
|
+
Wildcard `*` range는 parse되지만 자동으로 locale을 선택하지는 않습니다. Wildcard별 동작이 필요한 애플리케이션은 제공된 `Accept-Language` resolver 앞이나 뒤에 resolver를 추가할 수 있습니다.
|
|
207
|
+
|
|
208
|
+
예를 들어 이 resolver는 explicit user range를 먼저 유지하고, `*`는 fallback-only로 취급하며, explicit range가 match하지 않을 때만 첫 번째 configured supported locale을 선택합니다.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const acceptLanguagePolicy = createAcceptLanguageLocalePolicyResolver({
|
|
212
|
+
wildcardLocale: 'firstSupportedLocale',
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Non-HTTP Locale Adapters
|
|
217
|
+
|
|
218
|
+
Non-HTTP locale helper는 `@fluojs/i18n/adapters` subpath에서 제공합니다. WebSocket handshake, gRPC metadata, CLI option object, local storage wrapper, server session, request-like abstraction에 resolver-order locale selection을 제공하되 root package를 browser global, Node process state, framework-specific transport package와 결합하지 않습니다.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import {
|
|
222
|
+
bindLocale,
|
|
223
|
+
createHeaderLocaleResolver,
|
|
224
|
+
createQueryLocaleResolver,
|
|
225
|
+
createWeakMapLocaleStore,
|
|
226
|
+
getAdapterLocale,
|
|
227
|
+
} from '@fluojs/i18n/adapters';
|
|
228
|
+
|
|
229
|
+
interface SocketContext {
|
|
230
|
+
readonly handshake: {
|
|
231
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
232
|
+
readonly query: Readonly<Record<string, string | undefined>>;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const socketLocales = createWeakMapLocaleStore<SocketContext>();
|
|
237
|
+
|
|
238
|
+
const queryLocale = createQueryLocaleResolver<SocketContext>({
|
|
239
|
+
getQueryValue: (socket) => socket.handshake.query.locale,
|
|
240
|
+
source: 'socket-query',
|
|
241
|
+
});
|
|
242
|
+
const headerLocale = createHeaderLocaleResolver<SocketContext>({
|
|
243
|
+
getHeader: (socket) => socket.handshake.headers['accept-language'],
|
|
244
|
+
source: 'socket-accept-language',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
function bindSocketLocale(socket: SocketContext) {
|
|
248
|
+
return bindLocale(socket, {
|
|
249
|
+
defaultLocale: 'en',
|
|
250
|
+
supportedLocales: ['en', 'ko'],
|
|
251
|
+
resolvers: [queryLocale, headerLocale],
|
|
252
|
+
store: socketLocales,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function handleSocketMessage(socket: SocketContext) {
|
|
257
|
+
const locale = getAdapterLocale(socketLocales, socket)?.locale ?? 'en';
|
|
258
|
+
return locale;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Generic adapter contract는 의도적으로 explicit합니다.
|
|
263
|
+
|
|
264
|
+
- `resolveLocale(context, options)`는 application-provided resolver를 배열 순서대로 실행하고 empty, invalid, unsupported resolver output을 무시하며, 아무 것도 match하지 않으면 `defaultLocale`을 source `default`로 반환합니다.
|
|
265
|
+
- `bindLocale(context, { store, ...options })`는 locale을 resolve한 뒤 application-provided `LocaleAdapterStore`에 immutable metadata를 저장합니다.
|
|
266
|
+
- `createWeakMapLocaleStore()`는 socket, call, session, request object를 mutate하지 않고 per-object metadata storage를 제공합니다.
|
|
267
|
+
- `createHeaderLocaleResolver(...)`는 HTTP adapter와 같은 q-value 및 wildcard 동작으로 `Accept-Language` style 값을 parse합니다.
|
|
268
|
+
- `createHeaderLocalePolicyResolver(...)`는 HTTP type을 import하지 않고 동일한 opt-in regional-locale normalization 및 wildcard fallback policy를 제공합니다.
|
|
269
|
+
- `createQueryLocaleResolver(...)`, `createCookieLocaleResolver(...)`, `createStorageLocaleResolver(...)`는 caller-owned abstraction에서 locale candidate를 읽고 browser global이나 framework internal에는 접근하지 않습니다.
|
|
270
|
+
|
|
271
|
+
애플리케이션이 context shape와 accessor function을 선택합니다. 예를 들어 gRPC 통합은 `getHeader`로 metadata를 읽고, CLI 통합은 parsed `--locale` option을 `getQueryValue` 또는 `getStoredLocale`로 읽으며, browser application은 `localStorage` around safe wrapper를 `getStoredLocale`에 전달할 수 있습니다.
|
|
272
|
+
|
|
273
|
+
## Validation Error Localization
|
|
274
|
+
|
|
275
|
+
Validation issue localization은 `@fluojs/i18n/validation` subpath에서 제공합니다. 따라서 root `@fluojs/i18n` entry point는 framework-agnostic 상태를 유지하고, `@fluojs/validation` 기본 동작도 바꾸지 않습니다. 애플리케이션은 validation 실패 후 `ValidationIssue.message` snapshot을 명시적으로 번역해 opt-in합니다.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { createI18n } from '@fluojs/i18n';
|
|
279
|
+
import { localizeDtoValidationError } from '@fluojs/i18n/validation';
|
|
280
|
+
import { DefaultValidator, DtoValidationError } from '@fluojs/validation';
|
|
281
|
+
|
|
282
|
+
const i18n = createI18n({
|
|
283
|
+
defaultLocale: 'en',
|
|
284
|
+
supportedLocales: ['en', 'ko'],
|
|
285
|
+
fallbackLocales: { ko: ['en'] },
|
|
286
|
+
catalogs: {
|
|
287
|
+
en: { validation: { email: { EMAIL: '{{ field }} must be a valid email address.' } } },
|
|
288
|
+
ko: { validation: { email: { EMAIL: '{{ field }}에는 올바른 이메일 주소가 필요합니다.' } } },
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
await new DefaultValidator().materialize(input, CreateUserDto);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error instanceof DtoValidationError) {
|
|
296
|
+
throw localizeDtoValidationError(i18n, error, { locale: 'ko' });
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
기본 key candidate 순서는 가장 구체적인 것부터 덜 구체적인 것까지 `source.field.code`, `field.code`, `source.code`, `code`입니다. 기본 namespace는 `validation`이고, 호출자는 catalog 구조에 맞춰 `keyPrefix`, `namespace`, 또는 custom `keyBuilder`를 제공할 수 있습니다. Translation value에는 `code`, `field`, `source`, 원래 `message`가 포함됩니다. 누락된 번역은 기본적으로 원래 validation message를 보존합니다. `fallbackToIssueMessage: false`를 설정하면 `I18N_MISSING_MESSAGE` 코드의 `I18nError`를 throw합니다.
|
|
303
|
+
|
|
304
|
+
이 통합은 의도적으로 HTTP adapter가 아닙니다. Request locale resolution은 `@fluojs/i18n/http`, CLI configuration, WebSocket session state 또는 다른 application boundary에서 처리하고, 선택된 locale을 validation localization helper에 명시적으로 전달합니다.
|
|
305
|
+
|
|
306
|
+
## Node Filesystem Loader
|
|
307
|
+
|
|
308
|
+
Node 애플리케이션은 dedicated subpath에서 JSON filesystem loader를 선택적으로 사용할 수 있습니다.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { createFileSystemI18nLoader } from '@fluojs/i18n/loaders/fs';
|
|
312
|
+
|
|
313
|
+
const loader = createFileSystemI18nLoader({
|
|
314
|
+
rootDir: new URL('./locales', import.meta.url).pathname,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const common = await loader.load('en', 'common');
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Loader는 `${rootDir}/${locale}/${namespace}.json`을 읽고 immutable `I18nMessageTree`를 반환합니다. Namespace는 `admin/common` 같은 safe relative path segment를 사용할 수 있습니다. Locale과 namespace 값은 disk read 전에 validate되며, `.`, `..`, absolute path, empty segment, `common.json` 같은 extension-bearing name, traversal attempt는 `I18N_INVALID_LOADER_OPTIONS`로 거부됩니다. Missing file은 `I18N_MISSING_CATALOG`, malformed JSON 또는 invalid message tree shape는 `I18N_INVALID_CATALOG`를 throw합니다.
|
|
321
|
+
|
|
322
|
+
이 subpath는 Node built-in을 import하며 `@fluojs/i18n` root에서 export하지 않습니다. Bundler가 명시적으로 Node.js를 target하지 않는 한 Bun, Deno, Cloudflare Workers, browser 또는 다른 non-Node runtime-portable bundle에서 import하지 마세요.
|
|
323
|
+
|
|
324
|
+
## Remote Catalog Loader
|
|
325
|
+
|
|
326
|
+
Remote catalog loading은 dedicated provider-backed subpath에서 제공합니다. 애플리케이션은 root entry point에 runtime-specific dependency를 추가하지 않고 HTTP API, object store, database 또는 다른 asynchronous catalog source를 연결할 수 있습니다.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { createRemoteI18nLoader } from '@fluojs/i18n/loaders/remote';
|
|
330
|
+
|
|
331
|
+
const loader = createRemoteI18nLoader({
|
|
332
|
+
timeoutMs: 5_000,
|
|
333
|
+
provider: async ({ locale, namespace, signal }) => {
|
|
334
|
+
const response = await fetch(`https://catalog.example/${locale}/${namespace}.json`, { signal });
|
|
335
|
+
if (response.status === 404) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
return response.text();
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const common = await loader.load('en', 'common');
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Provider는 validated `locale`, `namespace`, 그리고 loader timeout과 optional per-call cancellation을 결합한 `AbortSignal`을 받습니다. Provider는 raw object message tree 또는 JSON string을 반환할 수 있습니다. `undefined`와 `null`은 missing catalog로 취급되어 `I18N_MISSING_CATALOG`를 throw합니다. Malformed JSON과 invalid message tree shape는 `I18N_INVALID_CATALOG`, provider failure는 `I18N_LOADER_FAILED`, timeout은 `I18N_LOADER_TIMEOUT`, caller cancellation은 `I18N_LOADER_ABORTED`로 보고됩니다. 반환된 catalog는 항상 detached immutable `I18nMessageTree` snapshot입니다.
|
|
346
|
+
|
|
347
|
+
Remote loader는 기본적으로 cache하지 않습니다. 모든 `load(locale, namespace)` 호출은 provider를 호출하고 그 provider result를 snapshot합니다. Memory, HTTP, CDN, database 또는 stale-while-revalidate caching이 필요한 애플리케이션은 cache invalidation이 application boundary에서 명시적으로 유지되도록 provider 내부 또는 provider wrapper에서 구현해야 합니다.
|
|
348
|
+
|
|
349
|
+
First-party in-memory policy가 필요한 애플리케이션은 loader를 명시적으로 wrap할 수 있습니다. Cache entry는 caller가 custom key를 제공하지 않는 한 `(locale, namespace, version)`으로 keying되며, `invalidate(...)` / `clear()`가 invalidation을 application-owned 상태로 유지합니다.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { createCachedRemoteI18nLoader, createRemoteI18nLoader } from '@fluojs/i18n/loaders/remote';
|
|
353
|
+
|
|
354
|
+
const uncachedLoader = createRemoteI18nLoader({ provider: fetchCatalog });
|
|
355
|
+
const cachedLoader = createCachedRemoteI18nLoader({
|
|
356
|
+
loader: uncachedLoader,
|
|
357
|
+
ttlMs: 60_000,
|
|
358
|
+
version: 'catalog-2026-05-11',
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
cachedLoader.invalidate('en', 'common');
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Filesystem loader와 마찬가지로 locale과 namespace 값은 provider 호출 전에 validate됩니다. Namespace는 `admin/common` 같은 safe relative path segment를 사용할 수 있습니다. `.`, `..`, absolute path, empty segment, `common.json` 같은 extension-bearing name, traversal attempt는 `I18N_INVALID_LOADER_OPTIONS`로 거부됩니다.
|
|
365
|
+
|
|
366
|
+
## Catalog Type Generation
|
|
367
|
+
|
|
368
|
+
Catalog type generation은 Node-oriented `@fluojs/i18n/typegen` tooling subpath에서 제공합니다. 이 기능은 `I18nService.translate(key: string, ...)`를 좁히지 않습니다. 애플리케이션은 type-safe translation key 변수 또는 typed translation facade가 필요한 위치에서 generated helper type을 선택적으로 사용할 수 있습니다.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { generateI18nCatalogTypesFromDirectory } from '@fluojs/i18n/typegen';
|
|
372
|
+
|
|
373
|
+
const declarations = await generateI18nCatalogTypesFromDirectory({
|
|
374
|
+
rootDir: new URL('./locales', import.meta.url).pathname,
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Directory helper는 `${rootDir}/${locale}/**/*.json`을 scan하고, 각 JSON 파일을 `I18nMessageTree`로 validate한 뒤 deterministic TypeScript declaration text를 생성합니다. Filesystem namespace path는 loader가 받는 형태 그대로 보존됩니다. 예를 들어 `locales/en/admin/common.json`은 namespace `admin/common`을 제공하고, nested leaf는 `admin/common.dashboard.title` 같은 fully qualified key가 됩니다. 이는 namespace를 그대로 prefix하는 `I18nService.translate('dashboard.title', { namespace: 'admin/common', ... })` lookup과 일치합니다.
|
|
379
|
+
|
|
380
|
+
Custom pipeline 또는 remote catalog에는 in-memory message tree에서 생성할 수 있습니다.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { generateI18nCatalogTypes } from '@fluojs/i18n/typegen';
|
|
384
|
+
|
|
385
|
+
const declarations = generateI18nCatalogTypes([
|
|
386
|
+
{
|
|
387
|
+
locale: 'en',
|
|
388
|
+
namespace: 'admin/common',
|
|
389
|
+
messages: {
|
|
390
|
+
dashboard: {
|
|
391
|
+
title: 'Dashboard',
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
]);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Generated declaration text에는 fully qualified key union, namespace union, namespace-to-leaf-key map, opt-in typed facade type이 포함됩니다. 예를 들어 `admin/common.dashboard.title`은 fully qualified key로 사용할 수 있고, 같은 메시지는 `I18nCatalogNamespaceKey<"admin/common">`을 통해 namespace `admin/common`과 leaf key `dashboard.title` 조합으로 표현할 수 있습니다.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import type { I18nCatalogTypedService } from './generated-i18n-catalog.d.ts';
|
|
402
|
+
|
|
403
|
+
const typedI18n = {
|
|
404
|
+
translate: i18n.translate.bind(i18n),
|
|
405
|
+
translateInNamespace: (namespace, key, options) => i18n.translate(key, { ...options, namespace }),
|
|
406
|
+
} satisfies I18nCatalogTypedService;
|
|
407
|
+
|
|
408
|
+
typedI18n.translate('admin/common.dashboard.title', { locale: 'en' });
|
|
409
|
+
typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en' });
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
이 helper declaration은 type-only이며 application-owned입니다. Runtime wrapper를 추가하지 않고, framework bridge를 import하지 않으며, 넓은 runtime `I18nService.translate(key: string, options)` signature도 바꾸지 않습니다.
|
|
413
|
+
|
|
414
|
+
두 helper 모두 locale 간 key를 deduplicate하고 stable diff를 위해 output을 sort하며, invalid catalog shape는 `I18N_INVALID_CATALOG`, unsafe locale 또는 namespace path는 `I18N_INVALID_LOADER_OPTIONS`로 거부합니다.
|
|
415
|
+
|
|
416
|
+
## 공개 API
|
|
417
|
+
|
|
418
|
+
### 코어 (@fluojs/i18n)
|
|
419
|
+
|
|
420
|
+
| Export | 설명 |
|
|
421
|
+
|---|---|
|
|
422
|
+
| `I18nModule` | DI 등록을 위한 모듈입니다. |
|
|
423
|
+
| `I18nService` | 번역 및 포맷팅을 위한 코어 서비스입니다. |
|
|
424
|
+
| `createI18n` | 독립형 서비스를 생성하기 위한 헬퍼입니다. |
|
|
425
|
+
| `I18nError` | 패키지 전용 에러 클래스입니다. |
|
|
426
|
+
|
|
427
|
+
**타입:** `I18nModuleOptions`, `I18nMessageCatalogs`, `I18nMessageTree`, `I18nTranslateOptions`, `I18nInterpolationValues`, `I18nMissingMessageHandler`, `I18nMissingMessageContext`, `I18nLocale`, `I18nTranslationKey`, `I18nErrorCode`, `I18nFallbackLocales`, `I18nFormatOptions`, `I18nFormatterOptions`, `I18nDateTimeFormatOptions`, `I18nNumberFormatOptions`, `I18nCurrencyFormatOptions`, `I18nListFormatOptions`, `I18nRelativeTimeFormatOptions`, `I18nNamedDateTimeFormats`, `I18nNamedNumberFormats`, `I18nNamedListFormats`, `I18nNamedRelativeTimeFormats`.
|
|
428
|
+
|
|
429
|
+
### HTTP 어댑터 (@fluojs/i18n/http)
|
|
430
|
+
|
|
431
|
+
| Export | 설명 |
|
|
432
|
+
|---|---|
|
|
433
|
+
| `resolveHttpLocale` | `RequestContext`에서 로케일 메타데이터를 확인하고 저장합니다. |
|
|
434
|
+
| `getHttpLocale` | `RequestContext`에서 로케일 메타데이터를 가져옵니다. |
|
|
435
|
+
| `setHttpLocale` | `RequestContext`에 로케일 메타데이터를 수동으로 저장합니다. |
|
|
436
|
+
| `createAcceptLanguageLocaleResolver` | `Accept-Language` 헤더에 대한 리졸버를 생성합니다. |
|
|
437
|
+
| `createAcceptLanguageLocalePolicyResolver` | Regional normalization과 wildcard fallback handling을 위한 opt-in `Accept-Language` policy resolver를 생성합니다. |
|
|
438
|
+
| `parseAcceptLanguage` | `Accept-Language` 헤더를 q-value 선호도로 파싱하는 유틸리티입니다. |
|
|
439
|
+
| `HTTP_LOCALE_CONTEXT_KEY` | `RequestContext`에 로케일 메타데이터를 저장할 때 사용하는 컨텍스트 키입니다. |
|
|
440
|
+
|
|
441
|
+
**타입:** `HttpLocaleContext`, `HttpLocaleResolver`, `HttpLocaleResolverInput`, `HttpLocaleResolverResult`, `ResolveHttpLocaleOptions`, `AcceptLanguageLocaleResolverOptions`, `AcceptLanguageLocalePolicyResolverOptions`, `AcceptLanguagePreference`.
|
|
442
|
+
|
|
443
|
+
### Non-HTTP Adapters (@fluojs/i18n/adapters)
|
|
444
|
+
|
|
445
|
+
| Export | 설명 |
|
|
446
|
+
|---|---|
|
|
447
|
+
| `resolveLocale` | 명시적 non-HTTP resolver chain에서 locale metadata를 resolve합니다. |
|
|
448
|
+
| `bindLocale` | Caller-provided adapter store에 locale metadata를 resolve하고 저장합니다. |
|
|
449
|
+
| `setAdapterLocale` | Caller-provided adapter store에 locale metadata를 수동으로 저장합니다. |
|
|
450
|
+
| `getAdapterLocale` | Caller-provided adapter store에서 locale metadata를 가져옵니다. |
|
|
451
|
+
| `createWeakMapLocaleStore` | Transport context를 mutate하지 않는 per-object metadata storage를 생성합니다. |
|
|
452
|
+
| `createHeaderLocaleResolver` | Caller-owned header abstraction용 `Accept-Language` style resolver를 생성합니다. |
|
|
453
|
+
| `createHeaderLocalePolicyResolver` | Regional normalization과 wildcard fallback handling을 위한 opt-in header policy resolver를 생성합니다. |
|
|
454
|
+
| `createQueryLocaleResolver` | Query, CLI option, request parameter abstraction용 resolver를 생성합니다. |
|
|
455
|
+
| `createCookieLocaleResolver` | Caller-owned cookie abstraction용 resolver를 생성합니다. |
|
|
456
|
+
| `createStorageLocaleResolver` | Local storage, server session, socket data, CLI config abstraction용 resolver를 생성합니다. |
|
|
457
|
+
|
|
458
|
+
**타입:** `LocaleAdapterContext`, `LocaleAdapterResolver`, `LocaleAdapterResolverInput`, `LocaleAdapterResolverResult`, `LocaleAdapterStore`, `ResolveLocaleOptions`, `BindLocaleOptions`, `HeaderLocaleResolverOptions`, `HeaderLocalePolicyResolverOptions`, `QueryLocaleResolverOptions`, `CookieLocaleResolverOptions`, `StorageLocaleResolverOptions`.
|
|
459
|
+
|
|
460
|
+
### Validation Integration (@fluojs/i18n/validation)
|
|
461
|
+
|
|
462
|
+
| Export | 설명 |
|
|
463
|
+
|---|---|
|
|
464
|
+
| `createValidationIssueTranslationKeys(issue, keyPrefix?)` | validation issue source, field path, code에서 기본 translation key candidate를 생성합니다. |
|
|
465
|
+
| `localizeValidationIssue(i18n, issue, options, index?)` | candidate key가 resolve되면 localized message가 포함된 validation issue snapshot을 반환합니다. |
|
|
466
|
+
| `localizeValidationIssues(i18n, issues, options)` | 원본 issue를 mutate하지 않고 issue list를 localize합니다. |
|
|
467
|
+
| `localizeDtoValidationError(i18n, error, options)` | localized issue message를 가진 새 `DtoValidationError`를 생성합니다. |
|
|
468
|
+
|
|
469
|
+
**타입:** `LocalizeValidationIssuesOptions`, `ValidationIssueTranslationKeyBuilder`, `ValidationIssueTranslationKeyContext`.
|
|
470
|
+
|
|
471
|
+
### ICU MessageFormat (@fluojs/i18n/icu)
|
|
472
|
+
|
|
473
|
+
| Export | 설명 |
|
|
474
|
+
|---|---|
|
|
475
|
+
| `createIcuI18n(options)` | Core lookup semantics를 보존하면서 standalone ICU MessageFormat service를 생성합니다. |
|
|
476
|
+
| `IcuI18nService` | `I18nService`로 메시지를 resolve한 뒤 ICU formatting을 수행하는 service입니다. |
|
|
477
|
+
|
|
478
|
+
**타입:** `I18nIcuTranslateOptions`, `I18nIcuValue`, `I18nIcuValues`.
|
|
479
|
+
|
|
480
|
+
### 파일시스템 로더 (@fluojs/i18n/loaders/fs)
|
|
481
|
+
|
|
482
|
+
| Export | 설명 |
|
|
483
|
+
|---|---|
|
|
484
|
+
| `createFileSystemI18nLoader` | Node.js JSON 파일시스템 로더를 생성합니다. |
|
|
485
|
+
| `FileSystemI18nLoader` | 파일시스템 로더의 클래스 구현체입니다. |
|
|
486
|
+
|
|
487
|
+
**타입:** `I18nLoader`, `I18nLoaderLoadOptions`, `FileSystemI18nLoaderOptions`.
|
|
488
|
+
|
|
489
|
+
### Remote Loader (@fluojs/i18n/loaders/remote)
|
|
490
|
+
|
|
491
|
+
| Export | 설명 |
|
|
492
|
+
|---|---|
|
|
493
|
+
| `createRemoteI18nLoader` | Provider-backed remote catalog loader를 생성합니다. |
|
|
494
|
+
| `RemoteI18nLoader` | Remote catalog loader의 클래스 구현체입니다. |
|
|
495
|
+
| `createCachedRemoteI18nLoader` | Remote catalog loader 주변에 opt-in in-memory cache wrapper를 생성합니다. |
|
|
496
|
+
| `CachedRemoteI18nLoader` | Explicit `invalidate(...)`와 `clear()` control을 제공하는 cache wrapper 구현체입니다. |
|
|
497
|
+
|
|
498
|
+
**타입:** `I18nLoader`, `I18nLoaderLoadOptions`, `RemoteI18nCatalogProvider`, `RemoteI18nCatalogRequest`, `RemoteI18nLoaderOptions`, `CachedI18nLoader`, `CachedI18nLoaderKeyInput`, `CachedI18nLoaderOptions`.
|
|
499
|
+
|
|
500
|
+
### Catalog Type Generation (@fluojs/i18n/typegen)
|
|
501
|
+
|
|
502
|
+
| Export | 설명 |
|
|
503
|
+
|---|---|
|
|
504
|
+
| `generateI18nCatalogTypes(inputs, options?)` | In-memory catalog tree에서 deterministic TypeScript key declaration을 생성합니다. |
|
|
505
|
+
| `generateI18nCatalogTypesFromDirectory(options)` | Disk의 locale/namespace JSON catalog를 읽고 key declaration을 생성합니다. |
|
|
506
|
+
|
|
507
|
+
**타입:** `I18nCatalogTypegenInput`, `I18nCatalogTypegenOptions`, `I18nCatalogTypegenDirectoryOptions`. Generated declaration 기본값에는 `I18nCatalogKey`, `I18nCatalogNamespace`, `I18nCatalogKeyByNamespace`, `I18nCatalogNamespaceKey`, `I18nCatalogTypedTranslateOptions`, `I18nCatalogTypedTranslate`, `I18nCatalogTypedService`가 포함됩니다.
|
|
508
|
+
|
|
509
|
+
## Ecosystem Bridge Evaluation
|
|
510
|
+
|
|
511
|
+
현재 bridge decision은 documentation-first입니다. NestJS i18n parity, i18next interop, next-intl catalog sharing, request-locale/validation convenience glue는 runtime helper를 추가하기 전에 migration guidance와 기존 opt-in subpath로 처리해야 합니다. 향후 bridge helper가 first-party subpath가 되기 위해 필요한 classification matrix와 acceptance criteria는 [i18n ecosystem bridge decision record](../../docs/reference/i18n-ecosystem-bridges.ko.md)를 참조하세요.
|
|
512
|
+
|
|
513
|
+
이 결정은 `@fluojs/i18n` root package가 NestJS i18n, i18next, next-intl, React/Next.js runtime assumption, HTTP-only validation localization과 결합하지 않는다는 보장을 유지합니다.
|
|
514
|
+
|
|
515
|
+
## Post-MVP 로드맵
|
|
516
|
+
|
|
517
|
+
WebSocket, gRPC, CLI, local storage, request-style abstraction을 위한 core locale-resolution roadmap item은 이제 `@fluojs/i18n/adapters`에서 사용할 수 있습니다. 향후 transport 작업은 dedicated framework package가 통합을 소유하지 않는 한 opt-in 및 subpath-scoped 상태를 유지해야 합니다.
|
|
518
|
+
|
|
519
|
+
## 관련 패키지
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
- **`@fluojs/core`**: 이 패키지가 사용하는 module metadata와 shared framework error를 제공합니다.
|
|
523
|
+
- **`@fluojs/config`**: module registration 및 option snapshotting convention에 가장 가까운 package layout model입니다.
|
|
524
|
+
- **`@fluojs/validation`**: `@fluojs/i18n/validation`이 소비하는 opt-in validation issue contract를 제공합니다.
|
|
525
|
+
|
|
526
|
+
## 예제 소스
|
|
527
|
+
|
|
528
|
+
- `packages/i18n/src/module.ts`
|
|
529
|
+
- `packages/i18n/src/service.ts`
|
|
530
|
+
- `packages/i18n/src/icu.ts`
|
|
531
|
+
- `packages/i18n/src/loaders/fs.ts`
|
|
532
|
+
- `packages/i18n/src/http.ts`
|
|
533
|
+
- `packages/i18n/src/adapters.ts`
|
|
534
|
+
- `packages/i18n/src/validation.ts`
|
|
535
|
+
- `packages/i18n/src/index.test.ts`
|
|
536
|
+
- `packages/i18n/src/loaders/remote.ts`
|
|
537
|
+
- `packages/i18n/src/typegen.ts`
|