@unisphere/nx 4.14.4 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ # Languages
2
+
3
+ This folder contains the internationalization (i18n) setup for this package.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ languages/
9
+ ├── en-US.json # Default English translations (source of truth)
10
+ ├── supported-languges.ts # List of supported language codes
11
+ ├── files/ # Translated language files (e.g., de-DE.json, fr-FR.json)
12
+ │ └── .gitkeep
13
+ ├── _db/ # Translation database for change tracking (git-ignored)
14
+ │ └── .gitignore
15
+ └── README.md # This file
16
+ ```
17
+
18
+ ## How It Works
19
+
20
+ - **`en-US.json`** — The master translation file. All keys are defined here in English.
21
+ - **`supported-languges.ts`** — Exports the list of supported language codes. Add new languages here when translations are available.
22
+ - **`files/`** — Contains translated JSON files for each supported language (excluding en-US). These are generated via the Unisphere CLI export/import workflow.
23
+ - **`_db/`** — Used internally by the CLI to track translation changes between exports. Do not commit.
24
+
25
+ ## Adding Translations
26
+
27
+ 1. Add your translation keys to `en-US.json`
28
+ 2. Run `npx unisphere languages export` to generate CSV files for translators
29
+ 3. After receiving translations, run `npx unisphere languages import` to update the `files/` directory
30
+ 4. Add the new language code to `supported-languges.ts`
31
+
32
+ ## Usage
33
+
34
+ Components in this package use translations via the `useTranslation` hook:
35
+
36
+ ```typescript
37
+ import { useTranslation } from './use-translation';
38
+
39
+ const MyComponent = () => {
40
+ const { t } = useTranslation();
41
+ return <span>{t('myKey')}</span>;
42
+ };
43
+ ```
44
+
45
+ ## CLI Commands
46
+
47
+ ```bash
48
+ npx unisphere languages export # Export translations to CSV for translation services
49
+ npx unisphere languages import # Import translated CSV files back to JSON
50
+ npx unisphere languages serve # Serve language files during local development
51
+ npx unisphere languages bundle # Bundle language files for distribution
52
+ ```
@@ -0,0 +1 @@
1
+ export const supportedLanguages: string[] = ['en-US'];
@@ -0,0 +1,9 @@
1
+ import en from '../languages/en-US.json';
2
+ import { createUseTranslation } from '@unisphere/ui-i18n-react';
3
+ import { supportedLanguages } from '../languages/supported-languges';
4
+
5
+ export const useTranslation = createUseTranslation({
6
+ packageName: '<%= packageJsonName %>',
7
+ defaultTranslations: en as any,
8
+ supportedLanguages,
9
+ });
@@ -0,0 +1,32 @@
1
+ # Languages
2
+
3
+ This folder contains the internationalization (i18n) setup for this runtime.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ languages/
9
+ ├── en-US.json # Default English translations (source of truth)
10
+ ├── supported-languges.ts # List of supported language codes
11
+ ├── files/ # Translated language files (e.g., de-DE.json, fr-FR.json)
12
+ │ └── .gitkeep
13
+ ├── _db/ # Translation database for change tracking (git-ignored)
14
+ │ └── .gitignore
15
+ └── README.md # This file
16
+ ```
17
+
18
+ ## How It Works
19
+
20
+ - **`en-US.json`** — The master translation file. All keys are defined here in English.
21
+ - **`supported-languges.ts`** — Exports the list of supported language codes. Add new languages here when translations are available.
22
+ - **`files/`** — Contains translated JSON files for each supported language (excluding en-US). These are generated via the Unisphere CLI export/import workflow.
23
+ - **`_db/`** — Used internally by the CLI to track translation changes between exports. Do not commit.
24
+
25
+ ## CLI Commands
26
+
27
+ ```bash
28
+ npx unisphere languages export # Export translations to CSV for translation services
29
+ npx unisphere languages import # Import translated CSV files back to JSON
30
+ npx unisphere languages serve # Serve language files during local development
31
+ npx unisphere languages bundle # Bundle language files for distribution
32
+ ```
@@ -0,0 +1 @@
1
+ export const supportedLanguages: string[] = ['en-US'];
@@ -12,6 +12,7 @@ import {
12
12
  widgetName,
13
13
  } from '<%= typesAlias %>';
14
14
  import { HtmlDomRuntimeVisual, KalturaAnalyticsServiceType } from '@unisphere/runtime';
15
+ import { UnisphereI18NRuntimeProxy } from '@unisphere/ui-i18n-react';
15
16
 
16
17
  export class Runtime
17
18
  extends UnisphereRuntimeBase<<%= runtimeName__pascalCase %>RuntimeSettings, Root>
@@ -0,0 +1,9 @@
1
+ import en from '../languages/en-US.json';
2
+ import { createUseTranslation } from '@unisphere/ui-i18n-react';
3
+ import { supportedLanguages } from '../languages/supported-languges';
4
+
5
+ export const useTranslation = createUseTranslation({
6
+ packageName: 'self',
7
+ defaultTranslations: en as any,
8
+ supportedLanguages,
9
+ });
@@ -10,6 +10,7 @@
10
10
 
