@orderly.network/i18n 3.0.0-beta.1 → 3.0.0-beta.3

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.
Files changed (57) hide show
  1. package/README.md +23 -469
  2. package/dist/{constant-BeXwHrGj.d.mts → constant-DkvDyddr.d.mts} +37 -40
  3. package/dist/{constant-BeXwHrGj.d.ts → constant-DkvDyddr.d.ts} +37 -40
  4. package/dist/constant.d.mts +1 -1
  5. package/dist/constant.d.ts +1 -1
  6. package/dist/constant.js.map +1 -1
  7. package/dist/constant.mjs.map +1 -1
  8. package/dist/index.d.mts +69 -30
  9. package/dist/index.d.ts +69 -30
  10. package/dist/index.js +140 -118
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +132 -115
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/locale.csv +33 -106
  15. package/dist/locales/de.json +33 -38
  16. package/dist/locales/en.json +33 -38
  17. package/dist/locales/es.json +33 -38
  18. package/dist/locales/fr.json +33 -38
  19. package/dist/locales/id.json +33 -38
  20. package/dist/locales/it.json +33 -38
  21. package/dist/locales/ja.json +33 -38
  22. package/dist/locales/ko.json +33 -38
  23. package/dist/locales/nl.json +33 -38
  24. package/dist/locales/pl.json +33 -38
  25. package/dist/locales/pt.json +33 -38
  26. package/dist/locales/ru.json +33 -38
  27. package/dist/locales/tc.json +33 -38
  28. package/dist/locales/tr.json +33 -38
  29. package/dist/locales/uk.json +33 -38
  30. package/dist/locales/vi.json +33 -38
  31. package/dist/locales/zh.json +33 -38
  32. package/dist/utils.d.mts +1 -1
  33. package/dist/utils.d.ts +1 -1
  34. package/dist/utils.js +40 -45
  35. package/dist/utils.js.map +1 -1
  36. package/dist/utils.mjs +40 -45
  37. package/dist/utils.mjs.map +1 -1
  38. package/docs/guide/AGENTS.md +109 -0
  39. package/docs/guide/cli.md +133 -0
  40. package/docs/guide/examples.md +455 -0
  41. package/docs/guide/exports.md +14 -0
  42. package/docs/guide/integration.md +223 -0
  43. package/docs/guide/utils.md +14 -0
  44. package/package.json +8 -6
  45. package/scripts/copyLocales.js +11 -0
  46. package/scripts/csv2json.js +28 -0
  47. package/scripts/diffCsv.js +175 -0
  48. package/scripts/fillJson.js +33 -0
  49. package/scripts/filterLocaleKeys.js +127 -0
  50. package/scripts/generateCsv.js +36 -0
  51. package/scripts/generateEnJson.js +11 -0
  52. package/scripts/generateMissingKeys.js +49 -0
  53. package/scripts/json-csv-converter.js +286 -0
  54. package/scripts/json2csv.js +38 -0
  55. package/scripts/mergeJson.js +67 -0
  56. package/scripts/separateJson.js +50 -0
  57. package/scripts/utils.js +94 -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": "3.0.0-beta.1",
3
+ "version": "3.0.0-beta.3",
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
- "script"
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
- "yargs": "^17.7.2",
51
- "tsconfig": "1.0.0-beta.1"
53
+ "tsconfig": "1.0.0-beta.3"
52
54
  },
