@wads.dev/i18n-editor 0.0.1-alpha.0 → 0.0.1-rc.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 +1204 -129
- package/dist/public/index.html +101 -57
- package/dist/public/styles.css +172 -12
- 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,176 +1,220 @@
|
|
|
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
|
-
<header class="
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
<h1>
|
|
16
|
-
|
|
12
|
+
<header class="app-header">
|
|
13
|
+
<div class="app-identity">
|
|
14
|
+
<span class="eyebrow" data-i18n="page.eyebrow">i18n bundle</span>
|
|
15
|
+
<h1 data-i18n="page.title">Translation Editor</h1>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="app-actions">
|
|
18
|
+
<label class="compact-language-field" for="editor-language" data-i18n-title="language.label" title="Language">
|
|
19
|
+
<select id="editor-language">
|
|
20
|
+
<option value="en" data-i18n="language.english">English</option>
|
|
21
|
+
<option value="pt" data-i18n="language.portuguese">Português</option>
|
|
22
|
+
</select>
|
|
23
|
+
</label>
|
|
24
|
+
<button id="toggle-translation-toolbar" class="icon-button" type="button" data-i18n-title="editor.hideToolbar" data-i18n-aria-label="editor.hideToolbar" aria-expanded="true" title="Hide toolbar" aria-label="Hide toolbar">
|
|
25
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 14 5-5 5 5" /></svg>
|
|
26
|
+
</button>
|
|
27
|
+
<button id="toggle-settings-panel" class="icon-button" type="button" aria-label="Show settings" aria-expanded="false" title="Show settings">
|
|
17
28
|
<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
29
|
</button>
|
|
19
30
|
</div>
|
|
20
|
-
<p class="intro">O editor detecta a configuração e a bundle do projeto atual. Quando necessário, ele gera a bundle diretamente do catálogo TypeScript.</p>
|
|
21
31
|
</header>
|
|
22
32
|
|
|
23
33
|
<section id="project-status" class="project-status loading" role="status" aria-live="polite">
|
|
24
34
|
<span class="status-spinner" aria-hidden="true"></span>
|
|
25
|
-
<span id="project-status-text">
|
|
26
|
-
<button id="retry-project-load" type="button" hidden>
|
|
35
|
+
<span id="project-status-text">Connecting to the project…</span>
|
|
36
|
+
<button id="retry-project-load" type="button" data-i18n="messages.retry" hidden>Try again</button>
|
|
27
37
|
</section>
|
|
28
38
|
|
|
29
39
|
<section id="settings-panel" class="settings-panel" aria-labelledby="settings-title" hidden>
|
|
30
40
|
<div class="settings-intro">
|
|
31
|
-
<h2 id="settings-title">
|
|
32
|
-
<p>
|
|
41
|
+
<h2 id="settings-title" data-i18n="settings.title">Project settings</h2>
|
|
42
|
+
<p data-i18n="settings.intro">Define the key structure and paths generated by exports from the project root.</p>
|
|
33
43
|
</div>
|
|
34
44
|
<div class="settings-fields">
|
|
35
45
|
<label class="level-field" for="level-count">
|
|
36
|
-
<span>
|
|
46
|
+
<span data-i18n="settings.levelCount">Level count</span>
|
|
37
47
|
<input id="level-count" type="number" min="0" step="1" value="2">
|
|
38
48
|
</label>
|
|
39
49
|
<label class="level-field level-names-field" for="level-names">
|
|
40
|
-
<span>
|
|
41
|
-
<input id="level-names" type="text" value="
|
|
50
|
+
<span data-i18n="settings.levelNames">Level names</span>
|
|
51
|
+
<input id="level-names" type="text" value="Module, Feature" placeholder="Module, Feature, Subfeature">
|
|
42
52
|
</label>
|
|
43
53
|
</div>
|
|
44
54
|
<div class="level-paths-section">
|
|
45
|
-
<h3>Imports
|
|
46
|
-
<p>
|
|
55
|
+
<h3 data-i18n="settings.importsByLevel">Imports by level</h3>
|
|
56
|
+
<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
57
|
<div id="level-imports" class="level-imports"></div>
|
|
48
58
|
</div>
|
|
49
59
|
<div class="settings-fields output-fields">
|
|
50
60
|
<label class="level-field" for="translations-directory">
|
|
51
|
-
<span>
|
|
61
|
+
<span data-i18n="settings.translationsDirectory">Translations directory</span>
|
|
52
62
|
<input id="translations-directory" type="text" value="i18n" placeholder="i18n">
|
|
53
63
|
</label>
|
|
54
64
|
<label class="level-field" for="language-file-template">
|
|
55
|
-
<span>
|
|
65
|
+
<span data-i18n="settings.languageFileTemplate">Language file template</span>
|
|
56
66
|
<input id="language-file-template" type="text" value="{language}.ts" placeholder="{language}.ts">
|
|
57
67
|
</label>
|
|
58
68
|
</div>
|
|
59
69
|
<div class="settings-fields output-fields">
|
|
60
70
|
<label class="level-field level-json-field" for="language-replacer">
|
|
61
|
-
<span>
|
|
71
|
+
<span data-i18n="settings.languageReplacer">Language replacer (JSON)</span>
|
|
62
72
|
<textarea id="language-replacer" spellcheck="false" placeholder='{ "pt": "ptBR" }'></textarea>
|
|
63
73
|
</label>
|
|
64
74
|
</div>
|
|
65
75
|
<div class="export-settings-section">
|
|
66
|
-
<h3>
|
|
67
|
-
<p>Configure
|
|
76
|
+
<h3 data-i18n="settings.exportTitle">Export</h3>
|
|
77
|
+
<p data-i18n="settings.exportIntro">Configure path resolution and formatting for generated TypeScript files.</p>
|
|
68
78
|
<div class="export-settings-fields">
|
|
69
79
|
<label class="level-field level-json-field" for="import-aliases">
|
|
70
|
-
<span>
|
|
80
|
+
<span data-i18n="settings.importAliases">Import aliases (JSON)</span>
|
|
71
81
|
<textarea id="import-aliases" spellcheck="false" placeholder='{ "@/": "src/" }'></textarea>
|
|
72
82
|
</label>
|
|
73
83
|
</div>
|
|
74
84
|
<div class="code-format-fields">
|
|
75
85
|
<label class="check-field" for="use-double-quotes">
|
|
76
86
|
<input id="use-double-quotes" type="checkbox">
|
|
77
|
-
<span>
|
|
87
|
+
<span data-i18n="settings.doubleQuotes">Use double quotes in generated strings</span>
|
|
78
88
|
</label>
|
|
79
89
|
<label class="check-field" for="use-semicolons">
|
|
80
90
|
<input id="use-semicolons" type="checkbox" checked>
|
|
81
|
-
<span>
|
|
91
|
+
<span data-i18n="settings.semicolons">Use semicolons</span>
|
|
82
92
|
</label>
|
|
83
93
|
<label class="check-field" for="use-shorthand-properties">
|
|
84
94
|
<input id="use-shorthand-properties" type="checkbox" checked>
|
|
85
|
-
<span>
|
|
95
|
+
<span data-i18n="settings.shorthandProperties">Use shorthand properties</span>
|
|
86
96
|
</label>
|
|
87
97
|
<label class="check-field" for="use-trailing-commas">
|
|
88
98
|
<input id="use-trailing-commas" type="checkbox" checked>
|
|
89
|
-
<span>
|
|
99
|
+
<span data-i18n="settings.trailingCommas">Use trailing commas</span>
|
|
90
100
|
</label>
|
|
91
101
|
<label class="level-field" for="indentation-character">
|
|
92
|
-
<span>
|
|
102
|
+
<span data-i18n="settings.indentationCharacter">Indentation character</span>
|
|
93
103
|
<select id="indentation-character">
|
|
94
|
-
<option value="space">
|
|
95
|
-
<option value="tab">Tab</option>
|
|
104
|
+
<option value="space" data-i18n="settings.space">Space</option>
|
|
105
|
+
<option value="tab" data-i18n="settings.tab">Tab</option>
|
|
96
106
|
</select>
|
|
97
107
|
</label>
|
|
98
108
|
<label class="level-field" for="indentation-size">
|
|
99
|
-
<span>
|
|
109
|
+
<span data-i18n="settings.indentationSize">Indentation size</span>
|
|
100
110
|
<input id="indentation-size" type="number" min="1" max="8" step="1" value="2">
|
|
101
111
|
</label>
|
|
102
112
|
<label class="level-field" for="print-width">
|
|
103
|
-
<span>
|
|
113
|
+
<span data-i18n="settings.printWidth">Maximum line width</span>
|
|
104
114
|
<input id="print-width" type="number" min="40" max="400" step="1" value="120">
|
|
105
115
|
</label>
|
|
106
116
|
<label class="level-field" for="max-object-inline-items">
|
|
107
|
-
<span>
|
|
117
|
+
<span data-i18n="settings.maxObjectInlineItems">Maximum inline properties</span>
|
|
108
118
|
<input id="max-object-inline-items" type="number" min="0" max="100" step="1" value="4">
|
|
109
119
|
</label>
|
|
110
120
|
<label class="level-field" for="max-array-inline-items">
|
|
111
|
-
<span>
|
|
121
|
+
<span data-i18n="settings.maxArrayInlineItems">Maximum inline items</span>
|
|
112
122
|
<input id="max-array-inline-items" type="number" min="0" max="100" step="1" value="8">
|
|
113
123
|
</label>
|
|
114
124
|
<label class="level-field" for="object-layout">
|
|
115
|
-
<span>
|
|
125
|
+
<span data-i18n="settings.objectLayout">Object layout</span>
|
|
116
126
|
<select id="object-layout">
|
|
117
|
-
<option value="fit">Inline
|
|
118
|
-
<option value="multiline">
|
|
127
|
+
<option value="fit" data-i18n="settings.inlineWhenFits">Inline when it fits</option>
|
|
128
|
+
<option value="multiline" data-i18n="settings.alwaysMultiline">Always multiline</option>
|
|
119
129
|
</select>
|
|
120
130
|
</label>
|
|
121
131
|
<label class="level-field" for="array-layout">
|
|
122
|
-
<span>
|
|
132
|
+
<span data-i18n="settings.arrayLayout">Array layout</span>
|
|
123
133
|
<select id="array-layout">
|
|
124
|
-
<option value="fit">Inline
|
|
125
|
-
<option value="multiline">
|
|
134
|
+
<option value="fit" data-i18n="settings.inlineWhenFits">Inline when it fits</option>
|
|
135
|
+
<option value="multiline" data-i18n="settings.alwaysMultiline">Always multiline</option>
|
|
126
136
|
</select>
|
|
127
137
|
</label>
|
|
128
138
|
</div>
|
|
129
139
|
</div>
|
|
130
140
|
<div class="deletion-settings-section">
|
|
131
|
-
<h3>
|
|
132
|
-
<p>
|
|
141
|
+
<h3 data-i18n="settings.divergentFiles">Divergent files</h3>
|
|
142
|
+
<p data-i18n="settings.divergentFilesHelp">Files inside translation directories that are absent from the plan are deletion candidates.</p>
|
|
133
143
|
<div class="deletion-settings-fields">
|
|
134
144
|
<label class="check-field" for="deletion-enabled">
|
|
135
145
|
<input id="deletion-enabled" type="checkbox" checked>
|
|
136
|
-
<span>
|
|
146
|
+
<span data-i18n="settings.detectDeletionCandidates">Detect and warn about deletion candidates</span>
|
|
137
147
|
</label>
|
|
138
148
|
<label class="level-field" for="ignored-deletion-extensions">
|
|
139
|
-
<span>
|
|
149
|
+
<span data-i18n="settings.ignoredExtensions">Ignored extensions</span>
|
|
140
150
|
<input id="ignored-deletion-extensions" type="text" placeholder=".md, .txt">
|
|
141
151
|
</label>
|
|
142
152
|
<label class="check-field" for="auto-delete">
|
|
143
153
|
<input id="auto-delete" type="checkbox">
|
|
144
|
-
<span>
|
|
154
|
+
<span data-i18n="settings.allowDeletion">Allow deletion without --delete</span>
|
|
145
155
|
</label>
|
|
146
156
|
</div>
|
|
147
157
|
</div>
|
|
148
158
|
</section>
|
|
149
159
|
|
|
150
160
|
<section class="report" aria-labelledby="report-title">
|
|
151
|
-
<div class="report-header">
|
|
152
|
-
<div>
|
|
153
|
-
<h2 id="report-title">
|
|
154
|
-
<p id="bundle-summary">
|
|
161
|
+
<div id="translation-toolbar" class="report-header">
|
|
162
|
+
<div class="report-heading">
|
|
163
|
+
<h2 id="report-title" data-i18n="editor.title">Translations</h2>
|
|
164
|
+
<p id="bundle-summary" data-i18n="editor.emptySummary">Load a bundle to view translations.</p>
|
|
155
165
|
<p id="editor-feedback" class="feedback" role="status" aria-live="polite"></p>
|
|
166
|
+
<p id="review-summary" class="review-summary" role="status" aria-live="polite"></p>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="translation-actions">
|
|
169
|
+
<section class="filter-box" aria-label="Filters" data-i18n-aria-label="editor.filters">
|
|
170
|
+
<label class="toolbar-toggle" for="show-unreferenced" data-i18n-title="editor.unreferencedOnly" title="Show unreferenced only">
|
|
171
|
+
<input id="show-unreferenced" type="checkbox" disabled>
|
|
172
|
+
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 9v4M12 17h.01M10.3 4.4 2.8 18a2 2 0 0 0 1.75 3h14.9A2 2 0 0 0 21.2 18L13.7 4.4a2 2 0 0 0-3.4 0Z" /></svg>
|
|
173
|
+
<span data-i18n="editor.unreferencedOnly">Show unreferenced only</span>
|
|
174
|
+
</label>
|
|
175
|
+
<label class="toolbar-toggle toolbar-toggle--with-count" for="show-new-keys" data-i18n-title="editor.newKeysOnly" title="Show new keys only">
|
|
176
|
+
<input id="show-new-keys" type="checkbox" disabled>
|
|
177
|
+
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3v18M3 12h18" /><path d="m5 5 2 2m10-2-2 2m2 10-2-2M5 19l2-2" /></svg>
|
|
178
|
+
<span data-i18n="editor.newKeysOnly">Show new keys only</span>
|
|
179
|
+
<output id="new-key-count" class="filter-count"></output>
|
|
180
|
+
</label>
|
|
181
|
+
<input id="translation-search" class="search-field" type="search" data-i18n-placeholder="editor.searchPlaceholder" data-i18n-aria-label="editor.search" placeholder="Search by key or translation" aria-label="Search">
|
|
182
|
+
</section>
|
|
183
|
+
<label class="toolbar-field visual-level-field" for="visual-level-count" data-i18n-title="editor.visualizationLevelsHelp" title="Choose how many hierarchy levels become collapsible translation groups.">
|
|
184
|
+
<span data-i18n="editor.visualizationLevels">Levels</span>
|
|
185
|
+
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
186
|
+
<input id="visual-level-count" type="number" min="0" max="12" step="1" value="2">
|
|
187
|
+
</label>
|
|
188
|
+
<button id="analyze-usage" class="secondary-button icon-button toolbar-icon-button" type="button" data-i18n-title="editor.analyzeUsages" data-i18n-aria-label="editor.analyzeUsages" title="Analyze usages" aria-label="Analyze usages" disabled>
|
|
189
|
+
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 1-2.34-5.66M20 4v7h-7" /></svg>
|
|
190
|
+
</button>
|
|
191
|
+
<button id="mark-current-reviewed" class="secondary-button icon-button toolbar-icon-button" type="button" data-i18n-title="editor.markCurrentReviewed" data-i18n-aria-label="editor.markCurrentReviewed" title="Mark current keys as reviewed" aria-label="Mark current keys as reviewed" disabled>
|
|
192
|
+
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6M5 6h7M5 18h7" /></svg>
|
|
193
|
+
</button>
|
|
194
|
+
<button id="toggle-all-groups" class="secondary-button icon-button toolbar-icon-button" type="button" data-i18n-title="editor.expandAll" data-i18n-aria-label="editor.expandAll" title="Expand all" aria-label="Expand all" aria-pressed="false" disabled>
|
|
195
|
+
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="m8 14 4 4 4-4M8 10l4-4 4 4M12 6v12" /></svg>
|
|
196
|
+
</button>
|
|
156
197
|
</div>
|
|
157
|
-
<label class="search-field" for="translation-search">
|
|
158
|
-
<span>Buscar</span>
|
|
159
|
-
<input id="translation-search" type="search" placeholder="Chave ou tradução">
|
|
160
|
-
</label>
|
|
161
198
|
</div>
|
|
162
|
-
<div id="empty-state" class="empty-state">
|
|
199
|
+
<div id="empty-state" class="empty-state" data-i18n="editor.noBundle">No bundle loaded.</div>
|
|
163
200
|
<div id="table-wrap" class="table-wrap" hidden></div>
|
|
164
201
|
</section>
|
|
165
202
|
|
|
166
203
|
<section class="export-preview-panel" aria-labelledby="export-preview-title">
|
|
167
204
|
<div class="export-preview-header">
|
|
168
205
|
<div>
|
|
169
|
-
<h2 id="export-preview-title">
|
|
170
|
-
<p>
|
|
206
|
+
<h2 id="export-preview-title" data-i18n="exportPreview.title">Generated files preview</h2>
|
|
207
|
+
<p data-i18n="exportPreview.intro">This is a real-time simulation. Check diffs to compare the result with the current project files.</p>
|
|
171
208
|
<p id="export-preview-feedback" class="export-preview-feedback" role="status" aria-live="polite"></p>
|
|
172
209
|
</div>
|
|
173
|
-
<
|
|
210
|
+
<div class="export-actions">
|
|
211
|
+
<label class="export-delete-option" for="export-delete-obsolete">
|
|
212
|
+
<input id="export-delete-obsolete" type="checkbox">
|
|
213
|
+
<span data-i18n="exportPreview.deleteDivergent">Delete divergent files</span>
|
|
214
|
+
</label>
|
|
215
|
+
<button id="check-export-diffs" class="secondary-button" type="button" data-i18n="exportPreview.checkDiffs" disabled>Check diffs</button>
|
|
216
|
+
<button id="export-project" class="primary-button" type="button" data-i18n="exportPreview.exportProject" disabled>Export to project</button>
|
|
217
|
+
</div>
|
|
174
218
|
</div>
|
|
175
219
|
<div id="export-preview" class="export-preview" aria-live="polite"></div>
|
|
176
220
|
</section>
|
package/dist/public/styles.css
CHANGED
|
@@ -20,12 +20,14 @@ body {
|
|
|
20
20
|
|
|
21
21
|
button, input, textarea { font: inherit; }
|
|
22
22
|
|
|
23
|
-
.page { width: calc(100% -
|
|
24
|
-
.
|
|
25
|
-
.
|
|
26
|
-
.
|
|
23
|
+
.page { width: calc(100% - 24px); margin: 12px auto 24px; }
|
|
24
|
+
.app-header { display: flex; align-items: center; justify-content: space-between; min-height: 52px; margin-bottom: 10px; padding: 0 4px; }
|
|
25
|
+
.app-identity, .app-actions { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
26
|
+
.app-identity { gap: 9px; }
|
|
27
|
+
.compact-language-field select { height: 32px; padding: 5px 25px 5px 9px; border: 1px solid #c8cfdd; border-radius: 8px; color: var(--ink); background: #fff; font: inherit; font-size: 12px; font-weight: 700; }
|
|
28
|
+
.eyebrow { margin: 0; color: var(--accent); font-size: 10px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; }
|
|
27
29
|
h1, h2, p { margin-top: 0; }
|
|
28
|
-
h1 { margin
|
|
30
|
+
h1 { margin: 0; font-size: clamp(22px, 2.25vw, 30px); letter-spacing: -.04em; line-height: 1.05; white-space: nowrap; }
|
|
29
31
|
h2 { margin-bottom: 4px; font-size: 18px; }
|
|
30
32
|
.intro, .report-header p { color: var(--muted); }
|
|
31
33
|
|
|
@@ -58,6 +60,47 @@ 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; width: 100%; flex-wrap: wrap; align-items: flex-end; justify-content: flex-end; gap: 8px; }
|
|
67
|
+
.toolbar-toggle { min-height: 38px; }
|
|
68
|
+
.toolbar-icon-button { width: 38px; height: 38px; border-radius: 9px; }
|
|
69
|
+
.filter-box { display: flex; flex: 1 1 430px; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin-right: auto; padding: 0; }
|
|
70
|
+
.filter-box-title { align-self: center; padding: 0 3px; color: #52617c; font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
|
|
71
|
+
.toolbar-toggle { display: inline-flex; align-items: center; gap: 7px; padding: 8px 10px; border: 1px solid #c8cfdd; border-radius: 9px; color: #34405b; background: #fff; font-size: 12px; font-weight: 750; line-height: 1.25; white-space: nowrap; }
|
|
72
|
+
.toolbar-toggle:has(input:checked) { border-color: #a5b4fc; color: #3730a3; background: #eef2ff; }
|
|
73
|
+
.toolbar-toggle:has(input:disabled) { color: #94a3b8; background: #f8fafc; }
|
|
74
|
+
.toolbar-toggle input { width: 15px; height: 15px; margin: 0; accent-color: var(--accent); }
|
|
75
|
+
.toolbar-toggle svg, .visual-level-field svg { display: none; width: 16px; height: 16px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
|
|
76
|
+
.filter-count { min-width: 16px; padding-left: 2px; color: var(--accent); font-variant-numeric: tabular-nums; }
|
|
77
|
+
.translation-group[hidden], .table-group[hidden] { display: none; }
|
|
78
|
+
.toolbar-field { display: grid; gap: 4px; color: #52617c; font-size: 11px; font-weight: 750; white-space: nowrap; }
|
|
79
|
+
.toolbar-field > span { color: #52617c; font-size: 11px; font-weight: 750; }
|
|
80
|
+
.toolbar-field input { height: 38px; border: 1px solid #c8cfdd; border-radius: 8px; color: var(--ink); background: #fff; }
|
|
81
|
+
.visual-level-field input { width: 86px; padding: 8px 9px; }
|
|
82
|
+
.usage-cell { min-width: 170px; font-size: 12px; }
|
|
83
|
+
.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; }
|
|
84
|
+
.usage-trigger:hover, .usage-trigger[aria-expanded="true"] { background: #f1f4fb; }
|
|
85
|
+
.usage-trigger-action { color: #52617c; font-size: 10px; font-weight: 750; text-decoration: underline; text-underline-offset: 2px; }
|
|
86
|
+
.usage-trigger.usage-used { color: #15803d; }
|
|
87
|
+
.usage-trigger.usage-unreferenced { color: var(--danger); }
|
|
88
|
+
.usage-trigger.usage-uncertain { color: #b45309; }
|
|
89
|
+
.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; }
|
|
90
|
+
.usage-popover strong { margin-bottom: 2px; }
|
|
91
|
+
.usage-popover code { display: block; overflow-wrap: anywhere; color: #3047b3; white-space: normal; }
|
|
92
|
+
.usage-popover-uncertain { border-color: #f0c078; }
|
|
93
|
+
.usage-popover-unreferenced { border-color: #f2b8b5; }
|
|
94
|
+
.usage-empty { color: var(--muted); }
|
|
95
|
+
.review-removed-row { color: #9f1239; background: #fff1f2; }
|
|
96
|
+
.review-removed-row td { border-color: #fecdd3; }
|
|
97
|
+
.review-removed-row .key { color: #881337; font-weight: 800; text-decoration: line-through; text-decoration-color: #fb7185; }
|
|
98
|
+
.review-removed-value, .base-removed { color: #be123c; font-weight: 800; }
|
|
99
|
+
.key-content { display: flex; align-items: center; justify-content: flex-start; gap: 8px; }
|
|
100
|
+
.key-content > span { min-width: 0; overflow-wrap: anywhere; }
|
|
101
|
+
.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; }
|
|
102
|
+
.remove-key-button:hover, .remove-key-button:focus-visible { border-color: #f2b8b5; color: var(--danger); background: #fff1f2; opacity: 1; outline: none; }
|
|
103
|
+
.remove-key-button-unreferenced { border-color: #f2b8b5; color: var(--danger); background: #fff1f2; opacity: 1; }
|
|
61
104
|
.export-preview-feedback { min-height: 21px; margin-top: 5px; font-size: 13px; }
|
|
62
105
|
.export-preview-feedback.error { color: var(--danger); }
|
|
63
106
|
.export-preview-feedback.warning { color: #b45309; font-weight: 700; }
|
|
@@ -82,9 +125,9 @@ h2 { margin-bottom: 4px; font-size: 18px; }
|
|
|
82
125
|
.secondary-button { border: 1px solid #c8cfdd; color: #4c5770; background: #fff; white-space: nowrap; }
|
|
83
126
|
.secondary-button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
|
84
127
|
.secondary-button:disabled { cursor: default; opacity: .55; }
|
|
85
|
-
button
|
|
128
|
+
button { display: flex; align-items: center; gap: 8px; }
|
|
86
129
|
button { cursor: pointer; border-radius: 9px; font-weight: 700; }
|
|
87
|
-
.search-field > span { color: #
|
|
130
|
+
.search-field > span { color: #52617c; font-size: 11px; font-weight: 750; }
|
|
88
131
|
textarea, .search-field input { width: 100%; border: 1px solid #c8cfdd; border-radius: 9px; color: var(--ink); background: #fff; outline: none; }
|
|
89
132
|
textarea:focus, .search-field input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(79, 70, 229, .14); }
|
|
90
133
|
textarea { min-height: 120px; padding: 12px; resize: vertical; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
@@ -93,12 +136,17 @@ button { border: 0; padding: 10px 14px; color: #fff; background: var(--accent);
|
|
|
93
136
|
.feedback.error { color: var(--danger); }
|
|
94
137
|
|
|
95
138
|
.report { overflow: hidden; }
|
|
96
|
-
.report-header {
|
|
139
|
+
.report-header { display: grid; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--line); }
|
|
140
|
+
.report-heading { display: flex; min-width: 0; flex-wrap: wrap; align-items: baseline; gap: 0; }
|
|
141
|
+
.report-heading h2 { margin: 0; font-size: 16px; white-space: nowrap; }
|
|
142
|
+
.report-heading p { display: inline; margin: 0; font-size: 12px; white-space: nowrap; }
|
|
143
|
+
.report-heading p:not(:empty)::before { content: '·'; margin: 0 7px; color: #a4afc1; }
|
|
144
|
+
.review-summary { color: #596780; }
|
|
97
145
|
.icon-button { display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 1px solid #c8cfdd; color: #4c5770; background: #fff; }
|
|
98
146
|
.icon-button:hover { border-color: var(--accent); color: var(--accent); }
|
|
99
147
|
.icon-button svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
|
|
100
|
-
.search-field { width: min(
|
|
101
|
-
.search-field input { margin-top:
|
|
148
|
+
.search-field { width: min(220px, 100%); }
|
|
149
|
+
.search-field input { margin-top: 0; padding: 8px 10px; }
|
|
102
150
|
.empty-state { padding: 56px 22px; color: var(--muted); text-align: center; }
|
|
103
151
|
.table-wrap { width: 100%; max-width: 100%; overflow-x: auto; }
|
|
104
152
|
.table-group { padding: 18px 22px; }
|
|
@@ -131,11 +179,123 @@ code { color: #4338ca; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Conso
|
|
|
131
179
|
tr[hidden] { display: none; }
|
|
132
180
|
|
|
133
181
|
@media (max-width: 720px) {
|
|
134
|
-
.page { width:
|
|
135
|
-
.
|
|
182
|
+
.page { width: calc(100% - 20px); margin: 8px auto 16px; }
|
|
183
|
+
.app-header { min-height: 44px; }
|
|
184
|
+
.eyebrow { display: none; }
|
|
185
|
+
h1 { font-size: 21px; }
|
|
186
|
+
.report-header { display: grid; }
|
|
187
|
+
.translation-actions { width: 100%; align-items: stretch; flex-direction: column; }
|
|
188
|
+
.filter-box { width: 100%; }
|
|
189
|
+
.filter-box-title { width: 100%; }
|
|
190
|
+
.toolbar-toggle, .toolbar-field { width: 100%; }
|
|
191
|
+
.toolbar-icon-button { width: calc(50% - 5px); }
|
|
192
|
+
.visual-level-field input, .search-field { width: 100%; }
|
|
136
193
|
.export-preview-header { flex-direction: column; }
|
|
137
194
|
.export-diff-details { margin-left: 0; }
|
|
138
195
|
.settings-fields, .level-replacers { grid-template-columns: 1fr; }
|
|
139
196
|
.export-settings-fields, .code-format-fields, .deletion-settings-fields { grid-template-columns: 1fr; }
|
|
140
197
|
.search-field { width: 100%; }
|
|
141
198
|
}
|
|
199
|
+
|
|
200
|
+
@media (max-width: 1050px) {
|
|
201
|
+
.filter-box { flex: 0 1 auto; margin-right: auto; }
|
|
202
|
+
.filter-box-title, .toolbar-toggle > span, .visual-level-field > span { display: none; }
|
|
203
|
+
.toolbar-toggle { position: relative; width: 38px; height: 38px; justify-content: center; padding: 0; }
|
|
204
|
+
.toolbar-toggle input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
205
|
+
.toolbar-toggle svg, .visual-level-field svg { display: block; }
|
|
206
|
+
.visual-level-field { position: relative; display: flex; align-items: center; justify-content: center; width: 66px; height: 38px; padding: 0 7px; border: 1px solid #c8cfdd; border-radius: 9px; background: #fff; }
|
|
207
|
+
.visual-level-field input { width: 30px; height: 30px; padding: 0; border: 0; background: transparent; text-align: center; }
|
|
208
|
+
.search-field { width: min(210px, 32vw); }
|
|
209
|
+
.search-field > span { display: none; }
|
|
210
|
+
.search-field input { height: 38px; }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@media (max-width: 720px) {
|
|
214
|
+
.translation-actions { align-items: flex-end; flex-direction: row; }
|
|
215
|
+
.filter-box { width: auto; }
|
|
216
|
+
.toolbar-toggle { width: 38px; }
|
|
217
|
+
.toolbar-field { width: auto; }
|
|
218
|
+
.toolbar-icon-button { width: 38px; }
|
|
219
|
+
.search-field { width: min(180px, calc(100vw - 210px)); }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.report-header[hidden] { display: none; }
|
|
223
|
+
|
|
224
|
+
.toolbar-icon-button {
|
|
225
|
+
display: inline-grid;
|
|
226
|
+
width: 38px;
|
|
227
|
+
height: 38px;
|
|
228
|
+
flex: 0 0 38px;
|
|
229
|
+
place-items: center;
|
|
230
|
+
padding: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.toolbar-icon-button.is-expanded svg { transform: rotate(180deg); }
|
|
234
|
+
|
|
235
|
+
.toolbar-icon-button svg {
|
|
236
|
+
width: 18px;
|
|
237
|
+
height: 18px;
|
|
238
|
+
fill: none;
|
|
239
|
+
stroke: currentColor;
|
|
240
|
+
stroke-linecap: round;
|
|
241
|
+
stroke-linejoin: round;
|
|
242
|
+
stroke-width: 1.8;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.search-field {
|
|
246
|
+
width: min(260px, 34vw);
|
|
247
|
+
height: 38px;
|
|
248
|
+
padding: 8px 10px;
|
|
249
|
+
border: 1px solid #c8cfdd;
|
|
250
|
+
border-radius: 9px;
|
|
251
|
+
color: var(--ink);
|
|
252
|
+
background: #fff;
|
|
253
|
+
outline: none;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.search-field:focus {
|
|
257
|
+
border-color: var(--accent);
|
|
258
|
+
box-shadow: 0 0 0 3px rgba(79, 70, 229, .14);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.visual-level-field {
|
|
262
|
+
display: inline-flex;
|
|
263
|
+
width: auto;
|
|
264
|
+
min-width: 98px;
|
|
265
|
+
height: 38px;
|
|
266
|
+
align-items: center;
|
|
267
|
+
gap: 6px;
|
|
268
|
+
padding: 0 8px;
|
|
269
|
+
border: 1px solid #c8cfdd;
|
|
270
|
+
border-radius: 9px;
|
|
271
|
+
background: #fff;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.visual-level-field input {
|
|
275
|
+
width: 32px;
|
|
276
|
+
height: 30px;
|
|
277
|
+
padding: 0;
|
|
278
|
+
border: 0;
|
|
279
|
+
background: transparent;
|
|
280
|
+
text-align: center;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@media (max-width: 1050px), (max-aspect-ratio: 1 / 1) {
|
|
284
|
+
.filter-box { margin-right: auto; }
|
|
285
|
+
.toolbar-toggle svg, .visual-level-field svg { display: block; }
|
|
286
|
+
.toolbar-toggle > span, .visual-level-field > span { display: none; }
|
|
287
|
+
.toolbar-toggle { width: 38px; height: 38px; padding: 0; justify-content: center; }
|
|
288
|
+
.toolbar-toggle.toolbar-toggle--with-count { width: 62px; min-width: 62px; gap: 6px; }
|
|
289
|
+
.toolbar-toggle input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
|
290
|
+
.visual-level-field { width: 68px; min-width: 68px; justify-content: center; padding: 0 7px; }
|
|
291
|
+
.visual-level-field input { display: block; }
|
|
292
|
+
.search-field { width: min(230px, 32vw); }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@media (max-width: 720px) {
|
|
296
|
+
.translation-actions { align-items: center; gap: 7px; }
|
|
297
|
+
.toolbar-icon-button, .toolbar-toggle { width: 38px; height: 38px; flex: 0 0 38px; }
|
|
298
|
+
.visual-level-field { width: 68px; min-width: 68px; height: 38px; flex: 0 0 68px; }
|
|
299
|
+
.toolbar-toggle.toolbar-toggle--with-count { width: 62px; min-width: 62px; flex-basis: 62px; }
|
|
300
|
+
.search-field { width: min(180px, calc(100vw - 230px)); }
|
|
301
|
+
}
|
|
@@ -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
|
};
|