11
11
  visual.htmlContainer?.render(
12
12
  <ScopedUnisphereWorkspaceProvider unisphereWorkspace={this._workspace} runtimeLogger={this._logger}>
13
+ <UnisphereI18NRuntimeProxy runtimeUri={this._options.runtimeDeployedPath} />
13
14
  <ThemeProvider
14
15
  cssPrefix={'<%= widgetName__lowerDashCase %>-<%= runtimeName__lowerDashCase %>-<%= visualName__lowerDashCase %>'}
15
16
  mode={typeof this._theme === 'string' ? this._theme : this._theme.mode}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Migration: Add Unisphere Languages Support
3
+ *
4
+ * Migrates packages from raw i18next/react-i18next to the Unisphere
5
+ * @unisphere/ui-i18n-react framework with createUseTranslation pattern.
6
+ * Adds UnisphereI18NRuntimeProxy to all runtimes.
7
+ *
8
+ * Steps:
9
+ * 1. For packages with existing en-US.json: move translations, create
10
+ * use-translation.tsx, update imports, remove LanguagesProvider
11
+ * 2. For all runtimes: add UnisphereI18NRuntimeProxy, remove LanguagesProvider usage
12
+ * 3. Update root package.json dependencies
13
+ */
14
+ import { Tree } from '@nx/devkit';
15
+ export default function update(tree: Tree): Promise<void>;
16
+ //# sourceMappingURL=add-languages-support.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add-languages-support.d.ts","sourceRoot":"","sources":["../../../src/migrations/4-15-0/add-languages-support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,IAAI,EAML,MAAM,YAAY,CAAC;AAiBpB,wBAA8B,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB9D"}
@@ -0,0 +1,629 @@
1
+ "use strict";
2
+ /**
3
+ * Migration: Add Unisphere Languages Support
4
+ *
5
+ * Migrates packages from raw i18next/react-i18next to the Unisphere
6
+ * @unisphere/ui-i18n-react framework with createUseTranslation pattern.
7
+ * Adds UnisphereI18NRuntimeProxy to all runtimes.
8
+ *
9
+ * Steps:
10
+ * 1. For packages with existing en-US.json: move translations, create
11
+ * use-translation.tsx, update imports, remove LanguagesProvider
12
+ * 2. For all runtimes: add UnisphereI18NRuntimeProxy, remove LanguagesProvider usage
13
+ * 3. Update root package.json dependencies
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.default = update;
17
+ const tslib_1 = require("tslib");
18
+ const devkit_1 = require("@nx/devkit");
19
+ const path = tslib_1.__importStar(require("path"));
20
+ async function update(tree) {
21
+ devkit_1.logger.info('🔄 Adding Unisphere languages support...');
22
+ if (!tree.exists('.unisphere')) {
23
+ devkit_1.logger.info('ℹ️ No .unisphere config found, skipping');
24
+ return;
25
+ }
26
+ const config = (0, devkit_1.readJson)(tree, '.unisphere');
27
+ const packages = config.elements?.packages || {};
28
+ const runtimes = config.elements?.runtimes || {};
29
+ // Step 1: Migrate packages with existing translations
30
+ const migratedPackages = migratePackages(tree, packages);
31
+ // Step 2: Migrate all runtimes
32
+ migrateRuntimes(tree, runtimes, migratedPackages);
33
+ // Step 3: Update root package.json
34
+ updateRootPackageJson(tree);
35
+ await (0, devkit_1.formatFiles)(tree);
36
+ devkit_1.logger.info('✅ Unisphere languages support migration complete');
37
+ }
38
+ // =============================================================================
39
+ // Step 1: Package Migrations
40
+ // =============================================================================
41
+ function migratePackages(tree, packages) {
42
+ const migratedPackages = [];
43
+ for (const [name, config] of Object.entries(packages)) {
44
+ const { sourceRoot } = config;
45
+ if (!tree.exists(sourceRoot)) {
46
+ devkit_1.logger.warn(` ⚠️ Package "${name}" source root not found: ${sourceRoot}`);
47
+ continue;
48
+ }
49
+ // Find existing en-US.json under this package
50
+ const translationFile = findTranslationFile(tree, sourceRoot);
51
+ // Read package name from package.json
52
+ const packageJsonPath = `${sourceRoot}/package.json`;
53
+ let packageName = `@local/${name}`;
54
+ if (tree.exists(packageJsonPath)) {
55
+ const pkgJson = (0, devkit_1.readJson)(tree, packageJsonPath);
56
+ packageName = pkgJson.name || packageName;
57
+ }
58
+ devkit_1.logger.info(` 📦 Migrating package "${name}" (${packageName})...`);
59
+ // 1a. Move translation file to src/languages/en-US.json or create empty one
60
+ const targetTranslationPath = `${sourceRoot}/src/languages/en-US.json`;
61
+ if (translationFile) {
62
+ moveTranslationFile(tree, translationFile, targetTranslationPath);
63
+ }
64
+ else {
65
+ // Create empty en-US.json if none existed
66
+ if (!tree.exists(targetTranslationPath)) {
67
+ tree.write(targetTranslationPath, '{}');
68
+ devkit_1.logger.info(` ✅ Created empty ${targetTranslationPath}`);
69
+ }
70
+ }
71
+ // 1b. Create _db/.gitignore
72
+ const dbGitignorePath = `${sourceRoot}/src/languages/_db/.gitignore`;
73
+ if (!tree.exists(dbGitignorePath)) {
74
+ tree.write(dbGitignorePath, '');
75
+ }
76
+ // 1c. Create files/.gitkeep
77
+ const filesGitkeepPath = `${sourceRoot}/src/languages/files/.gitkeep`;
78
+ if (!tree.exists(filesGitkeepPath)) {
79
+ tree.write(filesGitkeepPath, '');
80
+ }
81
+ // 1d. Create supported-languges.ts
82
+ const supportedLanguagesPath = `${sourceRoot}/src/languages/supported-languges.ts`;
83
+ createSupportedLanguagesFile(tree, supportedLanguagesPath);
84
+ // 1e. Create use-translation.tsx (if not already present)
85
+ const useTranslationPath = `${sourceRoot}/src/lib/use-translation.tsx`;
86
+ createUseTranslationFile(tree, useTranslationPath, packageName);
87
+ // 1f. Create README.md in languages folder
88
+ const readmePath = `${sourceRoot}/src/languages/README.md`;
89
+ createLanguagesReadme(tree, readmePath);
90
+ // 1g. Update useTranslation imports in package files
91
+ updateUseTranslationImports(tree, sourceRoot, useTranslationPath);
92
+ // 1h. Remove LanguagesProvider usage from package files
93
+ removeLanguagesProviderUsage(tree, sourceRoot);
94
+ // 1i. Delete languages-provider.tsx
95
+ deleteLanguagesProvider(tree, sourceRoot);
96
+ // 1j. Update barrel export
97
+ updateBarrelExport(tree, sourceRoot);
98
+ // 1k. Export useTranslation from barrel for runtime consumption
99
+ exportUseTranslationFromBarrel(tree, sourceRoot);
100
+ migratedPackages.push({
101
+ name,
102
+ sourceRoot,
103
+ packageName,
104
+ translationFilePath: targetTranslationPath,
105
+ });
106
+ }
107
+ if (migratedPackages.length === 0) {
108
+ devkit_1.logger.info('ℹ️ No packages with translations found');
109
+ }
110
+ else {
111
+ devkit_1.logger.info(` ✅ Migrated ${migratedPackages.length} package(s) with translations`);
112
+ }
113
+ return migratedPackages;
114
+ }
115
+ function findTranslationFile(tree, sourceRoot) {
116
+ let found = null;
117
+ (0, devkit_1.visitNotIgnoredFiles)(tree, sourceRoot, (filePath) => {
118
+ if (found)
119
+ return;
120
+ if (filePath.endsWith('/en-US.json') && filePath.includes('/languages/')) {
121
+ found = filePath;
122
+ }
123
+ });
124
+ // Fallback: look for en-US.json anywhere under src
125
+ if (!found) {
126
+ (0, devkit_1.visitNotIgnoredFiles)(tree, sourceRoot, (filePath) => {
127
+ if (found)
128
+ return;
129
+ if (filePath.endsWith('/en-US.json')) {
130
+ found = filePath;
131
+ }
132
+ });
133
+ }
134
+ return found;
135
+ }
136
+ function moveTranslationFile(tree, from, to) {
137
+ if (from === to)
138
+ return;
139
+ if (tree.exists(to)) {
140
+ devkit_1.logger.info(` ℹ️ Translation file already at target: ${to}`);
141
+ return;
142
+ }
143
+ const content = tree.read(from, 'utf-8');
144
+ if (content) {
145
+ tree.write(to, content);
146
+ tree.delete(from);
147
+ devkit_1.logger.info(` 📁 Moved ${from} → ${to}`);
148
+ }
149
+ }
150
+ function createSupportedLanguagesFile(tree, filePath) {
151
+ if (tree.exists(filePath)) {
152
+ devkit_1.logger.info(` ℹ️ supported-languges.ts already exists`);
153
+ return;
154
+ }
155
+ const content = `export const supportedLanguages: string[] = ['en-US'];\n`;
156
+ tree.write(filePath, content);
157
+ devkit_1.logger.info(` ✅ Created ${filePath}`);
158
+ }
159
+ function createUseTranslationFile(tree, filePath, packageName) {
160
+ if (tree.exists(filePath)) {
161
+ devkit_1.logger.info(` ℹ️ use-translation.tsx already exists`);
162
+ return;
163
+ }
164
+ const content = `import en from '../languages/en-US.json';
165
+ import { createUseTranslation } from '@unisphere/ui-i18n-react';
166
+ import { supportedLanguages } from '../languages/supported-languges';
167
+
168
+ export const useTranslation = createUseTranslation({
169
+ packageName: '${packageName}',
170
+ defaultTranslations: en as any,
171
+ supportedLanguages,
172
+ });
173
+ `;
174
+ tree.write(filePath, content);
175
+ devkit_1.logger.info(` ✅ Created ${filePath}`);
176
+ }
177
+ function createLanguagesReadme(tree, filePath) {
178
+ if (tree.exists(filePath)) {
179
+ return;
180
+ }
181
+ const content = `# Languages
182
+
183
+ This folder contains the internationalization (i18n) setup for this package.
184
+
185
+ ## Structure
186
+
187
+ \`\`\`
188
+ languages/
189
+ ├── en-US.json # Default English translations (source of truth)
190
+ ├── supported-languges.ts # List of supported language codes
191
+ ├── files/ # Translated language files (e.g., de-DE.json, fr-FR.json)
192
+ │ └── .gitkeep
193
+ ├── _db/ # Translation database for change tracking (git-ignored)
194
+ │ └── .gitignore
195
+ └── README.md # This file
196
+ \`\`\`
197
+
198
+ ## How It Works
199
+
200
+ - **\`en-US.json\`** — The master translation file. All keys are defined here in English.
201
+ - **\`supported-languges.ts\`** — Exports the list of supported language codes. Add new languages here when translations are available.
202
+ - **\`files/\`** — Contains translated JSON files for each supported language (excluding en-US). These are generated via the Unisphere CLI export/import workflow.
203
+ - **\`_db/\`** — Used internally by the CLI to track translation changes between exports. Do not commit.
204
+
205
+ ## Adding Translations
206
+
207
+ 1. Add your translation keys to \`en-US.json\`
208
+ 2. Run \`npx unisphere languages export\` to generate CSV files for translators
209
+ 3. After receiving translations, run \`npx unisphere languages import\` to update the \`files/\` directory
210
+ 4. Add the new language code to \`supported-languges.ts\`
211
+
212
+ ## Usage
213
+
214
+ Components in this package use translations via the \`useTranslation\` hook:
215
+
216
+ \`\`\`typescript
217
+ import { useTranslation } from './use-translation';
218
+
219
+ const MyComponent = () => {
220
+ const { t } = useTranslation();
221
+ return <span>{t('myKey')}</span>;
222
+ };
223
+ \`\`\`
224
+
225
+ ## CLI Commands
226
+
227
+ \`\`\`bash
228
+ npx unisphere languages export # Export translations to CSV for translation services
229
+ npx unisphere languages import # Import translated CSV files back to JSON
230
+ npx unisphere languages serve # Serve language files during local development
231
+ npx unisphere languages bundle # Bundle language files for distribution
232
+ \`\`\`
233
+ `;
234
+ tree.write(filePath, content);
235
+ devkit_1.logger.info(` ✅ Created ${filePath}`);
236
+ }
237
+ function updateUseTranslationImports(tree, sourceRoot, useTranslationPath) {
238
+ let updatedCount = 0;
239
+ (0, devkit_1.visitNotIgnoredFiles)(tree, sourceRoot, (filePath) => {
240
+ if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx'))
241
+ return;
242
+ const content = tree.read(filePath, 'utf-8');
243
+ if (!content)
244
+ return;
245
+ // Match: import { useTranslation } from 'react-i18next';
246
+ const reactI18nextImport = /import\s*\{\s*useTranslation\s*\}\s*from\s*['"]react-i18next['"];?\s*\n?/;
247
+ if (!reactI18nextImport.test(content))
248
+ return;
249
+ // Skip the languages-provider.tsx file itself (will be deleted)
250
+ if (filePath.includes('languages-provider'))
251
+ return;
252
+ // Calculate relative path from this file to use-translation.tsx
253
+ const fileDir = path.dirname(filePath);
254
+ let relativePath = path.relative(fileDir, useTranslationPath);
255
+ // Remove .tsx extension for import
256
+ relativePath = relativePath.replace(/\.tsx$/, '');
257
+ // Ensure it starts with ./
258
+ if (!relativePath.startsWith('.')) {
259
+ relativePath = './' + relativePath;
260
+ }
261
+ const updatedContent = content.replace(reactI18nextImport, `import { useTranslation } from '${relativePath}';\n`);
262
+ if (updatedContent !== content) {
263
+ tree.write(filePath, updatedContent);
264
+ updatedCount++;
265
+ }
266
+ });
267
+ if (updatedCount > 0) {
268
+ devkit_1.logger.info(` ✅ Updated useTranslation imports in ${updatedCount} file(s)`);
269
+ }
270
+ }
271
+ function removeLanguagesProviderUsage(tree, sourceRoot) {
272
+ let updatedCount = 0;
273
+ (0, devkit_1.visitNotIgnoredFiles)(tree, sourceRoot, (filePath) => {
274
+ if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx'))
275
+ return;
276
+ if (filePath.includes('languages-provider'))
277
+ return;
278
+ const content = tree.read(filePath, 'utf-8');
279
+ if (!content)
280
+ return;
281
+ if (!content.includes('LanguagesProvider'))
282
+ return;
283
+ let updatedContent = content;
284
+ // Remove LanguagesProvider import (handles various import patterns)
285
+ // Pattern 1: Standalone import
286
+ updatedContent = updatedContent.replace(/import\s*\{\s*LanguagesProvider\s*\}\s*from\s*['"][^'"]+['"];?\s*\n?/g, '');
287
+ // Pattern 2: Part of a multi-import (e.g., import { X, LanguagesProvider, Y } from ...)
288
+ updatedContent = updatedContent.replace(/,\s*LanguagesProvider\s*/g, '');
289
+ updatedContent = updatedContent.replace(/LanguagesProvider\s*,\s*/g, '');
290
+ // Remove <LanguagesProvider> wrapping (keep children)
291
+ // Handle: <LanguagesProvider>\n...children...\n</LanguagesProvider>
292
+ updatedContent = updatedContent.replace(/\s*<LanguagesProvider>\s*\n?/g, '');
293
+ updatedContent = updatedContent.replace(/\s*<\/LanguagesProvider>\s*\n?/g, '');
294
+ if (updatedContent !== content) {
295
+ tree.write(filePath, updatedContent);
296
+ updatedCount++;
297
+ }
298
+ });
299
+ if (updatedCount > 0) {
300
+ devkit_1.logger.info(` ✅ Removed LanguagesProvider from ${updatedCount} file(s)`);
301
+ }
302
+ }
303
+ function deleteLanguagesProvider(tree, sourceRoot) {
304
+ const possiblePaths = [
305
+ `${sourceRoot}/src/lib/providers/languages-provider.tsx`,
306
+ `${sourceRoot}/src/lib/providers/languages-provider.ts`,
307
+ ];
308
+ for (const filePath of possiblePaths) {
309
+ if (tree.exists(filePath)) {
310
+ tree.delete(filePath);
311
+ devkit_1.logger.info(` 🗑️ Deleted ${filePath}`);
312
+ }
313
+ }
314
+ }
315
+ function updateBarrelExport(tree, sourceRoot) {
316
+ const indexPath = `${sourceRoot}/src/index.ts`;
317
+ if (!tree.exists(indexPath))
318
+ return;
319
+ const content = tree.read(indexPath, 'utf-8');
320
+ if (!content)
321
+ return;
322
+ // Remove export of languages-provider
323
+ const updatedContent = content.replace(/export\s*\*\s*from\s*['"]\.\/lib\/providers\/languages-provider['"];?\s*\n?/g, '');
324
+ if (updatedContent !== content) {
325
+ tree.write(indexPath, updatedContent);
326
+ devkit_1.logger.info(` ✅ Removed LanguagesProvider from barrel export`);
327
+ }
328
+ }
329
+ function exportUseTranslationFromBarrel(tree, sourceRoot) {
330
+ const indexPath = `${sourceRoot}/src/index.ts`;
331
+ if (!tree.exists(indexPath))
332
+ return;
333
+ const content = tree.read(indexPath, 'utf-8');
334
+ if (!content)
335
+ return;
336
+ // Check if already exported
337
+ if (content.includes('use-translation'))
338
+ return;
339
+ const exportLine = `export { useTranslation } from './lib/use-translation';\n`;
340
+ const updatedContent = content + exportLine;
341
+ tree.write(indexPath, updatedContent);
342
+ devkit_1.logger.info(` ✅ Added useTranslation to barrel export`);
343
+ }
344
+ // =============================================================================
345
+ // Step 2: Runtime Migrations
346
+ // =============================================================================
347
+ function migrateRuntimes(tree, runtimes, migratedPackages) {
348
+ let updatedCount = 0;
349
+ for (const [name, config] of Object.entries(runtimes)) {
350
+ const { sourceRoot } = config;
351
+ if (!tree.exists(sourceRoot)) {
352
+ devkit_1.logger.warn(` ⚠️ Runtime "${name}" source root not found: ${sourceRoot}`);
353
+ continue;
354
+ }
355
+ const runtimeFilePath = `${sourceRoot}/src/lib/runtime.tsx`;
356
+ if (!tree.exists(runtimeFilePath)) {
357
+ // Try alternative patterns
358
+ const altPath = `${sourceRoot}/src/lib/runtime.ts`;
359
+ if (!tree.exists(altPath)) {
360
+ devkit_1.logger.warn(` ⚠️ Runtime "${name}" has no runtime.tsx at ${runtimeFilePath}`);
361
+ continue;
362
+ }
363
+ }
364
+ devkit_1.logger.info(` ⚡ Migrating runtime "${name}"...`);
365
+ // 2a. Create src/languages folder structure for runtime
366
+ createRuntimeLanguagesFolder(tree, sourceRoot);
367
+ // 2b. Create use-translation.tsx for runtime (uses 'self' as packageName)
368
+ const runtimeUseTranslationPath = `${sourceRoot}/src/lib/use-translation.tsx`;
369
+ createUseTranslationFile(tree, runtimeUseTranslationPath, 'self');
370
+ // 2c. Add UnisphereI18NRuntimeProxy to runtime.tsx
371
+ addRuntimeProxy(tree, runtimeFilePath);
372
+ // 2d. Remove LanguagesProvider from runtime
373
+ removeLanguagesProviderFromRuntime(tree, runtimeFilePath);
374
+ // 2e. Update useTranslation imports in runtime files
375
+ updateRuntimeUseTranslationImports(tree, sourceRoot, migratedPackages);
376
+ updatedCount++;
377
+ }
378
+ if (updatedCount > 0) {
379
+ devkit_1.logger.info(` ✅ Migrated ${updatedCount} runtime(s)`);
380
+ }
381
+ }
382
+ function createRuntimeLanguagesFolder(tree, sourceRoot) {
383
+ const langDir = `${sourceRoot}/src/languages`;
384
+ // en-US.json
385
+ if (!tree.exists(`${langDir}/en-US.json`)) {
386
+ tree.write(`${langDir}/en-US.json`, '{}');
387
+ }
388
+ // supported-languges.ts
389
+ if (!tree.exists(`${langDir}/supported-languges.ts`)) {
390
+ tree.write(`${langDir}/supported-languges.ts`, `export const supportedLanguages: string[] = ['en-US'];\n`);
391
+ }
392
+ // files/.gitkeep
393
+ if (!tree.exists(`${langDir}/files/.gitkeep`)) {
394
+ tree.write(`${langDir}/files/.gitkeep`, '');
395
+ }
396
+ // _db/.gitignore
397
+ if (!tree.exists(`${langDir}/_db/.gitignore`)) {
398
+ tree.write(`${langDir}/_db/.gitignore`, '');
399
+ }
400
+ // README.md
401
+ if (!tree.exists(`${langDir}/README.md`)) {
402
+ createLanguagesReadme(tree, `${langDir}/README.md`);
403
+ }
404
+ }
405
+ function addRuntimeProxy(tree, runtimeFilePath) {
406
+ if (!tree.exists(runtimeFilePath))
407
+ return;
408
+ let content = tree.read(runtimeFilePath, 'utf-8');
409
+ if (!content)
410
+ return;
411
+ // Check if already has the proxy
412
+ if (content.includes('UnisphereI18NRuntimeProxy')) {
413
+ devkit_1.logger.info(` ℹ️ UnisphereI18NRuntimeProxy already present`);
414
+ return;
415
+ }
416
+ // Add import for UnisphereI18NRuntimeProxy
417
+ content = addI18nProxyImport(content);
418
+ // Add the proxy component inside each render method's ScopedUnisphereWorkspaceProvider
419
+ content = insertProxyIntoRenderMethods(content);
420
+ tree.write(runtimeFilePath, content);
421
+ devkit_1.logger.info(` ✅ Added UnisphereI18NRuntimeProxy to runtime`);
422
+ }
423
+ function addI18nProxyImport(content) {
424
+ // Check if @unisphere/ui-i18n-react is already imported
425
+ if (content.includes('@unisphere/ui-i18n-react')) {
426
+ // Add UnisphereI18NRuntimeProxy to existing import
427
+ return content.replace(/(import\s*\{[^}]*)(}\s*from\s*['"]@unisphere\/ui-i18n-react['"])/, (_, start, end) => {
428
+ if (start.includes('UnisphereI18NRuntimeProxy'))
429
+ return start + end;
430
+ return `${start.trimEnd()}, UnisphereI18NRuntimeProxy ${end}`;
431
+ });
432
+ }
433
+ // Add new import after existing @unisphere imports or at the top
434
+ const unisphereImportPattern = /import\s*\{[^}]*\}\s*from\s*['"]@unisphere\/[^'"]+['"];?\s*\n/g;
435
+ let lastMatch = null;
436
+ let match;
437
+ while ((match = unisphereImportPattern.exec(content)) !== null) {
438
+ lastMatch = match;
439
+ }
440
+ const importStatement = `import { UnisphereI18NRuntimeProxy } from '@unisphere/ui-i18n-react';\n`;
441
+ if (lastMatch) {
442
+ const insertPos = lastMatch.index + lastMatch[0].length;
443
+ return (content.slice(0, insertPos) + importStatement + content.slice(insertPos));
444
+ }
445
+ // Fallback: add after all imports
446
+ const lastImportMatch = content.match(/^import\s.+from\s+['"][^'"]+['"];?\s*$/gm);
447
+ if (lastImportMatch) {
448
+ const lastImport = lastImportMatch[lastImportMatch.length - 1];
449
+ const lastImportEnd = content.lastIndexOf(lastImport) + lastImport.length;
450
+ return (content.slice(0, lastImportEnd) +
451
+ '\n' +
452
+ importStatement +
453
+ content.slice(lastImportEnd));
454
+ }
455
+ return importStatement + content;
456
+ }
457
+ function insertProxyIntoRenderMethods(content) {
458
+ // Strategy: Find each render method's JSX tree and insert the proxy
459
+ // after the first <ScopedUnisphereWorkspaceProvider ...> opening tag.
460
+ // This works whether ScopedProvider wraps ThemeProvider or is inside it.
461
+ //
462
+ // Pattern we're looking for:
463
+ // <ScopedUnisphereWorkspaceProvider
464
+ // runtimeLogger={...}
465
+ // unisphereWorkspace={...}
466
+ // >
467
+ // <-- INSERT PROXY HERE
468
+ //
469
+ // We find each occurrence of this multi-line opening tag and insert after it.
470
+ const lines = content.split('\n');
471
+ const result = [];
472
+ let i = 0;
473
+ while (i < lines.length) {
474
+ result.push(lines[i]);
475
+ // Detect start of ScopedUnisphereWorkspaceProvider opening tag
476
+ if (lines[i].includes('<ScopedUnisphereWorkspaceProvider')) {
477
+ // Find the closing > of this opening tag (may be on same line or subsequent lines)
478
+ let closingLine = i;
479
+ // Check if tag closes on same line (self-closing or inline)
480
+ if (lines[i].includes('>') && !lines[i].includes('</')) {
481
+ closingLine = i;
482
+ }
483
+ else {
484
+ // Look forward for the closing >
485
+ for (let j = i + 1; j < lines.length && j < i + 10; j++) {
486
+ result.push(lines[j]);
487
+ closingLine = j;
488
+ if (lines[j].trim() === '>' || lines[j].trimEnd().endsWith('>')) {
489
+ break;
490
+ }
491
+ }
492
+ i = closingLine;
493
+ }
494
+ // Determine indentation for the proxy (one level deeper than the closing >)
495
+ const closingIndent = lines[closingLine].match(/^(\s*)/)?.[1] || '';
496
+ const proxyIndent = closingIndent + ' ';
497
+ // Insert the proxy component
498
+ result.push(`${proxyIndent}<UnisphereI18NRuntimeProxy`);
499
+ result.push(`${proxyIndent} runtimeUri={this._options.runtimeDeployedPath}`);
500
+ result.push(`${proxyIndent}/>`);
501
+ }
502
+ i++;
503
+ }
504
+ return result.join('\n');
505
+ }
506
+ function removeLanguagesProviderFromRuntime(tree, runtimeFilePath) {
507
+ if (!tree.exists(runtimeFilePath))
508
+ return;
509
+ const content = tree.read(runtimeFilePath, 'utf-8');
510
+ if (!content)
511
+ return;
512
+ if (!content.includes('LanguagesProvider'))
513
+ return;
514
+ let updatedContent = content;
515
+ // Remove LanguagesProvider from import statement
516
+ // Pattern: import { X, LanguagesProvider, Y } from '...'
517
+ // Or standalone: import { LanguagesProvider } from '...'
518
+ // Check if it's the only named import
519
+ const standaloneImport = /import\s*\{\s*LanguagesProvider\s*\}\s*from\s*['"][^'"]+['"];?\s*\n?/g;
520
+ if (standaloneImport.test(updatedContent)) {
521
+ updatedContent = updatedContent.replace(standaloneImport, '');
522
+ }
523
+ else {
524
+ // Part of multi-import: remove with surrounding comma
525
+ updatedContent = updatedContent.replace(/,\s*\n?\s*LanguagesProvider/g, '');
526
+ updatedContent = updatedContent.replace(/LanguagesProvider\s*,\s*\n?\s*/g, '');
527
+ }
528
+ // Remove <LanguagesProvider> and </LanguagesProvider> JSX wrapping
529
+ updatedContent = updatedContent.replace(/(\s*)<LanguagesProvider>\s*\n?/g, '');
530
+ updatedContent = updatedContent.replace(/(\s*)<\/LanguagesProvider>\s*\n?/g, '');
531
+ if (updatedContent !== content) {
532
+ tree.write(runtimeFilePath, updatedContent);
533
+ devkit_1.logger.info(` ✅ Removed LanguagesProvider from runtime`);
534
+ }
535
+ }
536
+ function updateRuntimeUseTranslationImports(tree, runtimeSourceRoot, migratedPackages) {
537
+ if (migratedPackages.length === 0)
538
+ return;
539
+ // Determine which package this runtime uses by checking the main runtime.tsx file
540
+ // for imports from any of our migrated packages
541
+ let targetPackage = null;
542
+ const runtimeFilePath = `${runtimeSourceRoot}/src/lib/runtime.tsx`;
543
+ if (tree.exists(runtimeFilePath)) {
544
+ const runtimeContent = tree.read(runtimeFilePath, 'utf-8') || '';
545
+ for (const pkg of migratedPackages) {
546
+ if (runtimeContent.includes(pkg.packageName)) {
547
+ targetPackage = pkg;
548
+ break;
549
+ }
550
+ }
551
+ }
552
+ // If runtime doesn't import from any migrated package, check all source files
553
+ if (!targetPackage) {
554
+ (0, devkit_1.visitNotIgnoredFiles)(tree, runtimeSourceRoot, (filePath) => {
555
+ if (targetPackage)
556
+ return;
557
+ if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx'))
558
+ return;
559
+ const content = tree.read(filePath, 'utf-8');
560
+ if (!content)
561
+ return;
562
+ for (const pkg of migratedPackages) {
563
+ if (content.includes(pkg.packageName)) {
564
+ targetPackage = pkg;
565
+ break;
566
+ }
567
+ }
568
+ });
569
+ }
570
+ // Last resort: use first migrated package
571
+ if (!targetPackage) {
572
+ targetPackage = migratedPackages[0];
573
+ }
574
+ let updatedCount = 0;
575
+ (0, devkit_1.visitNotIgnoredFiles)(tree, runtimeSourceRoot, (filePath) => {
576
+ if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx'))
577
+ return;
578
+ const content = tree.read(filePath, 'utf-8');
579
+ if (!content)
580
+ return;
581
+ const reactI18nextImport = /import\s*\{\s*useTranslation\s*\}\s*from\s*['"]react-i18next['"];?\s*\n?/;
582
+ if (!reactI18nextImport.test(content))
583
+ return;
584
+ const updatedContent = content.replace(reactI18nextImport, `import { useTranslation } from '${targetPackage.packageName}';\n`);
585
+ if (updatedContent !== content) {
586
+ tree.write(filePath, updatedContent);
587
+ updatedCount++;
588
+ }
589
+ });
590
+ if (updatedCount > 0) {
591
+ devkit_1.logger.info(` ✅ Updated useTranslation imports in ${updatedCount} runtime file(s)`);
592
+ }
593
+ }
594
+ // =============================================================================
595
+ // Step 3: Root package.json updates
596
+ // =============================================================================
597
+ function updateRootPackageJson(tree) {
598
+ if (!tree.exists('package.json'))
599
+ return;
600
+ const packageJson = (0, devkit_1.readJson)(tree, 'package.json');
601
+ const deps = packageJson.dependencies || {};
602
+ const devDeps = packageJson.devDependencies || {};
603
+ let changed = false;
604
+ // Remove i18next and react-i18next
605
+ if (deps['i18next']) {
606
+ delete deps['i18next'];
607
+ changed = true;
608
+ devkit_1.logger.info(' 🗑️ Removed i18next from dependencies');
609
+ }
610
+ if (deps['react-i18next']) {
611
+ delete deps['react-i18next'];
612
+ changed = true;
613
+ devkit_1.logger.info(' 🗑️ Removed react-i18next from dependencies');
614
+ }
615
+ if (devDeps['i18next']) {
616
+ delete devDeps['i18next'];
617
+ changed = true;
618
+ }
619
+ if (devDeps['react-i18next']) {
620
+ delete devDeps['react-i18next'];
621
+ changed = true;
622
+ }
623
+ if (changed) {
624
+ packageJson.dependencies = deps;
625
+ packageJson.devDependencies = devDeps;
626
+ (0, devkit_1.writeJson)(tree, 'package.json', packageJson);
627
+ devkit_1.logger.info(' ✅ Updated root package.json dependencies');
628
+ }
629
+ }
package/migrations.json CHANGED
@@ -562,6 +562,14 @@
562
562
  "cli": {
563
563
  "postUpdateMessage": "✅ .github/workflows/cicd.yml updated"
564
564
  }
565
+ },
566
+ "4.15.0-add-languages-support": {
567
+ "version": "4.15.0",
568
+ "description": "Migrates packages from i18next/react-i18next to @unisphere/ui-i18n-react with createUseTranslation pattern and adds UnisphereI18NRuntimeProxy to all runtimes",
569
+ "factory": "./dist/migrations/4-15-0/add-languages-support.js",
570
+ "cli": {
571
+ "postUpdateMessage": "✅ Languages support migrated to @unisphere/ui-i18n-react"
572
+ }
565
573
  }
566
574
  },
567
575
  "packageJsonUpdates": {
@@ -997,6 +1005,21 @@
997
1005
  "alwaysAddToPackageJson": false
998
1006
  }
999
1007
  }
1008
+ },
1009
+ "4.15.0": {
1010
+ "version": "4.15.0",
1011
+ "cli": "nx",
1012
+ "postUpdateMessage": "🎉 Migration to @unisphere/nx 4.15.0 completed successfully!",
1013
+ "packages": {
1014
+ "@unisphere/ui-i18n-react": {
1015
+ "version": "^1.70.1",
1016
+ "alwaysAddToPackageJson": true
1017
+ },
1018
+ "@unisphere/cli": {
1019
+ "version": "6.0.1",
1020
+ "alwaysAddToPackageJson": false
1021
+ }
1022
+ }
1000
1023
  }
1001
1024
  }
1002
1025
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unisphere/nx",
3
- "version": "4.14.4",
3
+ "version": "4.15.0",
4
4
  "private": false,
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",