@orderly.network/i18n 2.12.0 → 2.12.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/README.md +27 -314
- package/bin/cli.js +92 -37
- package/dist/{constant-D_rlt5w0.d.mts → constant-DkvDyddr.d.mts} +12 -40
- package/dist/{constant-D_rlt5w0.d.ts → constant-DkvDyddr.d.ts} +12 -40
- package/dist/constant.d.mts +1 -1
- package/dist/constant.d.ts +1 -1
- package/dist/constant.js.map +1 -1
- package/dist/constant.mjs.map +1 -1
- package/dist/index.d.mts +84 -20
- package/dist/index.d.ts +84 -20
- package/dist/index.js +138 -107
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +129 -106
- package/dist/index.mjs.map +1 -1
- package/dist/locale.csv +7 -105
- package/dist/locales/de.json +7 -37
- package/dist/locales/en.json +7 -37
- package/dist/locales/es.json +7 -37
- package/dist/locales/fr.json +7 -37
- package/dist/locales/id.json +7 -37
- package/dist/locales/it.json +7 -37
- package/dist/locales/ja.json +7 -37
- package/dist/locales/ko.json +7 -37
- package/dist/locales/nl.json +7 -37
- package/dist/locales/pl.json +7 -37
- package/dist/locales/pt.json +7 -37
- package/dist/locales/ru.json +7 -37
- package/dist/locales/tc.json +7 -37
- package/dist/locales/tr.json +7 -37
- package/dist/locales/uk.json +7 -37
- package/dist/locales/vi.json +7 -37
- package/dist/locales/zh.json +7 -37
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +25 -50
- package/dist/utils.js.map +1 -1
- package/dist/utils.mjs +25 -50
- package/dist/utils.mjs.map +1 -1
- package/docs/guide/AGENTS.md +109 -0
- package/docs/guide/cli.md +133 -0
- package/docs/guide/examples.md +455 -0
- package/docs/guide/exports.md +14 -0
- package/docs/guide/integration.md +223 -0
- package/docs/guide/utils.md +14 -0
- package/package.json +13 -11
- package/{script → scripts}/copyLocales.js +1 -1
- package/scripts/filterLocaleKeys.js +127 -0
- package/{script → scripts}/generateCsv.js +3 -3
- package/{script → scripts}/utils.js +20 -14
- /package/{script → scripts}/csv2json.js +0 -0
- /package/{script → scripts}/diffCsv.js +0 -0
- /package/{script → scripts}/fillJson.js +0 -0
- /package/{script → scripts}/generateEnJson.js +0 -0
- /package/{script → scripts}/generateMissingKeys.js +0 -0
- /package/{script → scripts}/json-csv-converter.js +0 -0
- /package/{script → scripts}/json2csv.js +0 -0
- /package/{script → scripts}/mergeJson.js +0 -0
- /package/{script → scripts}/separateJson.js +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Integration Guide
|
|
2
|
+
|
|
3
|
+
**This guide** documents props, effects, and loading strategies. For **end-to-end copy-paste recipes** (Vite `import.meta.glob`, Next.js/webpack, HTTP `public/`, sync maps, URL sync), use [Examples](./examples.md).
|
|
4
|
+
|
|
5
|
+
**Overview:** `LocaleProvider` composes `**LanguageProvider`** (language list, optional HTTP `Backend`, change callbacks) and `**I18nextProvider`** from react-i18next. All of it uses the package’s **singleton `i18n`instance**. The default namespace is`**translation`** (`defaultNS`); see [Package exports](./exports.md).
|
|
6
|
+
|
|
7
|
+
Follow the steps below to integrate localization in your app with the Orderly SDK.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [1. Wrap Your App with LocaleProvider](#1-wrap-your-app-with-localeprovider)
|
|
12
|
+
- [2. Provide Locale Data](#2-provide-locale-data)
|
|
13
|
+
- [3. Extending Locale Files](#3-extending-locale-files)
|
|
14
|
+
- [4. Integrate External Resources](#4-integrate-external-resources)
|
|
15
|
+
|
|
16
|
+
## 1. Wrap Your App with LocaleProvider
|
|
17
|
+
|
|
18
|
+
`LocaleProvider` is the main entry: it wires locale state and the i18n React context. Wrap your app (or orderly subtree) at the root.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { LocaleProvider } from "@orderly.network/i18n";
|
|
22
|
+
|
|
23
|
+
export function App() {
|
|
24
|
+
return (
|
|
25
|
+
<LocaleProvider>
|
|
26
|
+
<YourApp />
|
|
27
|
+
</LocaleProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Locale-only props
|
|
33
|
+
|
|
34
|
+
These props are defined on `LocaleProvider` (see `localeProvider.tsx`):
|
|
35
|
+
|
|
36
|
+
| Prop | Description |
|
|
37
|
+
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `locale` | Optional **controlled** locale. When set and different from `i18n.language`, a `useEffect` calls `i18n.changeLanguage(locale)`. |
|
|
39
|
+
| `resource` | Flat messages for `**defaultNS`** (`translation`). Used only when `**resources`is not set**; requires`locale`. Calls `i18n.addResourceBundle(locale, defaultNS, resource, true, true)`. |
|
|
40
|
+
| `resources` | Static **Resources** map or **AsyncResources**. When set, **registerResources** runs in a `useEffect` (see **Behavior**). Takes precedence over `locale` + `resource`. Same contract as `ExternalLocaleProvider` / `useRegisterExternalResources`. |
|
|
41
|
+
|
|
42
|
+
**Async loader and `ns`:** The `AsyncResources` type is `(lang, ns) => Promise<Record<string, string>>`. When loading goes through `**registerResources`** (from `LocaleProvider` or `useRegisterExternalResources`), the implementation calls `**await resources(localeCode, defaultNS)`** — the second argument is **always** `defaultNS` (`translation`), not an arbitrary namespace. Use the parameter if you build URLs; for multiple i18n namespaces, use the i18n API directly.
|
|
43
|
+
|
|
44
|
+
### Inherited from `LanguageProvider`
|
|
45
|
+
|
|
46
|
+
Pass these through `LocaleProvider` like any other `LanguageProvider` prop:
|
|
47
|
+
|
|
48
|
+
| Prop | Description |
|
|
49
|
+
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `backend` | `BackendOptions`: `{ loadPath }` where `loadPath(lang, ns)` returns a URL `string`, `string[]`, or `undefined` for the HTTP `Backend`. Those URLs must resolve to real files (e.g. under `public/`); the package does **not** copy `dist/locales` into your app — sync manually or via a script / hook — see [HTTP backend](./examples.md#http-backend). |
|
|
51
|
+
| `languages` | Full `Language[]` for the switcher; when set as an array, replaces `defaultLanguages`. |
|
|
52
|
+
| `supportedLanguages` | Subset of `LocaleCode[]`; builds the list from `**defaultLanguages**` entries. |
|
|
53
|
+
| `onLanguageBeforeChanged` | `(lang) => Promise<void>`. **Runs first**; then the internal `Backend` loads the next language (`loadLanguage(lang, defaultNS)`). Use for prep work before HTTP loads. |
|
|
54
|
+
| `onLanguageChanged` | `(lang) => Promise<void>` — notification on the language-change path. |
|
|
55
|
+
| `convertDetectedLanguage` | `(browserLang: string) => LocaleCode` — optional mapping from the detector to your supported codes. |
|
|
56
|
+
| `popup` | `PopupProps` for the language switcher: optional `mode` (`modal`, `dropdown`, or `sheet`), `className`, `style`. |
|
|
57
|
+
|
|
58
|
+
### Behavior (effects)
|
|
59
|
+
|
|
60
|
+
- `**resources` set:\*\* `registerResources(resources, locale ?? currentLocale)` runs when `locale`, `resource`, `resources`, or the current locale from `useLocaleCode` changes. Static maps register every locale entry; async loaders fetch for the active locale code.
|
|
61
|
+
- `**resources` unset** and `**resource`+`locale`:** merges the flat bundle for that locale into `defaultNS`.
|
|
62
|
+
- `**locale` prop:\*\* separate effect — if `locale` is set and differs from `i18n.language`, `i18n.changeLanguage(locale)` runs.
|
|
63
|
+
|
|
64
|
+
Prefer **one** primary loading approach per app (**HTTP `backend`** vs **static/async `resources`** vs `**locale` + `resource**`) to avoid overlapping bundles. You can pass `**resources` on `LocaleProvider**` instead of `ExternalLocaleProvider` — same registration path. Use `**useRegisterExternalResources**` to avoid an extra wrapper (stable `resources` reference recommended).
|
|
65
|
+
|
|
66
|
+
### Loading strategies (quick reference)
|
|
67
|
+
|
|
68
|
+
- **HTTP:** `backend={{ loadPath }}` — load JSON from URLs (e.g. files under `public/`). You must **place** those JSON files on disk (or CDN): **manually** copy from `node_modules/.../i18n/dist/locales`, or run a **copy script** / **Husky** hook (`npm run copyLocales`, etc.) as in [HTTP backend](./examples.md#http-backend).
|
|
69
|
+
- **Bundled:** `resources` as a static map or **`AsyncResources`**. Recipes: [Async resources (Vite)](./examples.md#async-resources-vite) · [Async resources (Next.js and webpack)](./examples.md#async-resources-nextjs-and-webpack) · [Sync resources](./examples.md#sync-resources).
|
|
70
|
+
- **Controlled single bundle:** `locale` + `resource` when you inject one flat table for one language.
|
|
71
|
+
- **Host / external bundles:** `ExternalLocaleProvider` or `useRegisterExternalResources` — same `Resources` / `AsyncResources` as `LocaleProvider.resources`.
|
|
72
|
+
|
|
73
|
+
## 2. Provide Locale Data
|
|
74
|
+
|
|
75
|
+
### Default language
|
|
76
|
+
|
|
77
|
+
- English (`en`) ships with the package as the built-in base bundle.
|
|
78
|
+
|
|
79
|
+
### Supported locales
|
|
80
|
+
|
|
81
|
+
We currently support **17** locales. The table order matches `**defaultLanguages`\*\* in the package (`constant`).
|
|
82
|
+
|
|
83
|
+
| Locale Code | Language |
|
|
84
|
+
| ----------- | ------------------- |
|
|
85
|
+
| `en` | English |
|
|
86
|
+
| `zh` | Chinese |
|
|
87
|
+
| `ja` | Japanese |
|
|
88
|
+
| `es` | Spanish |
|
|
89
|
+
| `ko` | Korean |
|
|
90
|
+
| `vi` | Vietnamese |
|
|
91
|
+
| `de` | German |
|
|
92
|
+
| `fr` | French |
|
|
93
|
+
| `ru` | Russian |
|
|
94
|
+
| `id` | Indonesian |
|
|
95
|
+
| `tr` | Turkish |
|
|
96
|
+
| `it` | Italian |
|
|
97
|
+
| `pt` | Portuguese |
|
|
98
|
+
| `uk` | Ukrainian |
|
|
99
|
+
| `pl` | Polish |
|
|
100
|
+
| `nl` | Dutch |
|
|
101
|
+
| `tc` | Traditional Chinese |
|
|
102
|
+
|
|
103
|
+
### CSV for translation workflows
|
|
104
|
+
|
|
105
|
+
- Releases include a `dist/locale.csv` you can hand off for translation.
|
|
106
|
+
- Use the [CLI](./cli.md) to convert between CSV and JSON (`csv2json`, `json2csv`, etc.).
|
|
107
|
+
|
|
108
|
+
## 3. Extending locale files
|
|
109
|
+
|
|
110
|
+
You can translate SDK strings and add strings for your own UI.
|
|
111
|
+
|
|
112
|
+
- Use the `**extend.`\*\* key prefix for custom keys so they stay distinct from built-in keys (and align with tooling such as `separateJson` in the [CLI](./cli.md)).
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"extend.custom.button.label": "My Custom Button"
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 4. Integrate external resources
|
|
121
|
+
|
|
122
|
+
Use this when strings live outside this package (another bundle, CDN, or host app). `**LocaleProvider**` with `**resources**`, `**ExternalLocaleProvider**`, and `**useRegisterExternalResources**` all call the same `**registerResources**` helper.
|
|
123
|
+
|
|
124
|
+
The snippets below are **minimal** (wrapper vs hook). [Examples](./examples.md) uses **`LocaleProvider` + `resources`** with full app wiring (e.g. merge SDK + extend, provider tree)—use that for production-shaped code.
|
|
125
|
+
|
|
126
|
+
For **Vite**, bundling SDK locales with your `extend` JSON via `**AsyncResources`\*\* is the recommended setup — see [Async resources (Vite)](./examples.md#async-resources-vite).
|
|
127
|
+
|
|
128
|
+
### `ExternalLocaleProvider`
|
|
129
|
+
|
|
130
|
+
- **Async:** `(lang, ns) => Promise<Record<string, string>>` — invoked when the locale changes (same `ns` behavior as above when used through `registerResources`).
|
|
131
|
+
- **Sync:** static `Resources` map; all listed locales are registered on mount.
|
|
132
|
+
|
|
133
|
+
Async example:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import {
|
|
137
|
+
AsyncResources,
|
|
138
|
+
ExternalLocaleProvider,
|
|
139
|
+
LocaleCode,
|
|
140
|
+
LocaleProvider,
|
|
141
|
+
} from "@orderly.network/i18n";
|
|
142
|
+
|
|
143
|
+
const resources: AsyncResources = async (lang: LocaleCode) => {
|
|
144
|
+
return import(`./locales/${lang}.json`).then(
|
|
145
|
+
(res) => res.default as Record<string, string>,
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export function App() {
|
|
150
|
+
return (
|
|
151
|
+
<LocaleProvider>
|
|
152
|
+
<ExternalLocaleProvider resources={resources}>
|
|
153
|
+
<YourApp />
|
|
154
|
+
</ExternalLocaleProvider>
|
|
155
|
+
</LocaleProvider>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Sync example:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import {
|
|
164
|
+
ExternalLocaleProvider,
|
|
165
|
+
LocaleProvider,
|
|
166
|
+
Resources,
|
|
167
|
+
} from "@orderly.network/i18n";
|
|
168
|
+
|
|
169
|
+
const resources: Resources = {
|
|
170
|
+
en: { "extend.host.title": "Host app title" },
|
|
171
|
+
zh: { "extend.host.title": "宿主应用标题" },
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export function App() {
|
|
175
|
+
return (
|
|
176
|
+
<LocaleProvider>
|
|
177
|
+
<ExternalLocaleProvider resources={resources}>
|
|
178
|
+
<YourApp />
|
|
179
|
+
</ExternalLocaleProvider>
|
|
180
|
+
</LocaleProvider>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`ExternalLocaleProvider` renders only its children (no UI).
|
|
186
|
+
|
|
187
|
+
### `useRegisterExternalResources`
|
|
188
|
+
|
|
189
|
+
Same registration as above, without a wrapper component — call under `LocaleProvider` with a **stable** `resources` reference (`useCallback` for loaders, module scope or `useMemo` for maps):
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import {
|
|
193
|
+
LocaleProvider,
|
|
194
|
+
useRegisterExternalResources,
|
|
195
|
+
} from "@orderly.network/i18n";
|
|
196
|
+
import type { AsyncResources, LocaleCode } from "@orderly.network/i18n";
|
|
197
|
+
|
|
198
|
+
const loader: AsyncResources = async (lang: LocaleCode) => {
|
|
199
|
+
return import(`./locales/${lang}.json`).then(
|
|
200
|
+
(res) => res.default as Record<string, string>,
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
function Bridge() {
|
|
205
|
+
useRegisterExternalResources(loader);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `registerDefaultResource`
|
|
211
|
+
|
|
212
|
+
Call **before** the React tree mounts (e.g. app bootstrap) to merge keys into the **English** bundle and reduce flicker of raw keys. It uses `i18n.addResourceBundle(defaultLng, defaultNS, messages, true, true)` (deep merge with overwrite), same as other bundle registration paths:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { registerDefaultResource } from "@orderly.network/i18n";
|
|
216
|
+
|
|
217
|
+
registerDefaultResource({
|
|
218
|
+
"extend.app.loading": "Loading...",
|
|
219
|
+
"extend.app.title": "Orderly App",
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
See also: [Package exports](./exports.md) · [Utils](./utils.md) · [CLI](./cli.md) · [Examples](./examples.md) ([Vite](./examples.md#async-resources-vite) · [Next/webpack](./examples.md#async-resources-nextjs-and-webpack) · [HTTP](./examples.md#http-backend) · [URL sync](./examples.md#onlanguagechanged-url-sync))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Utils (integrator-facing)
|
|
2
|
+
|
|
3
|
+
The `@orderly.network/i18n/utils` entry exports the following helpers:
|
|
4
|
+
|
|
5
|
+
| Function | Purpose |
|
|
6
|
+
| --------------------------- | ------------------------------------------------------------------------------------ |
|
|
7
|
+
| `parseI18nLang` | Map browser language strings to a `LocaleCode` (from `LocaleEnum` or a custom list). |
|
|
8
|
+
| `removeLangPrefix` | Remove a leading locale segment from a pathname. |
|
|
9
|
+
| `getLocalePathFromPathname` | Read the locale segment from a pathname when present. |
|
|
10
|
+
| `generatePath` | Build a locale-prefixed path with optional search string. |
|
|
11
|
+
|
|
12
|
+
For generated API notes on `utils.ts`, see [`../utils.md`](../utils.md) in this package’s docs.
|
|
13
|
+
|
|
14
|
+
See also: [Integration guide](./integration.md) · [Examples](./examples.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orderly.network/i18n",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.1",
|
|
4
4
|
"description": "Internationalization for orderly sdk",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -20,16 +20,20 @@
|
|
|
20
20
|
},
|
|
21
21
|
"keywords": [],
|
|
22
22
|
"files": [
|
|
23
|
+
"bin",
|
|
23
24
|
"dist",
|
|
24
|
-
"
|
|
25
|
+
"scripts",
|
|
26
|
+
"docs/guide"
|
|
25
27
|
],
|
|
26
28
|
"publishConfig": {
|
|
27
29
|
"access": "public"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
32
|
+
"fs-extra": "^11.2.0",
|
|
30
33
|
"i18next": "^24.2.2",
|
|
31
34
|
"i18next-browser-languagedetector": "^8.0.4",
|
|
32
|
-
"react-i18next": "^15.4.1"
|
|
35
|
+
"react-i18next": "^15.4.1",
|
|
36
|
+
"yargs": "^17.7.2"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
35
39
|
"@babel/preset-env": "^7.22.9",
|
|
@@ -39,7 +43,6 @@
|
|
|
39
43
|
"@types/react-dom": "^18.3.0",
|
|
40
44
|
"@types/fs-extra": "^11.0.4",
|
|
41
45
|
"@types/jest": "^29.5.3",
|
|
42
|
-
"fs-extra": "^11.2.0",
|
|
43
46
|
"babel-jest": "^29.6.1",
|
|
44
47
|
"jest": "^29.6.1",
|
|
45
48
|
"jest-environment-jsdom": "^29.7.0",
|
|
@@ -47,8 +50,7 @@
|
|
|
47
50
|
"react-dom": "^18.2.0",
|
|
48
51
|
"tsup": "^8.5.1",
|
|
49
52
|
"typescript": "^5.1.6",
|
|
50
|
-
"
|
|
51
|
-
"tsconfig": "0.15.0"
|
|
53
|
+
"tsconfig": "0.15.1"
|
|
52
54
|
},
|
|
53
55
|
"peerDependencies": {
|
|
54
56
|
"react": ">=18",
|
|
@@ -61,15 +63,15 @@
|
|
|
61
63
|
"cli": "node ./bin/cli.js",
|
|
62
64
|
"csv2json": "pnpm cli csv2json ./dist/locale.csv ./dist/locales",
|
|
63
65
|
"json2csv": "pnpm cli json2csv ./dist/locales ./dist/locale.csv",
|
|
64
|
-
"generateCsv": "pnpm cli generateCsv ./dist/locale.csv",
|
|
66
|
+
"generateCsv": "pnpm cli generateCsv ./locales ./dist/locale.csv",
|
|
65
67
|
"diffcsv": "pnpm cli diffcsv ./dist/locale1.csv ./dist/locale2.csv",
|
|
66
68
|
"fillJson": "pnpm cli fillJson ./src/locale/zh.json ./dist/locale/zh.json",
|
|
67
69
|
"separateJson": "pnpm cli separateJson ./locales ./dist/locales extend",
|
|
68
70
|
"mergeJson": "pnpm cli mergeJson ./locales ./dist/locales",
|
|
69
71
|
"mergeExtendJson": "pnpm cli mergeJson ./locales ./locales",
|
|
70
|
-
"_generateEnJson": "node ./
|
|
71
|
-
"generateEnJson": "pnpm tsup && node ./
|
|
72
|
-
"copyLocales": "node ./
|
|
73
|
-
"generateMissingKeys": "pnpm tsup && pnpm _generateEnJson && node ./
|
|
72
|
+
"_generateEnJson": "node ./scripts/generateEnJson.js",
|
|
73
|
+
"generateEnJson": "pnpm tsup && node ./scripts/generateEnJson.js",
|
|
74
|
+
"copyLocales": "node ./scripts/copyLocales.js",
|
|
75
|
+
"generateMissingKeys": "pnpm tsup && pnpm _generateEnJson && node ./scripts/generateMissingKeys.js"
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const USAGE =
|
|
5
|
+
"Usage: node filterLocaleKeys.js (--keep|-k | --remove|-r) <prefix> [--locales-dir <dir>]";
|
|
6
|
+
|
|
7
|
+
const KEEP_MODES = ["--keep", "-k"];
|
|
8
|
+
const REMOVE_MODES = ["--remove", "-r"];
|
|
9
|
+
const VALID_MODES = [...KEEP_MODES, ...REMOVE_MODES];
|
|
10
|
+
|
|
11
|
+
const DEFAULT_LOCALES_DIR = path.join(__dirname, "..", "locales");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Filter locale JSON files by key prefix: keep or remove keys matching the prefix.
|
|
15
|
+
* @param {"keep"|"remove"} mode - "keep": only keep keys with prefix; "remove": remove keys with prefix
|
|
16
|
+
* @param {string} prefix - Key prefix to match (e.g. "trading." or "trading")
|
|
17
|
+
* @param {string} [localesDir] - Directory containing locale JSON files (default: packages/i18n/locales)
|
|
18
|
+
*/
|
|
19
|
+
function filterLocaleKeys(mode, prefix, localesDir = DEFAULT_LOCALES_DIR) {
|
|
20
|
+
if (!fs.existsSync(localesDir)) {
|
|
21
|
+
console.error(`Locales directory not found: ${localesDir}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const files = fs
|
|
26
|
+
.readdirSync(localesDir)
|
|
27
|
+
.filter((name) => name.endsWith(".json"))
|
|
28
|
+
.sort();
|
|
29
|
+
|
|
30
|
+
if (files.length === 0) {
|
|
31
|
+
console.warn("No locale JSON files found.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const filePath = path.join(localesDir, file);
|
|
37
|
+
let content;
|
|
38
|
+
try {
|
|
39
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`Failed to read file: ${filePath}`, err.message);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let data;
|
|
46
|
+
try {
|
|
47
|
+
data = JSON.parse(content);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`Failed to parse JSON: ${filePath}`, err.message);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Match key if it starts with prefix, or equals prefix without trailing dot
|
|
54
|
+
// (e.g. prefix "trading." should match key "trading" as well as "trading.xxx")
|
|
55
|
+
const prefixBase = prefix.endsWith(".") ? prefix.slice(0, -1) : prefix;
|
|
56
|
+
const matchesPrefix = (key) => key.startsWith(prefix) || key === prefixBase;
|
|
57
|
+
|
|
58
|
+
const filtered = {};
|
|
59
|
+
for (const [key, value] of Object.entries(data)) {
|
|
60
|
+
if (mode === "keep" ? matchesPrefix(key) : !matchesPrefix(key)) {
|
|
61
|
+
filtered[key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const filteredCount = Object.keys(filtered).length;
|
|
66
|
+
if (mode === "keep" && filteredCount === 0) {
|
|
67
|
+
console.warn(
|
|
68
|
+
`Skip ${file} (no keys starting with "${prefix}" were found).`,
|
|
69
|
+
);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (mode === "remove" && filteredCount === Object.keys(data).length) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`Skip ${file} (no keys starting with "${prefix}" were found to remove).`,
|
|
75
|
+
);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
filePath,
|
|
82
|
+
JSON.stringify(filtered, null, 2) + "\n",
|
|
83
|
+
"utf8",
|
|
84
|
+
);
|
|
85
|
+
if (mode === "keep") {
|
|
86
|
+
console.log(
|
|
87
|
+
`Filtered ${file}: kept ${filteredCount} keys with prefix "${prefix}".`,
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
const removedCount = Object.keys(data).length - filteredCount;
|
|
91
|
+
console.log(
|
|
92
|
+
`Filtered ${file}: removed ${removedCount} keys with prefix "${prefix}", kept ${filteredCount} keys.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(`Failed to write file: ${filePath}`, err.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Run as CLI when executed directly
|
|
102
|
+
if (require.main === module) {
|
|
103
|
+
const modeArg = process.argv[2];
|
|
104
|
+
const prefix = process.argv[3];
|
|
105
|
+
const localesDirIdx = process.argv.indexOf("--locales-dir");
|
|
106
|
+
const localesDir =
|
|
107
|
+
localesDirIdx >= 0 ? process.argv[localesDirIdx + 1] : DEFAULT_LOCALES_DIR;
|
|
108
|
+
|
|
109
|
+
if (!VALID_MODES.includes(modeArg)) {
|
|
110
|
+
console.error(
|
|
111
|
+
"Error: Please specify a mode: --keep|-k (keep keys with prefix) or --remove|-r (remove keys with prefix).",
|
|
112
|
+
);
|
|
113
|
+
console.error(USAGE);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!prefix) {
|
|
118
|
+
console.error("Error: <prefix> is required.");
|
|
119
|
+
console.error(USAGE);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mode = REMOVE_MODES.includes(modeArg) ? "remove" : "keep";
|
|
124
|
+
filterLocaleKeys(mode, prefix, localesDir);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { filterLocaleKeys };
|
|
@@ -5,17 +5,17 @@ const { multiJson2Csv } = require("./json-csv-converter");
|
|
|
5
5
|
const { defaultLanguages } = require("../dist");
|
|
6
6
|
|
|
7
7
|
/** Generate a locale CSV file */
|
|
8
|
-
async function generateCsv(outputPath) {
|
|
8
|
+
async function generateCsv(inputDir, outputPath) {
|
|
9
9
|
const headers = [""];
|
|
10
10
|
const jsonList = [];
|
|
11
11
|
|
|
12
12
|
for (const item of defaultLanguages) {
|
|
13
13
|
headers.push(item.localCode);
|
|
14
14
|
const json = await fs.readJSON(
|
|
15
|
-
path.resolve(
|
|
15
|
+
path.resolve(inputDir, `${item.localCode}.json`),
|
|
16
16
|
{
|
|
17
17
|
encoding: "utf8",
|
|
18
|
-
}
|
|
18
|
+
},
|
|
19
19
|
);
|
|
20
20
|
jsonList.push(json);
|
|
21
21
|
}
|
|
@@ -32,30 +32,37 @@ function validateI18nValue(value) {
|
|
|
32
32
|
// 2. check if placeholder format is correct (allow `{{variable}}`)
|
|
33
33
|
// allow `{{variable}}` or `{{ variable }}`
|
|
34
34
|
const interpolationRegex = /{{\s*[\w.-]+\s*}}/g;
|
|
35
|
-
|
|
36
|
-
const invalidInterpolationRegex =
|
|
37
|
-
/{{[^{}]*$|^[^{}]*}}|[^{]{{|}}[^}]}|^{|}$|{[^{}]*}|}/;
|
|
38
|
-
|
|
35
|
+
const strippedInterpolation = value.replace(interpolationRegex, "");
|
|
39
36
|
if (
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
strippedInterpolation.includes("{{") ||
|
|
38
|
+
strippedInterpolation.includes("}}")
|
|
42
39
|
) {
|
|
43
40
|
return { valid: false, error: i18nValidErrors.interpolation };
|
|
44
41
|
}
|
|
42
|
+
// mistaken single-brace placeholders like `{user.name}` (should be `{{user.name}}`)
|
|
43
|
+
if (/\{\s*[\w.-]+\s*\}/.test(strippedInterpolation)) {
|
|
44
|
+
return { valid: false, error: i18nValidErrors.interpolation };
|
|
45
|
+
}
|
|
46
|
+
// `{` without a closing `}` on the same line segment (typo vs `{{`)
|
|
47
|
+
if (/\{[^}]*$/.test(strippedInterpolation)) {
|
|
48
|
+
return { valid: false, error: i18nValidErrors.interpolation };
|
|
49
|
+
}
|
|
45
50
|
|
|
46
|
-
// 3. check if HTML tags are correctly closed
|
|
47
|
-
const tagRegex =
|
|
51
|
+
// 3. check if HTML tags are correctly closed (supports attributes, e.g. <script async src="...">)
|
|
52
|
+
const tagRegex = /<(\/?)\s*([a-zA-Z0-9][\w-]*)[^>]*>/g;
|
|
48
53
|
let stack = [];
|
|
49
54
|
let match;
|
|
50
55
|
|
|
51
56
|
while ((match = tagRegex.exec(value)) !== null) {
|
|
52
|
-
|
|
57
|
+
const [, slashPrefix, tagName] = match;
|
|
58
|
+
const fullTag = match[0];
|
|
59
|
+
const isClosing = slashPrefix === "/";
|
|
60
|
+
const isSelfClosing = !isClosing && /\/\s*>$/.test(fullTag.trim());
|
|
53
61
|
|
|
54
|
-
if (
|
|
55
|
-
// self-closing tag, no need to push to stack
|
|
62
|
+
if (isSelfClosing) {
|
|
56
63
|
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
64
|
+
}
|
|
65
|
+
if (isClosing) {
|
|
59
66
|
if (stack.length === 0 || stack.pop() !== tagName) {
|
|
60
67
|
return {
|
|
61
68
|
valid: false,
|
|
@@ -63,7 +70,6 @@ function validateI18nValue(value) {
|
|
|
63
70
|
};
|
|
64
71
|
}
|
|
65
72
|
} else {
|
|
66
|
-
// opening tag, push to stack
|
|
67
73
|
stack.push(tagName);
|
|
68
74
|
}
|
|
69
75
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|