intlayer-editor 2.0.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -0
  3. package/bin/start-server.js +16 -0
  4. package/dist/cjs/client/ContentEditionLayout.cjs +46 -0
  5. package/dist/cjs/client/ContentEditionLayout.cjs.map +1 -0
  6. package/dist/cjs/client/ContentEditionLayout.d.ts +12 -0
  7. package/dist/cjs/client/ContentSelectorWrapper.cjs +49 -0
  8. package/dist/cjs/client/ContentSelectorWrapper.cjs.map +1 -0
  9. package/dist/cjs/client/ContentSelectorWrapper.d.ts +12 -0
  10. package/dist/cjs/client/EditionPanel/EditionPanel.cjs +101 -0
  11. package/dist/cjs/client/EditionPanel/EditionPanel.cjs.map +1 -0
  12. package/dist/cjs/client/EditionPanel/EditionPanel.d.ts +11 -0
  13. package/dist/cjs/client/EditionPanel/index.cjs +27 -0
  14. package/dist/cjs/client/EditionPanel/index.cjs.map +1 -0
  15. package/dist/cjs/client/EditionPanel/index.d.ts +9 -0
  16. package/dist/cjs/client/EditionPanel/useEditedContentStore.cjs +90 -0
  17. package/dist/cjs/client/EditionPanel/useEditedContentStore.cjs.map +1 -0
  18. package/dist/cjs/client/EditionPanel/useEditedContentStore.d.ts +28 -0
  19. package/dist/cjs/client/EditionPanel/useEditionPanelStore.cjs +33 -0
  20. package/dist/cjs/client/EditionPanel/useEditionPanelStore.cjs.map +1 -0
  21. package/dist/cjs/client/EditionPanel/useEditionPanelStore.d.ts +16 -0
  22. package/dist/cjs/client/index.cjs +29 -0
  23. package/dist/cjs/client/index.cjs.map +1 -0
  24. package/dist/cjs/client/index.d.ts +13 -0
  25. package/dist/cjs/client/renderContentSelector.cjs +39 -0
  26. package/dist/cjs/client/renderContentSelector.cjs.map +1 -0
  27. package/dist/cjs/client/renderContentSelector.d.ts +6 -0
  28. package/dist/cjs/client/useEditorServer.cjs +53 -0
  29. package/dist/cjs/client/useEditorServer.cjs.map +1 -0
  30. package/dist/cjs/client/useEditorServer.d.ts +5 -0
  31. package/dist/cjs/server/content-editor.cjs +156 -0
  32. package/dist/cjs/server/content-editor.cjs.map +1 -0
  33. package/dist/cjs/server/content-editor.d.ts +12 -0
  34. package/dist/cjs/server/index.cjs +68 -0
  35. package/dist/cjs/server/index.cjs.map +1 -0
  36. package/dist/cjs/server/index.d.ts +3 -0
  37. package/dist/esm/client/ContentEditionLayout.d.mts +12 -0
  38. package/dist/esm/client/ContentEditionLayout.mjs +22 -0
  39. package/dist/esm/client/ContentEditionLayout.mjs.map +1 -0
  40. package/dist/esm/client/ContentSelectorWrapper.d.mts +12 -0
  41. package/dist/esm/client/ContentSelectorWrapper.mjs +25 -0
  42. package/dist/esm/client/ContentSelectorWrapper.mjs.map +1 -0
  43. package/dist/esm/client/EditionPanel/EditionPanel.d.mts +11 -0
  44. package/dist/esm/client/EditionPanel/EditionPanel.mjs +72 -0
  45. package/dist/esm/client/EditionPanel/EditionPanel.mjs.map +1 -0
  46. package/dist/esm/client/EditionPanel/index.d.mts +9 -0
  47. package/dist/esm/client/EditionPanel/index.mjs +4 -0
  48. package/dist/esm/client/EditionPanel/index.mjs.map +1 -0
  49. package/dist/esm/client/EditionPanel/useEditedContentStore.d.mts +28 -0
  50. package/dist/esm/client/EditionPanel/useEditedContentStore.mjs +66 -0
  51. package/dist/esm/client/EditionPanel/useEditedContentStore.mjs.map +1 -0
  52. package/dist/esm/client/EditionPanel/useEditionPanelStore.d.mts +16 -0
  53. package/dist/esm/client/EditionPanel/useEditionPanelStore.mjs +9 -0
  54. package/dist/esm/client/EditionPanel/useEditionPanelStore.mjs.map +1 -0
  55. package/dist/esm/client/index.d.mts +13 -0
  56. package/dist/esm/client/index.mjs +5 -0
  57. package/dist/esm/client/index.mjs.map +1 -0
  58. package/dist/esm/client/renderContentSelector.d.mts +6 -0
  59. package/dist/esm/client/renderContentSelector.mjs +15 -0
  60. package/dist/esm/client/renderContentSelector.mjs.map +1 -0
  61. package/dist/esm/client/useEditorServer.d.mts +5 -0
  62. package/dist/esm/client/useEditorServer.mjs +29 -0
  63. package/dist/esm/client/useEditorServer.mjs.map +1 -0
  64. package/dist/esm/server/content-editor.d.mts +12 -0
  65. package/dist/esm/server/content-editor.mjs +123 -0
  66. package/dist/esm/server/content-editor.mjs.map +1 -0
  67. package/dist/esm/server/index.d.mts +3 -0
  68. package/dist/esm/server/index.mjs +34 -0
  69. package/dist/esm/server/index.mjs.map +1 -0
  70. package/package.json +109 -0
  71. package/src/client/ContentEditionLayout.tsx +26 -0
  72. package/src/client/ContentSelectorWrapper.tsx +38 -0
  73. package/src/client/EditionPanel/EditionPanel.tsx +90 -0
  74. package/src/client/EditionPanel/index.ts +3 -0
  75. package/src/client/EditionPanel/useEditedContentStore.ts +98 -0
  76. package/src/client/EditionPanel/useEditionPanelStore.ts +19 -0
  77. package/src/client/index.ts +4 -0
  78. package/src/client/renderContentSelector.tsx +17 -0
  79. package/src/client/useEditorServer.ts +31 -0
  80. package/src/server/content-editor.ts +209 -0
  81. package/src/server/index.ts +40 -0
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "intlayer-editor",
3
+ "version": "2.0.0",
4
+ "private": false,
5
+ "description": "IntLayer Editor is a tool that allow you to edit your IntLayer declaration files using a graphical interface.",
6
+ "keywords": [
7
+ "intlayer",
8
+ "application",
9
+ "editor",
10
+ "typescript",
11
+ "javascript",
12
+ "json",
13
+ "file"
14
+ ],
15
+ "homepage": "https://github.com/aypineau/intlayer",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/aypineau/intlayer.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": {
22
+ "name": "Aymeric PINEAU",
23
+ "url": "https://github.com/aypineau"
24
+ },
25
+ "exports": {
26
+ "./client": {
27
+ "types": "./dist/esm/client/index.d.mts",
28
+ "require": "./dist/cjs/client/index.cjs",
29
+ "import": "./dist/esm/client/index.mjs"
30
+ },
31
+ "./server": {
32
+ "types": "./dist/esm/server/index.d.mts",
33
+ "require": "./dist/cjs/server/index.cjs",
34
+ "import": "./dist/esm/server/index.mjs"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "typesVersions": {
39
+ "*": {
40
+ "package.json": [
41
+ "./package.json"
42
+ ]
43
+ }
44
+ },
45
+ "bin": {
46
+ "intlayer-editor": "./bin/start-server.js"
47
+ },
48
+ "files": [
49
+ "./dist",
50
+ "./src",
51
+ "./bin",
52
+ "./package.json"
53
+ ],
54
+ "dependencies": {
55
+ "@types/body-parser": "^1.19.5",
56
+ "body-parser": "^1.20.2",
57
+ "commander": "^12.0.0",
58
+ "express": "^4.19.2",
59
+ "lucide-react": "^0.376.0",
60
+ "magic-regexp": "^0.8.0",
61
+ "react": "^18.2.0",
62
+ "react-dom": "^18.2.0",
63
+ "webpack": "^5.91.0",
64
+ "zustand": "^4.5.2",
65
+ "@intlayer/config": "^2.0.0",
66
+ "@intlayer/core": "^2.0.0",
67
+ "@intlayer/design-system": "^2.0.0",
68
+ "@intlayer/dictionaries-entry": "^2.0.0",
69
+ "intlayer": "^2.0.0"
70
+ },
71
+ "devDependencies": {
72
+ "@babel/generator": "7.24.4",
73
+ "@babel/parser": "7.24.4",
74
+ "@babel/types": "7.24.0",
75
+ "@changesets/changelog-github": "0.5.0",
76
+ "@changesets/cli": "2.27.1",
77
+ "@types/babel__generator": "^7.6.8",
78
+ "@types/express": "^4.17.21",
79
+ "@types/node": "^20.12.7",
80
+ "@types/react": "^18.2.79",
81
+ "@types/react-dom": "^18.2.25",
82
+ "rimraf": "5.0.5",
83
+ "ts-node": "^10.9.2",
84
+ "tsup": "^8.0.2",
85
+ "typescript": "^5.4.5",
86
+ "@utils/eslint-config": "^1.0.1",
87
+ "@utils/ts-config": "^1.0.1"
88
+ },
89
+ "engines": {
90
+ "node": ">=14.18"
91
+ },
92
+ "bug": {
93
+ "url": "https://github.com/aypineau/intlayer/issues"
94
+ },
95
+ "scripts": {
96
+ "build": "tsup",
97
+ "clean": "rimraf ./dist",
98
+ "dev": "tsup --watch",
99
+ "lint": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs",
100
+ "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs --fix",
101
+ "prettier": "prettier --check src",
102
+ "prettier:fix": "prettier --write src",
103
+ "serve": "webpack serve --config ./webpack.config.ts",
104
+ "start:dev": "node --experimental-specifier-resolution=node --loader ts-node/esm --watch src/index.ts",
105
+ "transpile": "webpack --config ./webpack.config.ts",
106
+ "typecheck": "tsup--project ./tsconfig.json --noEmit",
107
+ "watch": "webpack --config ./webpack.config.ts --watch"
108
+ }
109
+ }
@@ -0,0 +1,26 @@
1
+ import type { Locales } from '@intlayer/config/client';
2
+ import type { FC, ReactNode } from 'react';
3
+ import { EditionPanel } from './EditionPanel/index';
4
+
5
+ export type ContentEditionLayoutProps = {
6
+ children?: ReactNode;
7
+ locale: Locales;
8
+ localeList: Locales[];
9
+ setLocale: (locale: Locales) => void;
10
+ };
11
+
12
+ export const ContentEditionLayout: FC<ContentEditionLayoutProps> = ({
13
+ children,
14
+ locale,
15
+ setLocale,
16
+ localeList,
17
+ }) => (
18
+ <>
19
+ {children}
20
+ <EditionPanel
21
+ locale={locale}
22
+ localeList={localeList}
23
+ setLocale={setLocale}
24
+ />
25
+ </>
26
+ );
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import type { KeyPath } from '@intlayer/core';
4
+ import { ContentSelector } from '@intlayer/design-system';
5
+ import type { FC } from 'react';
6
+ import { useEditedContentStore } from './EditionPanel/useEditedContentStore';
7
+ import { useEditionPanelStore } from './EditionPanel/useEditionPanelStore';
8
+
9
+ type ContentSelectorWrapperProps = {
10
+ children: string;
11
+ dictionaryId: string;
12
+ dictionaryPath: string;
13
+ keyPath: KeyPath[];
14
+ };
15
+
16
+ export const ContentSelectorWrapper: FC<ContentSelectorWrapperProps> = ({
17
+ children,
18
+ dictionaryId,
19
+ dictionaryPath,
20
+ keyPath,
21
+ }) => {
22
+ const { setFocusedContent } = useEditionPanelStore();
23
+ const handleSelect = () =>
24
+ setFocusedContent({
25
+ dictionaryId,
26
+ dictionaryPath,
27
+ keyPath,
28
+ });
29
+
30
+ const { getEditedContentValue } = useEditedContentStore();
31
+ const editedValue = getEditedContentValue(dictionaryPath, keyPath);
32
+
33
+ return (
34
+ <ContentSelector onSelect={handleSelect}>
35
+ {editedValue ?? children}
36
+ </ContentSelector>
37
+ );
38
+ };
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import type { Locales } from '@intlayer/config/client';
4
+ import type { ContentModule } from '@intlayer/core';
5
+ import {
6
+ useRightDrawerStore,
7
+ RightDrawer,
8
+ DictionaryEditor,
9
+ LocaleSwitcher,
10
+ } from '@intlayer/design-system';
11
+ /**
12
+ * @intlayer/dictionaries-entry is a package that only returns the dictionary entry path.
13
+ * Using an external package allow to alias it in the bundle configuration (such as webpack).
14
+ * The alias allow hot reload the app (such as nextjs) on any dictionary change.
15
+ */
16
+ import dictionaries from '@intlayer/dictionaries-entry';
17
+ import { type FC, useEffect } from 'react';
18
+ import { useEditorServer } from '../useEditorServer';
19
+ import { useEditedContentStore } from './useEditedContentStore';
20
+ import { useEditionPanelStore } from './useEditionPanelStore';
21
+
22
+ type EditionPanelProps = {
23
+ locale: Locales;
24
+ localeList: Locales[];
25
+ setLocale: (locale: Locales) => void;
26
+ };
27
+
28
+ export const EditionPanel: FC<EditionPanelProps> = ({
29
+ locale,
30
+ localeList,
31
+ setLocale,
32
+ }) => {
33
+ const { open } = useRightDrawerStore();
34
+ const { focusedContent, setFocusedContent } = useEditionPanelStore();
35
+ const { editedContent, addEditedContent, clearEditedDictionaryContent } =
36
+ useEditedContentStore();
37
+ const { editContentRequest } = useEditorServer();
38
+
39
+ // Use effect to react to changes in focusedContent
40
+ useEffect(() => {
41
+ if (focusedContent !== null) {
42
+ open(); // Call the open function from useRightDrawerStore
43
+ }
44
+ }, [focusedContent, open]); // Depend on focusedContent and open to trigger the effect
45
+
46
+ if (!focusedContent) {
47
+ return null;
48
+ }
49
+
50
+ const dictionary: ContentModule = dictionaries[focusedContent.dictionaryId];
51
+
52
+ if (!dictionary?.filePath) {
53
+ return null;
54
+ }
55
+
56
+ const dictionaryPath = dictionary.filePath;
57
+ const editedDictionaryContent = editedContent[dictionaryPath] ?? [];
58
+
59
+ return (
60
+ <RightDrawer
61
+ title={dictionary.id}
62
+ label={`Edit dictionary ${dictionary.id}`}
63
+ header={
64
+ <LocaleSwitcher
65
+ setLocale={setLocale}
66
+ locale={locale}
67
+ localeList={localeList}
68
+ />
69
+ }
70
+ >
71
+ <DictionaryEditor
72
+ dictionary={dictionary}
73
+ locale={locale}
74
+ focusedKeyPath={focusedContent.keyPath}
75
+ editedContent={editedDictionaryContent}
76
+ onFocusKeyPath={(keyPath) =>
77
+ setFocusedContent({ ...focusedContent, keyPath })
78
+ }
79
+ onContentChange={(keyPath, newValue) =>
80
+ addEditedContent(dictionaryPath, keyPath, newValue)
81
+ }
82
+ onValidEdition={editContentRequest}
83
+ onCancelEdition={() => {
84
+ clearEditedDictionaryContent(dictionaryPath);
85
+ setFocusedContent(null);
86
+ }}
87
+ />
88
+ </RightDrawer>
89
+ );
90
+ };
@@ -0,0 +1,3 @@
1
+ export * from './EditionPanel';
2
+ export * from './useEditionPanelStore';
3
+ export * from './useEditedContentStore';
@@ -0,0 +1,98 @@
1
+ import type { KeyPath } from '@intlayer/core';
2
+ import type { FileContent } from '@intlayer/design-system';
3
+ import { create } from 'zustand';
4
+ import { persist, createJSONStorage } from 'zustand/middleware';
5
+
6
+ type DictionaryPath = string;
7
+
8
+ export type EditedContent = Record<DictionaryPath, FileContent[]>;
9
+
10
+ type EditedContentStore = {
11
+ editedContent: EditedContent;
12
+ addEditedContent: (
13
+ dictionaryPath: DictionaryPath,
14
+ keyPath: KeyPath[],
15
+ newValue: string
16
+ ) => void;
17
+ removeEditedContent: (
18
+ dictionaryPath: DictionaryPath,
19
+ keyPath: KeyPath[]
20
+ ) => void;
21
+ clearEditedDictionaryContent: (dictionaryPath: DictionaryPath) => void;
22
+ clearEditedContent: () => void;
23
+ getEditedContentValue: (
24
+ dictionaryPath: DictionaryPath,
25
+ keyPath: KeyPath[]
26
+ ) => string | undefined;
27
+ };
28
+
29
+ const isSameKeyPath = (keyPath1: KeyPath[], keyPath2: KeyPath[]) =>
30
+ keyPath1.every(
31
+ (element, index) =>
32
+ keyPath2[index] &&
33
+ keyPath2[index].key === element.key &&
34
+ keyPath2[index].type === element.type
35
+ );
36
+
37
+ export const useEditedContentStore = create(
38
+ persist<EditedContentStore>(
39
+ (set, get) => ({
40
+ editedContent: {},
41
+ addEditedContent: (dictionaryPath, keyPath, newValue) => {
42
+ set((state) => {
43
+ const editedContent = state.editedContent[dictionaryPath] ?? [];
44
+ return {
45
+ editedContent: {
46
+ ...state.editedContent,
47
+ [dictionaryPath]: [
48
+ ...editedContent.filter(
49
+ (content) => !isSameKeyPath(content.keyPath, keyPath)
50
+ ),
51
+ {
52
+ keyPath,
53
+ newValue,
54
+ },
55
+ ],
56
+ },
57
+ };
58
+ });
59
+ },
60
+
61
+ removeEditedContent: (dictionaryPath, keyPath) => {
62
+ set((state) => {
63
+ const editedContent = state.editedContent[dictionaryPath] ?? [];
64
+ return {
65
+ editedContent: {
66
+ ...state.editedContent,
67
+ [dictionaryPath]: editedContent.filter(
68
+ (content) => content.keyPath !== keyPath
69
+ ),
70
+ },
71
+ };
72
+ });
73
+ },
74
+
75
+ clearEditedDictionaryContent: (dictionaryPath) => {
76
+ set((state) => ({
77
+ editedContent: {
78
+ ...state.editedContent,
79
+ [dictionaryPath]: [],
80
+ },
81
+ }));
82
+ },
83
+
84
+ clearEditedContent: () => {
85
+ set({ editedContent: {} });
86
+ },
87
+
88
+ getEditedContentValue: (dictionaryPath, keyPath): string | undefined =>
89
+ get().editedContent[dictionaryPath]?.find((content) =>
90
+ isSameKeyPath(content.keyPath, keyPath)
91
+ )?.newValue,
92
+ }),
93
+ {
94
+ name: 'edited-content-store',
95
+ storage: createJSONStorage(() => sessionStorage),
96
+ }
97
+ )
98
+ );
@@ -0,0 +1,19 @@
1
+ import type { KeyPath } from '@intlayer/core';
2
+ import { create } from 'zustand';
3
+
4
+ type DictionaryPath = string;
5
+ type FileContent = {
6
+ dictionaryPath: DictionaryPath;
7
+ dictionaryId: string;
8
+ keyPath: KeyPath[];
9
+ };
10
+
11
+ type EditionPanelStore = {
12
+ focusedContent: FileContent | null;
13
+ setFocusedContent: (content: FileContent | null) => void;
14
+ };
15
+
16
+ export const useEditionPanelStore = create<EditionPanelStore>((set) => ({
17
+ focusedContent: null,
18
+ setFocusedContent: (content) => set({ focusedContent: content }),
19
+ }));
@@ -0,0 +1,4 @@
1
+ export * from './renderContentSelector';
2
+ export * from './EditionPanel/index';
3
+ export * from './ContentEditionLayout';
4
+ export * from './useEditorServer';
@@ -0,0 +1,17 @@
1
+ import type { KeyPath } from '@intlayer/core';
2
+ import { ContentSelectorWrapper } from './ContentSelectorWrapper';
3
+
4
+ export const renderContentEditor = (
5
+ content: string,
6
+ dictionaryId: string,
7
+ dictionaryPath: string,
8
+ keyPath: KeyPath[]
9
+ ) => (
10
+ <ContentSelectorWrapper
11
+ dictionaryId={dictionaryId}
12
+ dictionaryPath={dictionaryPath}
13
+ keyPath={keyPath}
14
+ >
15
+ {content}
16
+ </ContentSelectorWrapper>
17
+ );
@@ -0,0 +1,31 @@
1
+ import { getConfiguration } from '@intlayer/config/client';
2
+ import { useEditedContentStore } from './EditionPanel/useEditedContentStore';
3
+
4
+ export const useEditorServer = () => {
5
+ const { editedContent, clearEditedContent } = useEditedContentStore();
6
+
7
+ const editContentRequest = async () => {
8
+ const {
9
+ editor: { port },
10
+ } = getConfiguration();
11
+
12
+ await fetch(`http://localhost:${port}`, {
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ },
17
+ body: JSON.stringify(editedContent),
18
+ })
19
+ .then((response) => {
20
+ if (!response.ok) {
21
+ throw new Error('Failed to edit content');
22
+ }
23
+ clearEditedContent();
24
+ })
25
+ .catch((error) => {
26
+ console.error('Failed to edit content:', error);
27
+ });
28
+ };
29
+
30
+ return { editContentRequest };
31
+ };
@@ -0,0 +1,209 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { createRequire } from 'module';
3
+ import { parse } from '@babel/parser';
4
+ import type {
5
+ ObjectExpression,
6
+ VariableDeclarator,
7
+ Program,
8
+ Identifier,
9
+ StringLiteral,
10
+ AssignmentExpression,
11
+ ObjectProperty,
12
+ CallExpression,
13
+ ObjectMethod,
14
+ SpreadElement,
15
+ } from '@babel/types';
16
+ import {
17
+ NodeType,
18
+ type KeyPath,
19
+ type ObjectExpressionNode,
20
+ type TranslationOrEnumerationNode,
21
+ } from '@intlayer/core';
22
+ import prettier from 'prettier';
23
+ import type { EditedContent } from '../client/index';
24
+
25
+ const requireFunction =
26
+ typeof import.meta.url === 'undefined'
27
+ ? require
28
+ : createRequire(import.meta.url);
29
+
30
+ const { default: generate } = requireFunction('@babel/generator');
31
+
32
+ /**
33
+ * Helper function to find a nested property in an ObjectExpression based on a key path
34
+ */
35
+ const findNestedProperty = (
36
+ obj: ObjectExpression,
37
+ keyPath: KeyPath[]
38
+ ): ObjectExpression | undefined => {
39
+ let currentObj = obj;
40
+ for (const key of keyPath) {
41
+ let foundProperty:
42
+ | ObjectProperty
43
+ | ObjectMethod
44
+ | SpreadElement
45
+ | undefined;
46
+
47
+ if (
48
+ // if the keyPath is an object, select the related node
49
+ (key as ObjectExpressionNode).type === 'ObjectExpression'
50
+ ) {
51
+ foundProperty = currentObj.properties.find(
52
+ (prop) =>
53
+ 'key' in prop &&
54
+ (prop.key as Identifier).name === (key as ObjectExpressionNode).key
55
+ );
56
+ }
57
+
58
+ if (
59
+ // if the keypath is a translation or enumeration node, go across the function and select the related node
60
+ Object.values(NodeType).includes(
61
+ (key as TranslationOrEnumerationNode).type
62
+ )
63
+ ) {
64
+ foundProperty = (
65
+ (currentObj as unknown as CallExpression)
66
+ .arguments[0] as ObjectExpression
67
+ ).properties.find(
68
+ (prop) => 'key' in prop && (prop.key as Identifier).name === key.key
69
+ );
70
+ }
71
+
72
+ if (foundProperty && 'value' in foundProperty) {
73
+ currentObj = foundProperty.value as ObjectExpression;
74
+ } else {
75
+ return undefined;
76
+ }
77
+ }
78
+
79
+ return currentObj;
80
+ };
81
+
82
+ /**
83
+ * Find and update specific content based on key path
84
+ */
85
+ const findAndUpdate = (
86
+ objExpr: ObjectExpression,
87
+ keyPath: KeyPath[],
88
+ newValue: string
89
+ ) => {
90
+ const lastKey = keyPath[keyPath.length - 1]; // Get the last key in the path
91
+
92
+ if (lastKey) {
93
+ const propertyToUpdate = findNestedProperty(objExpr, keyPath); // Traverse the key path
94
+
95
+ if (propertyToUpdate && 'value' in propertyToUpdate) {
96
+ (propertyToUpdate as unknown as StringLiteral).value = newValue; // Update the value of the specified key
97
+ }
98
+ }
99
+ };
100
+
101
+ /**
102
+ * Traverse the AST and update based on key path and new value
103
+ */
104
+ const traverseNode = (node: Program, keyPath: KeyPath[], newValue: string) => {
105
+ if (Array.isArray(node.body)) {
106
+ (node.body as unknown as Program[]).forEach((subNode) =>
107
+ traverseNode(subNode, keyPath, newValue)
108
+ );
109
+ } else if (node.body) {
110
+ traverseNode(node.body, keyPath, newValue);
111
+ }
112
+
113
+ if (
114
+ // For ES Module (e.g., `const variable = ...; export default variable`)
115
+ 'declarations' in node
116
+ ) {
117
+ (node.declarations as VariableDeclarator[]).forEach((declaration) => {
118
+ if (declaration.init?.type === 'ObjectExpression') {
119
+ findAndUpdate(declaration.init, keyPath, newValue);
120
+ }
121
+ });
122
+ }
123
+
124
+ if (
125
+ // For ES Module (e.g., `export default { ... }`)
126
+ 'declaration' in node &&
127
+ (node.declaration as ObjectExpression).type === 'ObjectExpression'
128
+ ) {
129
+ return findAndUpdate(
130
+ node.declaration as ObjectExpression,
131
+ keyPath,
132
+ newValue
133
+ );
134
+ }
135
+
136
+ if (
137
+ // For CommonJS (e.g., `module.exports = ...`)
138
+ 'expression' in node &&
139
+ (node.expression as AssignmentExpression).right.type === 'ObjectExpression'
140
+ ) {
141
+ return findAndUpdate(
142
+ (node.expression as AssignmentExpression).right as ObjectExpression,
143
+ keyPath,
144
+ newValue
145
+ );
146
+ }
147
+
148
+ // throw new Error('Could not find the specified key path in the AST.');
149
+ };
150
+
151
+ /**
152
+ * Format the content with Prettier
153
+ */
154
+ const format = async (content: string) => {
155
+ // Resolve the configuration from the project
156
+ let options: prettier.Options = {};
157
+
158
+ try {
159
+ // Resolve the prettier configuration from the project
160
+ options =
161
+ (await prettier.resolveConfig(content, {
162
+ editorconfig: true,
163
+ })) ?? {};
164
+ } catch (error) {
165
+ console.error('Failed to resolve Prettier configuration:', error);
166
+ }
167
+
168
+ // Add the parser option to the resolved config
169
+ const config: prettier.Options = { ...options, parser: 'typescript' };
170
+
171
+ return prettier.format(content, config);
172
+ };
173
+
174
+ /**
175
+ * Edit the content of a file based on the key path and new value
176
+ */
177
+ export const editContent = async (editedContent: EditedContent) => {
178
+ // Loop into each dictionary path
179
+ for (const dictionaryPath of Object.keys(editedContent)) {
180
+ // Read the file
181
+ const fileContent = readFileSync(dictionaryPath, 'utf-8');
182
+
183
+ // Parse the content with TypeScript support
184
+ const parsed = parse(fileContent, {
185
+ sourceType: 'module',
186
+ plugins: ['jsx', 'typescript'],
187
+ });
188
+
189
+ // Loop into each key path and new value
190
+ for (const { keyPath, newValue } of editedContent[dictionaryPath]) {
191
+ // Update values based on key paths
192
+ traverseNode(parsed.program, keyPath, newValue);
193
+ }
194
+
195
+ // Generate the updated code
196
+ const updatedContent = generate(parsed).code;
197
+
198
+ if (fileContent === updatedContent) {
199
+ console.info(`Could not find specified key path in ${dictionaryPath}.`);
200
+ } else {
201
+ const formattedContent = await format(updatedContent);
202
+
203
+ // Write back to the file
204
+ writeFileSync(dictionaryPath, formattedContent, 'utf-8');
205
+
206
+ console.info('Updated the file successfully.');
207
+ }
208
+ }
209
+ };
@@ -0,0 +1,40 @@
1
+ import { getConfiguration } from '@intlayer/config';
2
+ import bodyParser from 'body-parser';
3
+ import express, { type Request, type Response } from 'express';
4
+ import type { EditedContent } from '../client';
5
+ import { editContent } from './content-editor';
6
+
7
+ export const startIntlayerEditor = () => {
8
+ const app = express();
9
+ const {
10
+ editor: { port },
11
+ } = getConfiguration();
12
+
13
+ app.use(bodyParser.json());
14
+ app.use(bodyParser.urlencoded({ extended: true }));
15
+
16
+ app.post(
17
+ '/',
18
+ async (
19
+ req: Request<undefined, undefined, EditedContent>,
20
+ res: Response
21
+ ) => {
22
+ const editedContent = req.body;
23
+
24
+ try {
25
+ await editContent(editedContent);
26
+
27
+ res.send({ success: true, editedContent });
28
+ } catch (error) {
29
+ console.error(error);
30
+ res.status(500).send({ success: false });
31
+ }
32
+ }
33
+ );
34
+
35
+ app.listen(port, () => {
36
+ console.info(
37
+ `Intlayer editor server is running on http://localhost:${port}`
38
+ );
39
+ });
40
+ };