53
55
  "peerDependencies": {
54
56
  "react": ">=18",
@@ -0,0 +1,11 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ async function copyLocales() {
5
+ await fs.copy(
6
+ path.resolve(__dirname, "../locales"),
7
+ path.resolve(__dirname, "../dist/locales"),
8
+ );
9
+ }
10
+
11
+ copyLocales();
@@ -0,0 +1,28 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { csv2multiJson } = require("./json-csv-converter");
4
+ const { checkFileExists } = require("./utils");
5
+
6
+ /** Convert locale CSV to multiple locale JSON files */
7
+ async function csv2json(inputPath, outputDir) {
8
+ const csv = fs.readFileSync(inputPath, { encoding: "utf8" });
9
+
10
+ const json = csv2multiJson(csv);
11
+
12
+ const files = [];
13
+
14
+ for (const key of Object.keys(json)) {
15
+ const filePath = path.resolve(outputDir, `${key}.json`);
16
+ await checkFileExists(filePath);
17
+ await fs.outputFile(filePath, JSON.stringify(json[key], undefined, 4), {
18
+ encoding: "utf8",
19
+ });
20
+ files.push(filePath);
21
+ }
22
+
23
+ console.log("csv2json success =>", files);
24
+ }
25
+
26
+ module.exports = {
27
+ csv2json,
28
+ };
@@ -0,0 +1,175 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { csv2multiJson } = require("./json-csv-converter");
4
+ const packageJson = require("../package.json");
5
+
6
+ /**
7
+ * https://www.jsdelivr.com/package/npm/@orderly.network/i18n?tab=files&path=dist
8
+ * Compare two locale CSV files
9
+ */
10
+ async function diffCsv(oldFile, newFile) {
11
+ const oldCsv = await fs.readFile(oldFile, { encoding: "utf8" });
12
+ const newCsv = await fs.readFile(newFile, { encoding: "utf8" });
13
+
14
+ const oldJson = csv2multiJson(oldCsv);
15
+ const newJson = csv2multiJson(newCsv);
16
+ const diffResult = compareJsonFiles(oldJson, newJson);
17
+ console.log("CSV diff result:", JSON.stringify(diffResult, null, 2));
18
+
19
+ const filepath = path.resolve("LOCALE_CHANGELOG.md");
20
+
21
+ // generate .md file
22
+ let markdownContent = generateMarkdown(diffResult);
23
+
24
+ if (!(await fs.exists(filepath))) {
25
+ const title = `# Locale Changelog`;
26
+ markdownContent = `${title}\n\n${markdownContent}`;
27
+
28
+ await fs.writeFile(filepath, markdownContent, {
29
+ encoding: "utf8",
30
+ });
31
+ console.log("LOCALE_CHANGELOG.md created");
32
+ } else {
33
+ // Read existing content
34
+ const existingContent = await fs.readFile(filepath, { encoding: "utf8" });
35
+
36
+ // Find the position after "# Locale Changelog"
37
+ const titleIndex = existingContent.indexOf("# Locale Changelog");
38
+ if (titleIndex === -1) {
39
+ console.error("Could not find '# Locale Changelog' title in the file");
40
+ return;
41
+ }
42
+
43
+ const titleEndIndex = titleIndex + "# Locale Changelog".length;
44
+
45
+ // Split content and insert new content after the title
46
+ const beforeTitle = existingContent.slice(0, titleEndIndex);
47
+ const afterTitle = existingContent.slice(titleEndIndex);
48
+
49
+ // Combine all parts
50
+ const newContent = `${beforeTitle}\n\n${markdownContent}${afterTitle}`;
51
+
52
+ // Write back to file
53
+ await fs.writeFile(filepath, newContent, {
54
+ encoding: "utf8",
55
+ });
56
+ console.log("LOCALE_CHANGELOG.md updated");
57
+ }
58
+ }
59
+
60
+ // Compare function
61
+ function compareJsonFiles(oldJson, newJson) {
62
+ const result = {
63
+ added: {},
64
+ removed: {},
65
+ updated: {},
66
+ };
67
+
68
+ Object.keys(newJson).forEach((lang) => {
69
+ result.added[lang] = {};
70
+ result.removed[lang] = {};
71
+ result.updated[lang] = {};
72
+
73
+ const oldKeys = oldJson[lang] || {};
74
+ const newKeys = newJson[lang];
75
+
76
+ // Find added keys
77
+ Object.keys(newKeys).forEach((key) => {
78
+ if (!(key in oldKeys)) {
79
+ result.added[lang][key] = newKeys[key];
80
+ }
81
+ });
82
+
83
+ // Find removed keys
84
+ Object.keys(oldKeys).forEach((key) => {
85
+ if (!(key in newKeys)) {
86
+ result.removed[lang][key] = oldKeys[key];
87
+ }
88
+ });
89
+
90
+ // Find updated keys (same key, different value)
91
+ Object.keys(newKeys).forEach((key) => {
92
+ if (key in oldKeys && oldKeys[key] !== newKeys[key]) {
93
+ result.updated[lang][key] = {
94
+ old: oldKeys[key],
95
+ new: newKeys[key],
96
+ };
97
+ }
98
+ });
99
+ });
100
+
101
+ return result;
102
+ }
103
+
104
+ // generate Markdown conent
105
+ function generateMarkdown(diff) {
106
+ let mdContent = `## ${packageJson.version}\n\n`;
107
+
108
+ const addedKeysEmpty = Object.keys(diff.added).every((lang) => {
109
+ return Object.keys(diff.added[lang]).length === 0;
110
+ });
111
+ const removedKeysEmpty = Object.keys(diff.removed).every((lang) => {
112
+ return Object.keys(diff.removed[lang]).length === 0;
113
+ });
114
+ const updatedKeysEmpty = Object.keys(diff.updated).every((lang) => {
115
+ return Object.keys(diff.updated[lang]).length === 0;
116
+ });
117
+
118
+ if (addedKeysEmpty && removedKeysEmpty && updatedKeysEmpty) {
119
+ return `${mdContent}### No locale changes`;
120
+ }
121
+
122
+ // handle added content
123
+ if (!addedKeysEmpty) {
124
+ mdContent += `### Added Keys\n`;
125
+ Object.keys(diff.added).forEach((lang) => {
126
+ if (Object.keys(diff.added[lang]).length === 0) {
127
+ mdContent += `\n#### Language: **${lang}**\n> No added keys.\n`;
128
+ } else {
129
+ mdContent += `\n#### Language: **${lang}**\n`;
130
+ mdContent += `| Key | Value |\n| --- | --- |\n`;
131
+ Object.entries(diff.added[lang]).forEach(([key, value]) => {
132
+ mdContent += `| ${key} | ${value} |\n`;
133
+ });
134
+ }
135
+ });
136
+ }
137
+
138
+ // handle removed content
139
+ if (!removedKeysEmpty) {
140
+ mdContent += `\n### Removed Keys\n`;
141
+ Object.keys(diff.removed).forEach((lang) => {
142
+ if (Object.keys(diff.removed[lang]).length === 0) {
143
+ mdContent += `\n#### Language: **${lang}**\n> No removed keys.\n`;
144
+ } else {
145
+ mdContent += `\n#### Language: **${lang}**\n`;
146
+ mdContent += `| Key | Value |\n| --- | --- |\n`;
147
+ Object.entries(diff.removed[lang]).forEach(([key, value]) => {
148
+ mdContent += `| ${key} | ${value} |\n`;
149
+ });
150
+ }
151
+ });
152
+ }
153
+
154
+ // handle updated content
155
+ if (!updatedKeysEmpty) {
156
+ mdContent += `\n### Updated Keys\n`;
157
+ Object.keys(diff.updated).forEach((lang) => {
158
+ if (Object.keys(diff.updated[lang]).length === 0) {
159
+ mdContent += `\n#### Language: **${lang}**\n> No updates found.\n`;
160
+ } else {
161
+ mdContent += `\n#### Language: **${lang}**\n`;
162
+ mdContent += `| Key | Old Value | New Value |\n| --- | --- | --- |\n`;
163
+ Object.entries(diff.updated[lang]).forEach(([key, values]) => {
164
+ mdContent += `| ${key} | ${values.old} | ${values.new} |\n`;
165
+ });
166
+ }
167
+ });
168
+ }
169
+
170
+ return mdContent;
171
+ }
172
+
173
+ module.exports = {
174
+ diffCsv,
175
+ };
@@ -0,0 +1,33 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { checkFileExists } = require("./utils");
4
+ const { en } = require("../dist");
5
+
6
+ /** Fill values from the input locale JSON file and generate a new locale JSON file */
7
+ async function fillJson(inputPath, outputPath) {
8
+ const inputJson = await fs.readJSON(inputPath, { encoding: "utf8" });
9
+
10
+ const newJson = {};
11
+ const missingValues = {};
12
+ Object.keys(en).forEach((key) => {
13
+ const value = inputJson[key] || "";
14
+ if (!value) {
15
+ missingValues[key] = en[key];
16
+ }
17
+ newJson[key] = value;
18
+ });
19
+ console.log("missingValues", missingValues);
20
+
21
+ const jsonPath = path.resolve(outputPath);
22
+ await checkFileExists(jsonPath);
23
+
24
+ await fs.outputFile(jsonPath, JSON.stringify(newJson, null, 4), {
25
+ encoding: "utf8",
26
+ });
27
+
28
+ console.log("fillJson success =>", jsonPath);
29
+ }
30
+
31
+ module.exports = {
32
+ fillJson,
33
+ };
@@ -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 };