@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.
@@ -1,147 +1,154 @@
1
1
  <!doctype html>
2
- <html lang="pt-BR">
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>Editor de bundle i18n</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>Visualizador de traduções</h1>
16
- <button id="toggle-settings-panel" class="icon-button" type="button" aria-label="Mostrar configurações" aria-expanded="false" title="Mostrar configurações">
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">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>
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">Conectando ao projeto…</span>
26
- <button id="retry-project-load" type="button" hidden>Tentar novamente</button>
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">Configurações do projeto</h2>
32
- <p>Defina a estrutura das chaves e os caminhos que uma futura exportação deverá gerar a partir da raiz do projeto.</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>Quantidade de níveis</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>Nomes dos níveis</span>
41
- <input id="level-names" type="text" value="Módulo, Feature" placeholder="Módulo, Feature, Subfeature">
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 por nível</h3>
46
- <p>O nível 0 representa a raiz e não consome uma parte da chave. Use <code>{value}</code> nos demais níveis. Replacers aceitam JSON plano ou aninhado.</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>Pasta de traduções</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>Arquivo por idioma</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>Replacer dos idiomas (JSON)</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>Exportação</h3>
67
- <p>Configure a resolução de caminhos e a formatação dos arquivos TypeScript gerados.</p>
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>Aliases de import (JSON)</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>Usar aspas duplas nas strings geradas</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>Usar ponto e vírgula</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>Usar propriedades shorthand</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>Usar vírgula no último elemento</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>Caractere de indentação</span>
99
+ <span data-i18n="settings.indentationCharacter">Indentation character</span>
93
100
  <select id="indentation-character">
94
- <option value="space">Espaço</option>
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>Quantidade por nível</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>Largura máxima da linha</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>Máximo de propriedades inline</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>Máximo de itens inline</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>Layout de objetos</span>
122
+ <span data-i18n="settings.objectLayout">Object layout</span>
116
123
  <select id="object-layout">
117
- <option value="fit">Inline quando couber</option>
118
- <option value="multiline">Sempre multiline</option>
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>Layout de arrays</span>
129
+ <span data-i18n="settings.arrayLayout">Array layout</span>
123
130
  <select id="array-layout">
124
- <option value="fit">Inline quando couber</option>
125
- <option value="multiline">Sempre multiline</option>
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>Arquivos divergentes</h3>
132
- <p>Arquivos dentro das pastas de tradução que não aparecem no plano são candidatos à exclusão.</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>Detectar e avisar sobre candidatos à exclusão</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>Extensões ignoradas</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>Permitir exclusão sem <code>--delete</code></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">Tabela de traduções</h2>
154
- <p id="bundle-summary">Carregue um bundle para visualizar as traduções.</p>
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
- <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>
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">Nenhum bundle carregado.</div>
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">Prévia dos arquivos gerados</h2>
170
- <p>Esta é uma simulação em tempo real. Verifique os diffs quando quiser comparar o resultado com os arquivos atuais do projeto.</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
- <button id="check-export-diffs" class="secondary-button" type="button" disabled>Verificar diffs</button>
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>
@@ -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 publicDirectory = fileURLToPath(new URL('../public/', import.meta.url));
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<void>;
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
  };
@@ -118,5 +118,5 @@ export function createProjectContext(options = {}) {
118
118
  });
119
119
  return generation;
120
120
  }
121
- return { getInfo, generateBundle };
121
+ return { projectDirectory, getInfo, generateBundle };
122
122
  }
@@ -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 {};