@wads.dev/i18n-editor 0.0.1-alpha.0 → 0.0.1-beta.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.
- package/README.md +29 -4
- package/dist/cli/index.js +55 -0
- package/dist/core/keyPath.js +18 -4
- package/dist/core/projectApi.d.ts +33 -0
- package/dist/core/removeKey.d.ts +5 -0
- package/dist/core/removeKey.js +16 -0
- package/dist/public/editor.js +900 -97
- package/dist/public/index.html +78 -53
- package/dist/public/styles.css +31 -0
- package/dist/server/createServer.js +44 -2
- package/dist/server/exportProject.d.ts +5 -1
- package/dist/server/exportProject.js +6 -0
- package/dist/server/projectContext.d.ts +1 -0
- package/dist/server/projectContext.js +1 -1
- package/dist/server/usageAnalysis.d.ts +13 -0
- package/dist/server/usageAnalysis.js +357 -0
- package/package.json +10 -5
package/dist/public/index.html
CHANGED
|
@@ -1,147 +1,154 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>
|
|
6
|
+
<title>i18n Bundle Editor</title>
|
|
7
7
|
<link rel="stylesheet" href="./styles.css">
|
|
8
8
|
<script type="module" src="./editor.js"></script>
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<main class="page">
|
|
12
12
|
<header class="page-header">
|
|
13
|
-
<p class="eyebrow">i18n bundle</p>
|
|
13
|
+
<p class="eyebrow" data-i18n="page.eyebrow">i18n bundle</p>
|
|
14
14
|
<div class="page-title-row">
|
|
15
|
-
<h1>
|
|
16
|
-
<
|
|
15
|
+
<h1 data-i18n="page.title">Translation Editor</h1>
|
|
16
|
+
<label class="language-field" for="editor-language">
|
|
17
|
+
<span data-i18n="language.label">Language</span>
|
|
18
|
+
<select id="editor-language">
|
|
19
|
+
<option value="en" data-i18n="language.english">English</option>
|
|
20
|
+
<option value="pt" data-i18n="language.portuguese">Português</option>
|
|
21
|
+
</select>
|
|
22
|
+
</label>
|
|
23
|
+
<button id="toggle-settings-panel" class="icon-button" type="button" aria-label="Show settings" aria-expanded="false" title="Show settings">
|
|
17
24
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z"/><path d="m19.4 13.5 1.1.9-2 3.4-1.3-.5a8 8 0 0 1-1.8 1l-.2 1.4h-4l-.2-1.4a8 8 0 0 1-1.8-1l-1.3.5-2-3.4 1.1-.9a8.5 8.5 0 0 1 0-2l-1.1-.9 2-3.4 1.3.5a8 8 0 0 1 1.8-1l.2-1.4h4l.2 1.4a8 8 0 0 1 1.8 1l1.3-.5 2 3.4-1.1.9a8.5 8.5 0 0 1 0 2Z"/></svg>
|
|
18
25
|
</button>
|
|
19
26
|
</div>
|
|
20
|
-
<p class="intro">
|
|
27
|
+
<p class="intro" data-i18n="page.intro">The editor detects the current project's configuration and bundle. When necessary, it generates the bundle directly from the TypeScript catalog.</p>
|
|
21
28
|
</header>
|
|
22
29
|
|
|
23
30
|
<section id="project-status" class="project-status loading" role="status" aria-live="polite">
|
|
24
31
|
<span class="status-spinner" aria-hidden="true"></span>
|
|
25
|
-
<span id="project-status-text">
|
|
26
|
-
<button id="retry-project-load" type="button" hidden>
|
|
32
|
+
<span id="project-status-text">Connecting to the project…</span>
|
|
33
|
+
<button id="retry-project-load" type="button" data-i18n="messages.retry" hidden>Try again</button>
|
|
27
34
|
</section>
|
|
28
35
|
|
|
29
36
|
<section id="settings-panel" class="settings-panel" aria-labelledby="settings-title" hidden>
|
|
30
37
|
<div class="settings-intro">
|
|
31
|
-
<h2 id="settings-title">
|
|
32
|
-
<p>
|
|
38
|
+
<h2 id="settings-title" data-i18n="settings.title">Project settings</h2>
|
|
39
|
+
<p data-i18n="settings.intro">Define the key structure and paths generated by exports from the project root.</p>
|
|
33
40
|
</div>
|
|
34
41
|
<div class="settings-fields">
|
|
35
42
|
<label class="level-field" for="level-count">
|
|
36
|
-
<span>
|
|
43
|
+
<span data-i18n="settings.levelCount">Level count</span>
|
|
37
44
|
<input id="level-count" type="number" min="0" step="1" value="2">
|
|
38
45
|
</label>
|
|
39
46
|
<label class="level-field level-names-field" for="level-names">
|
|
40
|
-
<span>
|
|
41
|
-
<input id="level-names" type="text" value="
|
|
47
|
+
<span data-i18n="settings.levelNames">Level names</span>
|
|
48
|
+
<input id="level-names" type="text" value="Module, Feature" placeholder="Module, Feature, Subfeature">
|
|
42
49
|
</label>
|
|
43
50
|
</div>
|
|
44
51
|
<div class="level-paths-section">
|
|
45
|
-
<h3>Imports
|
|
46
|
-
<p>
|
|
52
|
+
<h3 data-i18n="settings.importsByLevel">Imports by level</h3>
|
|
53
|
+
<p data-i18n="settings.importsHelp">Level 0 represents the root and does not consume a key segment. Use {value} in the remaining levels. Replacers accept flat or nested JSON.</p>
|
|
47
54
|
<div id="level-imports" class="level-imports"></div>
|
|
48
55
|
</div>
|
|
49
56
|
<div class="settings-fields output-fields">
|
|
50
57
|
<label class="level-field" for="translations-directory">
|
|
51
|
-
<span>
|
|
58
|
+
<span data-i18n="settings.translationsDirectory">Translations directory</span>
|
|
52
59
|
<input id="translations-directory" type="text" value="i18n" placeholder="i18n">
|
|
53
60
|
</label>
|
|
54
61
|
<label class="level-field" for="language-file-template">
|
|
55
|
-
<span>
|
|
62
|
+
<span data-i18n="settings.languageFileTemplate">Language file template</span>
|
|
56
63
|
<input id="language-file-template" type="text" value="{language}.ts" placeholder="{language}.ts">
|
|
57
64
|
</label>
|
|
58
65
|
</div>
|
|
59
66
|
<div class="settings-fields output-fields">
|
|
60
67
|
<label class="level-field level-json-field" for="language-replacer">
|
|
61
|
-
<span>
|
|
68
|
+
<span data-i18n="settings.languageReplacer">Language replacer (JSON)</span>
|
|
62
69
|
<textarea id="language-replacer" spellcheck="false" placeholder='{ "pt": "ptBR" }'></textarea>
|
|
63
70
|
</label>
|
|
64
71
|
</div>
|
|
65
72
|
<div class="export-settings-section">
|
|
66
|
-
<h3>
|
|
67
|
-
<p>Configure
|
|
73
|
+
<h3 data-i18n="settings.exportTitle">Export</h3>
|
|
74
|
+
<p data-i18n="settings.exportIntro">Configure path resolution and formatting for generated TypeScript files.</p>
|
|
68
75
|
<div class="export-settings-fields">
|
|
69
76
|
<label class="level-field level-json-field" for="import-aliases">
|
|
70
|
-
<span>
|
|
77
|
+
<span data-i18n="settings.importAliases">Import aliases (JSON)</span>
|
|
71
78
|
<textarea id="import-aliases" spellcheck="false" placeholder='{ "@/": "src/" }'></textarea>
|
|
72
79
|
</label>
|
|
73
80
|
</div>
|
|
74
81
|
<div class="code-format-fields">
|
|
75
82
|
<label class="check-field" for="use-double-quotes">
|
|
76
83
|
<input id="use-double-quotes" type="checkbox">
|
|
77
|
-
<span>
|
|
84
|
+
<span data-i18n="settings.doubleQuotes">Use double quotes in generated strings</span>
|
|
78
85
|
</label>
|
|
79
86
|
<label class="check-field" for="use-semicolons">
|
|
80
87
|
<input id="use-semicolons" type="checkbox" checked>
|
|
81
|
-
<span>
|
|
88
|
+
<span data-i18n="settings.semicolons">Use semicolons</span>
|
|
82
89
|
</label>
|
|
83
90
|
<label class="check-field" for="use-shorthand-properties">
|
|
84
91
|
<input id="use-shorthand-properties" type="checkbox" checked>
|
|
85
|
-
<span>
|
|
92
|
+
<span data-i18n="settings.shorthandProperties">Use shorthand properties</span>
|
|
86
93
|
</label>
|
|
87
94
|
<label class="check-field" for="use-trailing-commas">
|
|
88
95
|
<input id="use-trailing-commas" type="checkbox" checked>
|
|
89
|
-
<span>
|
|
96
|
+
<span data-i18n="settings.trailingCommas">Use trailing commas</span>
|
|
90
97
|
</label>
|
|
91
98
|
<label class="level-field" for="indentation-character">
|
|
92
|
-
<span>
|
|
99
|
+
<span data-i18n="settings.indentationCharacter">Indentation character</span>
|
|
93
100
|
<select id="indentation-character">
|
|
94
|
-
<option value="space">
|
|
95
|
-
<option value="tab">Tab</option>
|
|
101
|
+
<option value="space" data-i18n="settings.space">Space</option>
|
|
102
|
+
<option value="tab" data-i18n="settings.tab">Tab</option>
|
|
96
103
|
</select>
|
|
97
104
|
</label>
|
|
98
105
|
<label class="level-field" for="indentation-size">
|
|
99
|
-
<span>
|
|
106
|
+
<span data-i18n="settings.indentationSize">Indentation size</span>
|
|
100
107
|
<input id="indentation-size" type="number" min="1" max="8" step="1" value="2">
|
|
101
108
|
</label>
|
|
102
109
|
<label class="level-field" for="print-width">
|
|
103
|
-
<span>
|
|
110
|
+
<span data-i18n="settings.printWidth">Maximum line width</span>
|
|
104
111
|
<input id="print-width" type="number" min="40" max="400" step="1" value="120">
|
|
105
112
|
</label>
|
|
106
113
|
<label class="level-field" for="max-object-inline-items">
|
|
107
|
-
<span>
|
|
114
|
+
<span data-i18n="settings.maxObjectInlineItems">Maximum inline properties</span>
|
|
108
115
|
<input id="max-object-inline-items" type="number" min="0" max="100" step="1" value="4">
|
|
109
116
|
</label>
|
|
110
117
|
<label class="level-field" for="max-array-inline-items">
|
|
111
|
-
<span>
|
|
118
|
+
<span data-i18n="settings.maxArrayInlineItems">Maximum inline items</span>
|
|
112
119
|
<input id="max-array-inline-items" type="number" min="0" max="100" step="1" value="8">
|
|
113
120
|
</label>
|
|
114
121
|
<label class="level-field" for="object-layout">
|
|
115
|
-
<span>
|
|
122
|
+
<span data-i18n="settings.objectLayout">Object layout</span>
|
|
116
123
|
<select id="object-layout">
|
|
117
|
-
<option value="fit">Inline
|
|
118
|
-
<option value="multiline">
|
|
124
|
+
<option value="fit" data-i18n="settings.inlineWhenFits">Inline when it fits</option>
|
|
125
|
+
<option value="multiline" data-i18n="settings.alwaysMultiline">Always multiline</option>
|
|
119
126
|
</select>
|
|
120
127
|
</label>
|
|
121
128
|
<label class="level-field" for="array-layout">
|
|
122
|
-
<span>
|
|
129
|
+
<span data-i18n="settings.arrayLayout">Array layout</span>
|
|
123
130
|
<select id="array-layout">
|
|
124
|
-
<option value="fit">Inline
|
|
125
|
-
<option value="multiline">
|
|
131
|
+
<option value="fit" data-i18n="settings.inlineWhenFits">Inline when it fits</option>
|
|
132
|
+
<option value="multiline" data-i18n="settings.alwaysMultiline">Always multiline</option>
|
|
126
133
|
</select>
|
|
127
134
|
</label>
|
|
128
135
|
</div>
|
|
129
136
|
</div>
|
|
130
137
|
<div class="deletion-settings-section">
|
|
131
|
-
<h3>
|
|
132
|
-
<p>
|
|
138
|
+
<h3 data-i18n="settings.divergentFiles">Divergent files</h3>
|
|
139
|
+
<p data-i18n="settings.divergentFilesHelp">Files inside translation directories that are absent from the plan are deletion candidates.</p>
|
|
133
140
|
<div class="deletion-settings-fields">
|
|
134
141
|
<label class="check-field" for="deletion-enabled">
|
|
135
142
|
<input id="deletion-enabled" type="checkbox" checked>
|
|
136
|
-
<span>
|
|
143
|
+
<span data-i18n="settings.detectDeletionCandidates">Detect and warn about deletion candidates</span>
|
|
137
144
|
</label>
|
|
138
145
|
<label class="level-field" for="ignored-deletion-extensions">
|
|
139
|
-
<span>
|
|
146
|
+
<span data-i18n="settings.ignoredExtensions">Ignored extensions</span>
|
|
140
147
|
<input id="ignored-deletion-extensions" type="text" placeholder=".md, .txt">
|
|
141
148
|
</label>
|
|
142
149
|
<label class="check-field" for="auto-delete">
|
|
143
150
|
<input id="auto-delete" type="checkbox">
|
|
144
|
-
<span>
|
|
151
|
+
<span data-i18n="settings.allowDeletion">Allow deletion without --delete</span>
|
|
145
152
|
</label>
|
|
146
153
|
</div>
|
|
147
154
|
</div>
|
|
@@ -150,27 +157,45 @@
|
|
|
150
157
|
<section class="report" aria-labelledby="report-title">
|
|
151
158
|
<div class="report-header">
|
|
152
159
|
<div>
|
|
153
|
-
<h2 id="report-title">
|
|
154
|
-
<p id="bundle-summary">
|
|
160
|
+
<h2 id="report-title" data-i18n="editor.title">Translations</h2>
|
|
161
|
+
<p id="bundle-summary" data-i18n="editor.emptySummary">Load a bundle to view translations.</p>
|
|
155
162
|
<p id="editor-feedback" class="feedback" role="status" aria-live="polite"></p>
|
|
156
163
|
</div>
|
|
157
|
-
<
|
|
158
|
-
<
|
|
159
|
-
<
|
|
160
|
-
|
|
164
|
+
<div class="translation-actions">
|
|
165
|
+
<button id="analyze-usage" class="secondary-button" type="button" data-i18n="editor.analyzeUsages" disabled>Analyze usages</button>
|
|
166
|
+
<label class="usage-filter" for="show-unreferenced">
|
|
167
|
+
<input id="show-unreferenced" type="checkbox" disabled>
|
|
168
|
+
<span data-i18n="editor.unreferencedOnly">Unreferenced only</span>
|
|
169
|
+
</label>
|
|
170
|
+
<label class="visual-level-field" for="visual-level-count">
|
|
171
|
+
<span data-i18n="editor.visualizationLevels">Visualization levels</span>
|
|
172
|
+
<input id="visual-level-count" type="number" min="0" max="12" step="1" value="2">
|
|
173
|
+
</label>
|
|
174
|
+
<label class="search-field" for="translation-search">
|
|
175
|
+
<span data-i18n="editor.search">Search</span>
|
|
176
|
+
<input id="translation-search" type="search" data-i18n-placeholder="editor.searchPlaceholder" placeholder="Key or translation">
|
|
177
|
+
</label>
|
|
178
|
+
</div>
|
|
161
179
|
</div>
|
|
162
|
-
<div id="empty-state" class="empty-state">
|
|
180
|
+
<div id="empty-state" class="empty-state" data-i18n="editor.noBundle">No bundle loaded.</div>
|
|
163
181
|
<div id="table-wrap" class="table-wrap" hidden></div>
|
|
164
182
|
</section>
|
|
165
183
|
|
|
166
184
|
<section class="export-preview-panel" aria-labelledby="export-preview-title">
|
|
167
185
|
<div class="export-preview-header">
|
|
168
186
|
<div>
|
|
169
|
-
<h2 id="export-preview-title">
|
|
170
|
-
<p>
|
|
187
|
+
<h2 id="export-preview-title" data-i18n="exportPreview.title">Generated files preview</h2>
|
|
188
|
+
<p data-i18n="exportPreview.intro">This is a real-time simulation. Check diffs to compare the result with the current project files.</p>
|
|
171
189
|
<p id="export-preview-feedback" class="export-preview-feedback" role="status" aria-live="polite"></p>
|
|
172
190
|
</div>
|
|
173
|
-
<
|
|
191
|
+
<div class="export-actions">
|
|
192
|
+
<label class="export-delete-option" for="export-delete-obsolete">
|
|
193
|
+
<input id="export-delete-obsolete" type="checkbox">
|
|
194
|
+
<span data-i18n="exportPreview.deleteDivergent">Delete divergent files</span>
|
|
195
|
+
</label>
|
|
196
|
+
<button id="check-export-diffs" class="secondary-button" type="button" data-i18n="exportPreview.checkDiffs" disabled>Check diffs</button>
|
|
197
|
+
<button id="export-project" class="primary-button" type="button" data-i18n="exportPreview.exportProject" disabled>Export to project</button>
|
|
198
|
+
</div>
|
|
174
199
|
</div>
|
|
175
200
|
<div id="export-preview" class="export-preview" aria-live="polite"></div>
|
|
176
201
|
</section>
|
package/dist/public/styles.css
CHANGED
|
@@ -23,6 +23,8 @@ button, input, textarea { font: inherit; }
|
|
|
23
23
|
.page { width: calc(100% - 40px); margin: 44px auto; }
|
|
24
24
|
.page-header { max-width: 720px; margin-bottom: 28px; }
|
|
25
25
|
.page-title-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
26
|
+
.language-field { display: grid; gap: 3px; margin-left: auto; color: var(--muted); font-size: 10px; font-weight: 750; }
|
|
27
|
+
.language-field select { padding: 6px 24px 6px 8px; border: 1px solid #c8cfdd; border-radius: 8px; color: var(--ink); background: #fff; font: inherit; font-size: 12px; }
|
|
26
28
|
.eyebrow { margin: 0 0 5px; color: var(--accent); font-size: 12px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
|
|
27
29
|
h1, h2, p { margin-top: 0; }
|
|
28
30
|
h1 { margin-bottom: 8px; font-size: clamp(30px, 5vw, 46px); letter-spacing: -.045em; line-height: 1.05; }
|
|
@@ -58,6 +60,33 @@ h2 { margin-bottom: 4px; font-size: 18px; }
|
|
|
58
60
|
.export-preview-panel { margin-top: 18px; overflow: hidden; }
|
|
59
61
|
.export-preview-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; padding: 20px 22px; border-bottom: 1px solid var(--line); }
|
|
60
62
|
.export-preview-header p { margin-bottom: 0; color: var(--muted); }
|
|
63
|
+
.export-actions { display: flex; flex: 0 0 auto; align-items: center; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
|
|
64
|
+
.export-delete-option { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; font-weight: 700; }
|
|
65
|
+
.export-delete-option input { width: 15px; height: 15px; margin: 0; accent-color: var(--danger); }
|
|
66
|
+
.translation-actions { display: flex; align-items: flex-end; gap: 10px; }
|
|
67
|
+
.usage-filter { display: flex; align-items: center; gap: 7px; min-height: 38px; color: #34405b; font-size: 12px; font-weight: 750; white-space: nowrap; }
|
|
68
|
+
.usage-filter input { width: 15px; height: 15px; margin: 0; accent-color: var(--accent); }
|
|
69
|
+
.translation-group[hidden], .table-group[hidden] { display: none; }
|
|
70
|
+
.visual-level-field { display: grid; gap: 4px; color: #52617c; font-size: 11px; font-weight: 750; white-space: nowrap; }
|
|
71
|
+
.visual-level-field input { width: 86px; padding: 8px 9px; border: 1px solid #c8cfdd; border-radius: 8px; color: var(--ink); background: #fff; }
|
|
72
|
+
.usage-cell { min-width: 170px; font-size: 12px; }
|
|
73
|
+
.usage-trigger { display: inline-flex; align-items: center; gap: 8px; padding: 4px 7px; border: 0; border-radius: 6px; color: inherit; background: transparent; font: inherit; font-weight: 800; text-align: left; cursor: pointer; }
|
|
74
|
+
.usage-trigger:hover, .usage-trigger[aria-expanded="true"] { background: #f1f4fb; }
|
|
75
|
+
.usage-trigger-action { color: #52617c; font-size: 10px; font-weight: 750; text-decoration: underline; text-underline-offset: 2px; }
|
|
76
|
+
.usage-trigger.usage-used { color: #15803d; }
|
|
77
|
+
.usage-trigger.usage-unreferenced { color: var(--danger); }
|
|
78
|
+
.usage-trigger.usage-uncertain { color: #b45309; }
|
|
79
|
+
.usage-popover { position: fixed; z-index: 1000; display: grid; gap: 7px; width: max-content; min-width: 340px; max-width: min(620px, calc(100vw - 24px)); max-height: min(360px, calc(100vh - 24px)); overflow: auto; padding: 12px 14px; border: 1px solid #c8cfdd; border-radius: 10px; color: var(--ink); background: #fff; box-shadow: 0 18px 50px rgba(24, 39, 75, .22); font-size: 12px; }
|
|
80
|
+
.usage-popover strong { margin-bottom: 2px; }
|
|
81
|
+
.usage-popover code { display: block; overflow-wrap: anywhere; color: #3047b3; white-space: normal; }
|
|
82
|
+
.usage-popover-uncertain { border-color: #f0c078; }
|
|
83
|
+
.usage-popover-unreferenced { border-color: #f2b8b5; }
|
|
84
|
+
.usage-empty { color: var(--muted); }
|
|
85
|
+
.key-content { display: flex; align-items: center; justify-content: flex-start; gap: 8px; }
|
|
86
|
+
.key-content > span { min-width: 0; overflow-wrap: anywhere; }
|
|
87
|
+
.remove-key-button { display: inline-grid; place-items: center; width: 24px; height: 24px; flex: 0 0 auto; padding: 0; border: 1px solid transparent; border-radius: 6px; color: #9f3a38; background: transparent; font: 700 18px/1 sans-serif; cursor: pointer; opacity: .22; }
|
|
88
|
+
.remove-key-button:hover, .remove-key-button:focus-visible { border-color: #f2b8b5; color: var(--danger); background: #fff1f2; opacity: 1; outline: none; }
|
|
89
|
+
.remove-key-button-unreferenced { border-color: #f2b8b5; color: var(--danger); background: #fff1f2; opacity: 1; }
|
|
61
90
|
.export-preview-feedback { min-height: 21px; margin-top: 5px; font-size: 13px; }
|
|
62
91
|
.export-preview-feedback.error { color: var(--danger); }
|
|
63
92
|
.export-preview-feedback.warning { color: #b45309; font-weight: 700; }
|
|
@@ -133,6 +162,8 @@ tr[hidden] { display: none; }
|
|
|
133
162
|
@media (max-width: 720px) {
|
|
134
163
|
.page { width: min(100% - 24px, 1500px); margin: 24px auto; }
|
|
135
164
|
.report-header { align-items: flex-start; flex-direction: column; }
|
|
165
|
+
.page-title-row { align-items: flex-start; flex-wrap: wrap; }
|
|
166
|
+
.translation-actions { width: 100%; align-items: stretch; flex-direction: column; }
|
|
136
167
|
.export-preview-header { flex-direction: column; }
|
|
137
168
|
.export-diff-details { margin-left: 0; }
|
|
138
169
|
.settings-fields, .level-replacers { grid-template-columns: 1fr; }
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import fastifyStatic from '@fastify/static';
|
|
2
2
|
import { assertBundle } from '@wads.dev/i18n-ts/bundle';
|
|
3
3
|
import Fastify from 'fastify';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { normalizeEditorProjectConfig } from '../core/projectConfig.js';
|
|
6
|
-
import { planProjectExport } from './exportProject.js';
|
|
8
|
+
import { applyProjectExport, planProjectExport } from './exportProject.js';
|
|
7
9
|
import { createProjectContext } from './projectContext.js';
|
|
10
|
+
import { inspectTranslationUsageCache, refreshTranslationUsageCache } from './usageAnalysis.js';
|
|
8
11
|
export async function createServer(options = {}) {
|
|
9
12
|
const server = Fastify({ logger: false });
|
|
10
|
-
const
|
|
13
|
+
const compiledPublicDirectory = fileURLToPath(new URL('../public/', import.meta.url));
|
|
14
|
+
const publicDirectory = fs.existsSync(compiledPublicDirectory)
|
|
15
|
+
? compiledPublicDirectory
|
|
16
|
+
: path.resolve(fileURLToPath(new URL('../../dist/public/', import.meta.url)));
|
|
11
17
|
const project = createProjectContext(options);
|
|
18
|
+
let exportQueue = Promise.resolve();
|
|
12
19
|
server.get('/api/project', async (_request, reply) => {
|
|
13
20
|
try {
|
|
14
21
|
return await project.getInfo();
|
|
@@ -39,6 +46,41 @@ export async function createServer(options = {}) {
|
|
|
39
46
|
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
|
40
47
|
}
|
|
41
48
|
});
|
|
49
|
+
server.post('/api/usage-analysis', { bodyLimit: 20 * 1024 * 1024 }, async (request, reply) => {
|
|
50
|
+
try {
|
|
51
|
+
const usageOptions = {
|
|
52
|
+
projectDirectory: project.projectDirectory,
|
|
53
|
+
bundle: assertBundle(request.body?.bundle),
|
|
54
|
+
config: normalizeEditorProjectConfig(request.body?.config),
|
|
55
|
+
};
|
|
56
|
+
return request.body?.wait === true
|
|
57
|
+
? await refreshTranslationUsageCache(usageOptions)
|
|
58
|
+
: await inspectTranslationUsageCache(usageOptions);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.post('/api/export', { bodyLimit: 20 * 1024 * 1024 }, async (request, reply) => {
|
|
65
|
+
try {
|
|
66
|
+
const bundle = assertBundle(request.body?.bundle);
|
|
67
|
+
const config = normalizeEditorProjectConfig(request.body?.config);
|
|
68
|
+
const deleteObsolete = request.body?.deleteObsolete === true;
|
|
69
|
+
const exportTask = exportQueue.catch(() => undefined).then(async () => {
|
|
70
|
+
const plan = await planProjectExport(options, { bundle, config });
|
|
71
|
+
const result = await applyProjectExport(plan, { deleteObsolete });
|
|
72
|
+
return {
|
|
73
|
+
changes: plan.changes.map(({ kind, path, status, diff }) => ({ kind, path, status, diff })),
|
|
74
|
+
...result,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
exportQueue = exportTask;
|
|
78
|
+
return await exportTask;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
42
84
|
await server.register(fastifyStatic, {
|
|
43
85
|
root: publicDirectory,
|
|
44
86
|
prefix: '/',
|
|
@@ -24,4 +24,8 @@ export type ProjectExportState = {
|
|
|
24
24
|
export declare function planProjectExport(options?: ProjectContextOptions, state?: ProjectExportState): Promise<ProjectExportPlan>;
|
|
25
25
|
export declare function applyProjectExport(plan: ProjectExportPlan, options?: {
|
|
26
26
|
deleteObsolete?: boolean;
|
|
27
|
-
}): Promise<
|
|
27
|
+
}): Promise<{
|
|
28
|
+
written: number;
|
|
29
|
+
deleted: number;
|
|
30
|
+
preserved: number;
|
|
31
|
+
}>;
|
|
@@ -252,6 +252,7 @@ export async function planProjectExport(options = {}, state) {
|
|
|
252
252
|
return { projectDirectory, bundlePath: info.bundlePath, changes, managedDirectories, deletion: config.deletion };
|
|
253
253
|
}
|
|
254
254
|
export async function applyProjectExport(plan, options = {}) {
|
|
255
|
+
let written = 0;
|
|
255
256
|
for (const change of plan.changes.filter((item) => item.status !== 'delete')) {
|
|
256
257
|
if (change.status === 'unchanged' || change.content === undefined)
|
|
257
258
|
continue;
|
|
@@ -260,6 +261,7 @@ export async function applyProjectExport(plan, options = {}) {
|
|
|
260
261
|
try {
|
|
261
262
|
await fs.writeFile(temporaryPath, change.content, 'utf8');
|
|
262
263
|
await fs.rename(temporaryPath, change.absolutePath);
|
|
264
|
+
written += 1;
|
|
263
265
|
}
|
|
264
266
|
catch (error) {
|
|
265
267
|
await fs.rm(temporaryPath, { force: true });
|
|
@@ -268,10 +270,14 @@ export async function applyProjectExport(plan, options = {}) {
|
|
|
268
270
|
}
|
|
269
271
|
const shouldDelete = plan.deletion !== false
|
|
270
272
|
&& (plan.deletion.autoDelete || options.deleteObsolete === true);
|
|
273
|
+
let deleted = 0;
|
|
271
274
|
if (shouldDelete) {
|
|
272
275
|
for (const change of plan.changes.filter((item) => item.status === 'delete')) {
|
|
273
276
|
await fs.rm(change.absolutePath, { force: true, recursive: true });
|
|
277
|
+
deleted += 1;
|
|
274
278
|
}
|
|
275
279
|
await Promise.all(plan.managedDirectories.map(removeEmptyDescendants));
|
|
276
280
|
}
|
|
281
|
+
const deletionCandidates = plan.changes.filter((item) => item.status === 'delete').length;
|
|
282
|
+
return { written, deleted, preserved: deletionCandidates - deleted };
|
|
277
283
|
}
|
|
@@ -6,6 +6,7 @@ export type ProjectContextOptions = {
|
|
|
6
6
|
bundleFile?: string;
|
|
7
7
|
};
|
|
8
8
|
export declare function createProjectContext(options?: ProjectContextOptions): {
|
|
9
|
+
projectDirectory: string;
|
|
9
10
|
getInfo: () => Promise<ProjectInfo>;
|
|
10
11
|
generateBundle: () => Promise<GenerateBundleResult>;
|
|
11
12
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { I18nBundle } from '@wads.dev/i18n-ts/bundle';
|
|
2
|
+
import type { EditorProjectConfig } from '../core/projectConfig.js';
|
|
3
|
+
import type { TranslationUsageReport, TranslationUsageResponse } from '../core/projectApi.js';
|
|
4
|
+
type UsageAnalysisOptions = {
|
|
5
|
+
projectDirectory: string;
|
|
6
|
+
bundle: I18nBundle;
|
|
7
|
+
config: EditorProjectConfig;
|
|
8
|
+
};
|
|
9
|
+
export declare function inspectTranslationUsageCache(options: UsageAnalysisOptions): Promise<TranslationUsageResponse>;
|
|
10
|
+
export declare function refreshTranslationUsageCache(options: UsageAnalysisOptions): Promise<TranslationUsageResponse>;
|
|
11
|
+
export declare function analyzeTranslationUsageCached(options: UsageAnalysisOptions, refresh?: boolean): Promise<TranslationUsageReport>;
|
|
12
|
+
export declare function analyzeTranslationUsage({ projectDirectory, bundle, config, }: UsageAnalysisOptions): TranslationUsageReport;
|
|
13
|
+
export {};
|