@wads.dev/i18n-editor 0.0.1-alpha.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/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +191 -0
- package/dist/core/exportPlan.d.ts +16 -0
- package/dist/core/exportPlan.js +143 -0
- package/dist/core/keyPath.d.ts +13 -0
- package/dist/core/keyPath.js +59 -0
- package/dist/core/moveKey.d.ts +6 -0
- package/dist/core/moveKey.js +32 -0
- package/dist/core/projectApi.d.ts +31 -0
- package/dist/core/projectApi.js +1 -0
- package/dist/core/projectConfig.d.ts +33 -0
- package/dist/core/projectConfig.js +93 -0
- package/dist/core/setStringValue.d.ts +7 -0
- package/dist/core/setStringValue.js +23 -0
- package/dist/core/sourceFiles.d.ts +19 -0
- package/dist/core/sourceFiles.js +406 -0
- package/dist/public/editor.js +1461 -0
- package/dist/public/index.html +179 -0
- package/dist/public/styles.css +141 -0
- package/dist/server/createServer.d.ts +3 -0
- package/dist/server/createServer.js +47 -0
- package/dist/server/exportProject.d.ts +27 -0
- package/dist/server/exportProject.js +277 -0
- package/dist/server/projectContext.d.ts +11 -0
- package/dist/server/projectContext.js +122 -0
- package/package.json +51 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Editor de bundle i18n</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css">
|
|
8
|
+
<script type="module" src="./editor.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<main class="page">
|
|
12
|
+
<header class="page-header">
|
|
13
|
+
<p class="eyebrow">i18n bundle</p>
|
|
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">
|
|
17
|
+
<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
|
+
</button>
|
|
19
|
+
</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
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<section id="project-status" class="project-status loading" role="status" aria-live="polite">
|
|
24
|
+
<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>
|
|
27
|
+
</section>
|
|
28
|
+
|
|
29
|
+
<section id="settings-panel" class="settings-panel" aria-labelledby="settings-title" hidden>
|
|
30
|
+
<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>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="settings-fields">
|
|
35
|
+
<label class="level-field" for="level-count">
|
|
36
|
+
<span>Quantidade de níveis</span>
|
|
37
|
+
<input id="level-count" type="number" min="0" step="1" value="2">
|
|
38
|
+
</label>
|
|
39
|
+
<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">
|
|
42
|
+
</label>
|
|
43
|
+
</div>
|
|
44
|
+
<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>
|
|
47
|
+
<div id="level-imports" class="level-imports"></div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="settings-fields output-fields">
|
|
50
|
+
<label class="level-field" for="translations-directory">
|
|
51
|
+
<span>Pasta de traduções</span>
|
|
52
|
+
<input id="translations-directory" type="text" value="i18n" placeholder="i18n">
|
|
53
|
+
</label>
|
|
54
|
+
<label class="level-field" for="language-file-template">
|
|
55
|
+
<span>Arquivo por idioma</span>
|
|
56
|
+
<input id="language-file-template" type="text" value="{language}.ts" placeholder="{language}.ts">
|
|
57
|
+
</label>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="settings-fields output-fields">
|
|
60
|
+
<label class="level-field level-json-field" for="language-replacer">
|
|
61
|
+
<span>Replacer dos idiomas (JSON)</span>
|
|
62
|
+
<textarea id="language-replacer" spellcheck="false" placeholder='{ "pt": "ptBR" }'></textarea>
|
|
63
|
+
</label>
|
|
64
|
+
</div>
|
|
65
|
+
<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>
|
|
68
|
+
<div class="export-settings-fields">
|
|
69
|
+
<label class="level-field level-json-field" for="import-aliases">
|
|
70
|
+
<span>Aliases de import (JSON)</span>
|
|
71
|
+
<textarea id="import-aliases" spellcheck="false" placeholder='{ "@/": "src/" }'></textarea>
|
|
72
|
+
</label>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="code-format-fields">
|
|
75
|
+
<label class="check-field" for="use-double-quotes">
|
|
76
|
+
<input id="use-double-quotes" type="checkbox">
|
|
77
|
+
<span>Usar aspas duplas nas strings geradas</span>
|
|
78
|
+
</label>
|
|
79
|
+
<label class="check-field" for="use-semicolons">
|
|
80
|
+
<input id="use-semicolons" type="checkbox" checked>
|
|
81
|
+
<span>Usar ponto e vírgula</span>
|
|
82
|
+
</label>
|
|
83
|
+
<label class="check-field" for="use-shorthand-properties">
|
|
84
|
+
<input id="use-shorthand-properties" type="checkbox" checked>
|
|
85
|
+
<span>Usar propriedades shorthand</span>
|
|
86
|
+
</label>
|
|
87
|
+
<label class="check-field" for="use-trailing-commas">
|
|
88
|
+
<input id="use-trailing-commas" type="checkbox" checked>
|
|
89
|
+
<span>Usar vírgula no último elemento</span>
|
|
90
|
+
</label>
|
|
91
|
+
<label class="level-field" for="indentation-character">
|
|
92
|
+
<span>Caractere de indentação</span>
|
|
93
|
+
<select id="indentation-character">
|
|
94
|
+
<option value="space">Espaço</option>
|
|
95
|
+
<option value="tab">Tab</option>
|
|
96
|
+
</select>
|
|
97
|
+
</label>
|
|
98
|
+
<label class="level-field" for="indentation-size">
|
|
99
|
+
<span>Quantidade por nível</span>
|
|
100
|
+
<input id="indentation-size" type="number" min="1" max="8" step="1" value="2">
|
|
101
|
+
</label>
|
|
102
|
+
<label class="level-field" for="print-width">
|
|
103
|
+
<span>Largura máxima da linha</span>
|
|
104
|
+
<input id="print-width" type="number" min="40" max="400" step="1" value="120">
|
|
105
|
+
</label>
|
|
106
|
+
<label class="level-field" for="max-object-inline-items">
|
|
107
|
+
<span>Máximo de propriedades inline</span>
|
|
108
|
+
<input id="max-object-inline-items" type="number" min="0" max="100" step="1" value="4">
|
|
109
|
+
</label>
|
|
110
|
+
<label class="level-field" for="max-array-inline-items">
|
|
111
|
+
<span>Máximo de itens inline</span>
|
|
112
|
+
<input id="max-array-inline-items" type="number" min="0" max="100" step="1" value="8">
|
|
113
|
+
</label>
|
|
114
|
+
<label class="level-field" for="object-layout">
|
|
115
|
+
<span>Layout de objetos</span>
|
|
116
|
+
<select id="object-layout">
|
|
117
|
+
<option value="fit">Inline quando couber</option>
|
|
118
|
+
<option value="multiline">Sempre multiline</option>
|
|
119
|
+
</select>
|
|
120
|
+
</label>
|
|
121
|
+
<label class="level-field" for="array-layout">
|
|
122
|
+
<span>Layout de arrays</span>
|
|
123
|
+
<select id="array-layout">
|
|
124
|
+
<option value="fit">Inline quando couber</option>
|
|
125
|
+
<option value="multiline">Sempre multiline</option>
|
|
126
|
+
</select>
|
|
127
|
+
</label>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<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>
|
|
133
|
+
<div class="deletion-settings-fields">
|
|
134
|
+
<label class="check-field" for="deletion-enabled">
|
|
135
|
+
<input id="deletion-enabled" type="checkbox" checked>
|
|
136
|
+
<span>Detectar e avisar sobre candidatos à exclusão</span>
|
|
137
|
+
</label>
|
|
138
|
+
<label class="level-field" for="ignored-deletion-extensions">
|
|
139
|
+
<span>Extensões ignoradas</span>
|
|
140
|
+
<input id="ignored-deletion-extensions" type="text" placeholder=".md, .txt">
|
|
141
|
+
</label>
|
|
142
|
+
<label class="check-field" for="auto-delete">
|
|
143
|
+
<input id="auto-delete" type="checkbox">
|
|
144
|
+
<span>Permitir exclusão sem <code>--delete</code></span>
|
|
145
|
+
</label>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</section>
|
|
149
|
+
|
|
150
|
+
<section class="report" aria-labelledby="report-title">
|
|
151
|
+
<div class="report-header">
|
|
152
|
+
<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>
|
|
155
|
+
<p id="editor-feedback" class="feedback" role="status" aria-live="polite"></p>
|
|
156
|
+
</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
|
+
</div>
|
|
162
|
+
<div id="empty-state" class="empty-state">Nenhum bundle carregado.</div>
|
|
163
|
+
<div id="table-wrap" class="table-wrap" hidden></div>
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
<section class="export-preview-panel" aria-labelledby="export-preview-title">
|
|
167
|
+
<div class="export-preview-header">
|
|
168
|
+
<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>
|
|
171
|
+
<p id="export-preview-feedback" class="export-preview-feedback" role="status" aria-live="polite"></p>
|
|
172
|
+
</div>
|
|
173
|
+
<button id="check-export-diffs" class="secondary-button" type="button" disabled>Verificar diffs</button>
|
|
174
|
+
</div>
|
|
175
|
+
<div id="export-preview" class="export-preview" aria-live="polite"></div>
|
|
176
|
+
</section>
|
|
177
|
+
</main>
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
--ink: #16213b;
|
|
4
|
+
--muted: #64748b;
|
|
5
|
+
--surface: #ffffff;
|
|
6
|
+
--background: #f4f6fb;
|
|
7
|
+
--line: #dfe5f0;
|
|
8
|
+
--accent: #4f46e5;
|
|
9
|
+
--danger: #b42318;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
* { box-sizing: border-box; }
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
background: var(--background);
|
|
17
|
+
color: var(--ink);
|
|
18
|
+
font: 14px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
button, input, textarea { font: inherit; }
|
|
22
|
+
|
|
23
|
+
.page { width: calc(100% - 40px); margin: 44px auto; }
|
|
24
|
+
.page-header { max-width: 720px; margin-bottom: 28px; }
|
|
25
|
+
.page-title-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
26
|
+
.eyebrow { margin: 0 0 5px; color: var(--accent); font-size: 12px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
|
|
27
|
+
h1, h2, p { margin-top: 0; }
|
|
28
|
+
h1 { margin-bottom: 8px; font-size: clamp(30px, 5vw, 46px); letter-spacing: -.045em; line-height: 1.05; }
|
|
29
|
+
h2 { margin-bottom: 4px; font-size: 18px; }
|
|
30
|
+
.intro, .report-header p { color: var(--muted); }
|
|
31
|
+
|
|
32
|
+
.project-status, .settings-panel, .report, .export-preview-panel { border: 1px solid var(--line); border-radius: 16px; background: var(--surface); box-shadow: 0 16px 40px rgba(24, 39, 75, .06); }
|
|
33
|
+
.project-status { display: flex; align-items: center; gap: 11px; padding: 14px 16px; margin-bottom: 18px; color: var(--muted); }
|
|
34
|
+
.project-status[hidden] { display: none; }
|
|
35
|
+
.project-status.error { color: var(--danger); border-color: #f2b8b5; }
|
|
36
|
+
.project-status button { margin-left: auto; }
|
|
37
|
+
.status-spinner { width: 17px; height: 17px; flex: 0 0 auto; border: 2px solid #d8dcef; border-top-color: var(--accent); border-radius: 50%; animation: status-spin .75s linear infinite; }
|
|
38
|
+
.project-status.error .status-spinner { display: none; }
|
|
39
|
+
@keyframes status-spin { to { transform: rotate(360deg); } }
|
|
40
|
+
.settings-panel { display: grid; gap: 18px; padding: 16px; margin-bottom: 18px; }
|
|
41
|
+
.settings-panel[hidden] { display: none; }
|
|
42
|
+
.settings-panel h2, .settings-panel p { margin-bottom: 2px; }
|
|
43
|
+
.settings-panel p { color: var(--muted); font-size: 13px; }
|
|
44
|
+
.settings-fields, .level-replacers { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
|
45
|
+
.level-imports { display: grid; gap: 12px; margin-top: 12px; }
|
|
46
|
+
.level-import-card { display: grid; gap: 10px; padding: 13px; border: 1px solid var(--line); border-radius: 10px; background: #fafbff; }
|
|
47
|
+
.level-import-card h4 { margin: 0; color: #34405b; font-size: 13px; }
|
|
48
|
+
.level-paths-section, .export-settings-section, .deletion-settings-section { border-top: 1px solid var(--line); padding-top: 16px; }
|
|
49
|
+
.level-paths-section h3, .export-settings-section h3, .deletion-settings-section h3 { margin: 0 0 3px; color: #34405b; font-size: 14px; }
|
|
50
|
+
.export-settings-fields, .deletion-settings-fields { display: grid; grid-template-columns: minmax(260px, 1fr) minmax(220px, 1fr); align-items: end; gap: 14px; margin-top: 12px; }
|
|
51
|
+
.code-format-fields { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); align-items: end; gap: 10px 18px; margin-top: 14px; }
|
|
52
|
+
.check-field { display: flex; align-items: center; gap: 9px; min-height: 40px; color: #34405b; font-size: 13px; font-weight: 700; }
|
|
53
|
+
.check-field input { width: 16px; height: 16px; margin: 0; accent-color: var(--accent); }
|
|
54
|
+
.level-field { display: grid; gap: 5px; min-width: 180px; color: #34405b; font-size: 13px; font-weight: 700; }
|
|
55
|
+
.level-field input, .level-field select { width: 100%; padding: 9px 11px; border: 1px solid #c8cfdd; border-radius: 9px; color: var(--ink); background: #fff; outline: none; }
|
|
56
|
+
.level-json-field textarea { min-height: 92px; }
|
|
57
|
+
.level-field input:focus, .level-field select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(79, 70, 229, .14); }
|
|
58
|
+
.export-preview-panel { margin-top: 18px; overflow: hidden; }
|
|
59
|
+
.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
|
+
.export-preview-header p { margin-bottom: 0; color: var(--muted); }
|
|
61
|
+
.export-preview-feedback { min-height: 21px; margin-top: 5px; font-size: 13px; }
|
|
62
|
+
.export-preview-feedback.error { color: var(--danger); }
|
|
63
|
+
.export-preview-feedback.warning { color: #b45309; font-weight: 700; }
|
|
64
|
+
.export-preview { color: var(--muted); background: #fafbff; }
|
|
65
|
+
.export-preview-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
|
|
66
|
+
.export-preview-item { padding: 10px 16px; border-top: 1px solid #edf0f6; }
|
|
67
|
+
.export-preview-item:first-child { border-top: 0; }
|
|
68
|
+
.export-preview-path { display: flex; align-items: baseline; gap: 10px; }
|
|
69
|
+
.export-preview-status { width: 62px; flex: 0 0 auto; font: 700 11px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; text-transform: uppercase; }
|
|
70
|
+
.export-preview-create .export-preview-status { color: #15803d; }
|
|
71
|
+
.export-preview-modify .export-preview-status { color: #b45309; }
|
|
72
|
+
.export-preview-delete .export-preview-status { color: var(--danger); }
|
|
73
|
+
.export-preview-unchanged .export-preview-status { color: var(--muted); }
|
|
74
|
+
.export-diff-details { margin: 8px 0 0 72px; }
|
|
75
|
+
.export-diff-details > summary { cursor: pointer; color: #4c5770; font-size: 12px; font-weight: 750; }
|
|
76
|
+
.export-file-diff { margin: 8px 0 2px; overflow-x: auto; border: 1px solid var(--line); border-radius: 8px; background: #fff; font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
77
|
+
.export-file-diff > span { display: block; min-width: max-content; padding: 0 10px; white-space: pre; }
|
|
78
|
+
.diff-addition { color: #166534; background: #ecfdf3; }
|
|
79
|
+
.diff-removal { color: #991b1b; background: #fff1f2; }
|
|
80
|
+
.diff-file-header, .diff-range { color: #92400e; background: #fffbeb; }
|
|
81
|
+
.diff-context { color: #475569; }
|
|
82
|
+
.secondary-button { border: 1px solid #c8cfdd; color: #4c5770; background: #fff; white-space: nowrap; }
|
|
83
|
+
.secondary-button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
|
84
|
+
.secondary-button:disabled { cursor: default; opacity: .55; }
|
|
85
|
+
button, .report-header { display: flex; align-items: center; gap: 14px; }
|
|
86
|
+
button { cursor: pointer; border-radius: 9px; font-weight: 700; }
|
|
87
|
+
.search-field > span { color: #34405b; font-size: 13px; font-weight: 700; }
|
|
88
|
+
textarea, .search-field input { width: 100%; border: 1px solid #c8cfdd; border-radius: 9px; color: var(--ink); background: #fff; outline: none; }
|
|
89
|
+
textarea:focus, .search-field input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(79, 70, 229, .14); }
|
|
90
|
+
textarea { min-height: 120px; padding: 12px; resize: vertical; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
91
|
+
button { border: 0; padding: 10px 14px; color: #fff; background: var(--accent); }
|
|
92
|
+
.feedback { margin: 0; color: var(--muted); }
|
|
93
|
+
.feedback.error { color: var(--danger); }
|
|
94
|
+
|
|
95
|
+
.report { overflow: hidden; }
|
|
96
|
+
.report-header { justify-content: space-between; padding: 20px 22px; border-bottom: 1px solid var(--line); }
|
|
97
|
+
.icon-button { display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 1px solid #c8cfdd; color: #4c5770; background: #fff; }
|
|
98
|
+
.icon-button:hover { border-color: var(--accent); color: var(--accent); }
|
|
99
|
+
.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(340px, 100%); }
|
|
101
|
+
.search-field input { margin-top: 5px; padding: 9px 11px; }
|
|
102
|
+
.empty-state { padding: 56px 22px; color: var(--muted); text-align: center; }
|
|
103
|
+
.table-wrap { width: 100%; max-width: 100%; overflow-x: auto; }
|
|
104
|
+
.table-group { padding: 18px 22px; }
|
|
105
|
+
.table-group + .table-group { border-top: 1px solid var(--line); }
|
|
106
|
+
.table-group-title { margin: 0 0 9px; color: #4b5872; font-size: 13px; font-weight: 800; letter-spacing: .04em; text-align: left; text-transform: uppercase; }
|
|
107
|
+
.translation-group { border-top: 1px solid var(--line); }
|
|
108
|
+
.translation-group > summary { display: flex; cursor: pointer; align-items: center; justify-content: flex-start; gap: 12px; padding: 15px 22px; color: #303b57; font-weight: 800; list-style: none; text-align: left; }
|
|
109
|
+
.translation-group > summary::-webkit-details-marker { display: none; }
|
|
110
|
+
.translation-group > summary::before { content: '›'; color: var(--accent); font-size: 22px; line-height: 1; transition: transform .15s ease; }
|
|
111
|
+
.translation-group[open] > summary::before { transform: rotate(90deg); }
|
|
112
|
+
.group-count { margin-left: auto; color: var(--muted); font-size: 12px; font-weight: 650; }
|
|
113
|
+
.translation-group-content { border-top: 1px solid var(--line); background: #fcfcff; }
|
|
114
|
+
.translation-group-content > .translation-group { margin-left: 22px; border-left: 2px solid #d9ddf4; }
|
|
115
|
+
.translation-group-content > .translation-group > summary { padding-left: 16px; }
|
|
116
|
+
table { width: max-content; min-width: 100%; border-collapse: collapse; }
|
|
117
|
+
th { padding: 11px 14px; background: #eef1f7; color: #42506b; text-align: left; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
|
|
118
|
+
td { min-width: 150px; max-width: 380px; padding: 12px 14px; border-top: 1px solid var(--line); vertical-align: top; overflow-wrap: anywhere; }
|
|
119
|
+
tbody tr:hover td { background: #fafbff; }
|
|
120
|
+
td.group { min-width: 115px; color: #4b5872; font-weight: 700; }
|
|
121
|
+
td.key { min-width: 210px; cursor: pointer; }
|
|
122
|
+
.language-header, .language-value { min-width: 260px; }
|
|
123
|
+
.inline-value-input { width: 100%; min-width: 0; padding: 7px 9px; border: 1px solid var(--accent); border-radius: 6px; color: var(--ink); background: #fff; outline: 2px solid rgba(79, 70, 229, .14); }
|
|
124
|
+
code { color: #4338ca; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
125
|
+
.function-value { color: #9f4c00; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
126
|
+
.base-value { min-width: 140px; font-weight: 750; }
|
|
127
|
+
.base-string { color: #0f766e; }
|
|
128
|
+
.base-function { color: #9f4c00; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
129
|
+
.base-inconsistent { color: var(--danger); }
|
|
130
|
+
.base-missing { color: var(--muted); }
|
|
131
|
+
tr[hidden] { display: none; }
|
|
132
|
+
|
|
133
|
+
@media (max-width: 720px) {
|
|
134
|
+
.page { width: min(100% - 24px, 1500px); margin: 24px auto; }
|
|
135
|
+
.report-header { align-items: flex-start; flex-direction: column; }
|
|
136
|
+
.export-preview-header { flex-direction: column; }
|
|
137
|
+
.export-diff-details { margin-left: 0; }
|
|
138
|
+
.settings-fields, .level-replacers { grid-template-columns: 1fr; }
|
|
139
|
+
.export-settings-fields, .code-format-fields, .deletion-settings-fields { grid-template-columns: 1fr; }
|
|
140
|
+
.search-field { width: 100%; }
|
|
141
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fastifyStatic from '@fastify/static';
|
|
2
|
+
import { assertBundle } from '@wads.dev/i18n-ts/bundle';
|
|
3
|
+
import Fastify from 'fastify';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { normalizeEditorProjectConfig } from '../core/projectConfig.js';
|
|
6
|
+
import { planProjectExport } from './exportProject.js';
|
|
7
|
+
import { createProjectContext } from './projectContext.js';
|
|
8
|
+
export async function createServer(options = {}) {
|
|
9
|
+
const server = Fastify({ logger: false });
|
|
10
|
+
const publicDirectory = fileURLToPath(new URL('../public/', import.meta.url));
|
|
11
|
+
const project = createProjectContext(options);
|
|
12
|
+
server.get('/api/project', async (_request, reply) => {
|
|
13
|
+
try {
|
|
14
|
+
return await project.getInfo();
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return reply.code(500).send({ error: error instanceof Error ? error.message : String(error) });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
server.post('/api/bundle', async (_request, reply) => {
|
|
21
|
+
try {
|
|
22
|
+
return await project.generateBundle();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return reply.code(500).send({ error: error instanceof Error ? error.message : String(error) });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
server.post('/api/export-preview', { bodyLimit: 20 * 1024 * 1024 }, async (request, reply) => {
|
|
29
|
+
try {
|
|
30
|
+
const plan = await planProjectExport(options, {
|
|
31
|
+
bundle: assertBundle(request.body?.bundle),
|
|
32
|
+
config: normalizeEditorProjectConfig(request.body?.config),
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
changes: plan.changes.map(({ kind, path, status, diff }) => ({ kind, path, status, diff })),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
await server.register(fastifyStatic, {
|
|
43
|
+
root: publicDirectory,
|
|
44
|
+
prefix: '/',
|
|
45
|
+
});
|
|
46
|
+
return server;
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { I18nBundle } from '@wads.dev/i18n-ts/bundle';
|
|
2
|
+
import type { EditorProjectConfig, I18nDeletionConfig } from '../core/projectConfig.js';
|
|
3
|
+
import { type GeneratedSourceFile } from '../core/sourceFiles.js';
|
|
4
|
+
import { type ProjectContextOptions } from './projectContext.js';
|
|
5
|
+
export type ProjectExportChange = {
|
|
6
|
+
kind: GeneratedSourceFile['kind'] | 'obsolete';
|
|
7
|
+
path: string;
|
|
8
|
+
absolutePath: string;
|
|
9
|
+
status: 'create' | 'modify' | 'unchanged' | 'delete';
|
|
10
|
+
content?: string;
|
|
11
|
+
diff?: string;
|
|
12
|
+
};
|
|
13
|
+
export type ProjectExportPlan = {
|
|
14
|
+
projectDirectory: string;
|
|
15
|
+
bundlePath: string;
|
|
16
|
+
changes: ProjectExportChange[];
|
|
17
|
+
managedDirectories: string[];
|
|
18
|
+
deletion: false | I18nDeletionConfig;
|
|
19
|
+
};
|
|
20
|
+
export type ProjectExportState = {
|
|
21
|
+
bundle: I18nBundle;
|
|
22
|
+
config: EditorProjectConfig;
|
|
23
|
+
};
|
|
24
|
+
export declare function planProjectExport(options?: ProjectContextOptions, state?: ProjectExportState): Promise<ProjectExportPlan>;
|
|
25
|
+
export declare function applyProjectExport(plan: ProjectExportPlan, options?: {
|
|
26
|
+
deleteObsolete?: boolean;
|
|
27
|
+
}): Promise<void>;
|