@strapi-ai/translator 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/admin/SettingsPage-C5nm32bD.mjs +466 -0
  4. package/dist/admin/SettingsPage-CD90jfwT.js +466 -0
  5. package/dist/admin/en-CU6ocWlA.mjs +97 -0
  6. package/dist/admin/en-I9cvzxfF.js +97 -0
  7. package/dist/admin/index-CS0LCA35.mjs +1694 -0
  8. package/dist/admin/index-DQ3-ym--.js +1693 -0
  9. package/dist/admin/index.js +4 -0
  10. package/dist/admin/index.mjs +4 -0
  11. package/dist/admin/src/components/DiffView.d.ts +31 -0
  12. package/dist/admin/src/components/Initializer.d.ts +3 -0
  13. package/dist/admin/src/components/PluginIcon.d.ts +4 -0
  14. package/dist/admin/src/components/ProgressBar.d.ts +7 -0
  15. package/dist/admin/src/components/TranslationModal.d.ts +23 -0
  16. package/dist/admin/src/components/TranslationPanel.d.ts +4 -0
  17. package/dist/admin/src/components/TranslationWorkflow.d.ts +2 -0
  18. package/dist/admin/src/components/settings/ProviderConnectionFields.d.ts +12 -0
  19. package/dist/admin/src/components/settings/TranslationSettingsFields.d.ts +7 -0
  20. package/dist/admin/src/hooks/useProviderModels.d.ts +13 -0
  21. package/dist/admin/src/hooks/useTranslationConfig.d.ts +8 -0
  22. package/dist/admin/src/index.d.ts +12 -0
  23. package/dist/admin/src/pages/SettingsPage.d.ts +2 -0
  24. package/dist/admin/src/pluginId.d.ts +1 -0
  25. package/dist/admin/src/utils/clampNumber.d.ts +1 -0
  26. package/dist/admin/src/utils/configPayload.d.ts +23 -0
  27. package/dist/admin/src/utils/contentManagerLocale.d.ts +1 -0
  28. package/dist/admin/src/utils/getTranslation.d.ts +1 -0
  29. package/dist/admin/src/utils/providerDefaults.d.ts +10 -0
  30. package/dist/admin/src/utils/sse.d.ts +2 -0
  31. package/dist/admin/src/utils/translatableFields.d.ts +3 -0
  32. package/dist/admin/src/utils/translationPatch.d.ts +13 -0
  33. package/dist/server/index.js +1217 -0
  34. package/dist/server/index.mjs +1217 -0
  35. package/dist/server/src/application/assemble-translation-patch.d.ts +3 -0
  36. package/dist/server/src/application/extract-translatable-units.d.ts +9 -0
  37. package/dist/server/src/application/translate-entry.d.ts +17 -0
  38. package/dist/server/src/application/translation-provider.d.ts +33 -0
  39. package/dist/server/src/bootstrap.d.ts +5 -0
  40. package/dist/server/src/config/index.d.ts +5 -0
  41. package/dist/server/src/controllers/index.d.ts +27 -0
  42. package/dist/server/src/controllers/translation.controller.d.ts +28 -0
  43. package/dist/server/src/domain/field-path.d.ts +6 -0
  44. package/dist/server/src/domain/translation-config.d.ts +27 -0
  45. package/dist/server/src/domain/translation-event.d.ts +59 -0
  46. package/dist/server/src/domain/translation-patch.d.ts +11 -0
  47. package/dist/server/src/domain/translation-unit.d.ts +28 -0
  48. package/dist/server/src/domain/value-codecs/index.d.ts +3 -0
  49. package/dist/server/src/domain/value-codecs/string-codec.d.ts +2 -0
  50. package/dist/server/src/domain/value-codecs/structured-value-codec.d.ts +3 -0
  51. package/dist/server/src/domain/value-codecs/types.d.ts +11 -0
  52. package/dist/server/src/index.d.ts +64 -0
  53. package/dist/server/src/infrastructure/llm/chat-completion-client.d.ts +18 -0
  54. package/dist/server/src/infrastructure/llm/llm-translation-provider.d.ts +15 -0
  55. package/dist/server/src/infrastructure/llm/model-list-client.d.ts +19 -0
  56. package/dist/server/src/infrastructure/llm/openai-chat-completion-client.d.ts +6 -0
  57. package/dist/server/src/infrastructure/strapi/reconcile-schema.d.ts +7 -0
  58. package/dist/server/src/infrastructure/strapi/secret-cipher.d.ts +8 -0
  59. package/dist/server/src/infrastructure/strapi/translation-config-repository.d.ts +16 -0
  60. package/dist/server/src/infrastructure/strapi/translation-session-service.d.ts +37 -0
  61. package/dist/server/src/routes/index.d.ts +19 -0
  62. package/dist/server/src/services/index.d.ts +7 -0
  63. package/package.json +87 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 brighill
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # AI Translator for Strapi
2
+
3
+ Just another vibe-coded AI translator plugin for Strapi 5. Translates localized content with any OpenAI-compatible model and shows you a
4
+ streamed diff before anything touches your entry.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install @strapi-ai/translator
10
+ ```
11
+
12
+ ```ts
13
+ // config/plugins.ts
14
+ export default {
15
+ 'ai-translator': { enabled: true },
16
+ };
17
+ ```
@@ -0,0 +1,466 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef } from "react";
3
+ import { Typography, Field, TextInput, Box, Button, Combobox, ComboboxOption, Textarea, Flex, NumberInput, Main } from "@strapi/design-system";
4
+ import { useIntl } from "react-intl";
5
+ import { P as PLUGIN_ID, g as getTranslation } from "./index-CS0LCA35.mjs";
6
+ import { Earth } from "@strapi/icons";
7
+ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
8
+ const PluginIcon = (props) => /* @__PURE__ */ jsx(Earth, { ...props });
9
+ const DEFAULT_SYSTEM_PROMPT = "You are a CMS content translator. Output only the translated text, with no explanations, no quotes, and no metadata. Preserve formatting and markup exactly: Markdown, HTML, custom tags, links, images. Leave code blocks, URLs, query parameters, variables, and placeholders unchanged. Keep brand names, product names, and currency values in their original form. Translate meaning by meaning, using natural, concise wording and the standard terminology of the target locale.";
10
+ const DEFAULT_PROVIDER_BASE_URL = "";
11
+ const DEFAULT_PROVIDER_MODEL = "";
12
+ const DEFAULT_PROVIDER_TEMPERATURE = 0.3;
13
+ const MAX_PROVIDER_TEMPERATURE = 2;
14
+ const MIN_PROVIDER_TEMPERATURE = 0;
15
+ const DEFAULT_MAX_TOKENS = 0;
16
+ const DEFAULT_MAX_RETRIES = 3;
17
+ const DEFAULT_RETRY_BASE_DELAY_MS = 1e3;
18
+ const DEFAULT_RETRY_MAX_DELAY_MS = 3e4;
19
+ const toEditablePayload = (settings) => {
20
+ const { apiKey, apiKeyConfigured, apiKeyPreview, ...editable } = settings;
21
+ const trimmedApiKey = apiKey.trim();
22
+ return trimmedApiKey ? { ...editable, apiKey: trimmedApiKey } : editable;
23
+ };
24
+ const DEFAULT_CONFIG = {
25
+ apiKey: "",
26
+ baseUrl: DEFAULT_PROVIDER_BASE_URL,
27
+ model: DEFAULT_PROVIDER_MODEL,
28
+ fallbackModel: "",
29
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
30
+ temperature: DEFAULT_PROVIDER_TEMPERATURE,
31
+ maxTokens: DEFAULT_MAX_TOKENS,
32
+ maxRetries: DEFAULT_MAX_RETRIES,
33
+ retryBaseDelayMs: DEFAULT_RETRY_BASE_DELAY_MS,
34
+ retryMaxDelayMs: DEFAULT_RETRY_MAX_DELAY_MS,
35
+ apiKeyConfigured: false,
36
+ apiKeyPreview: ""
37
+ };
38
+ const useTranslationConfig = () => {
39
+ const [config, setConfig] = useState({ ...DEFAULT_CONFIG });
40
+ const { get, post } = useFetchClient();
41
+ const { toggleNotification } = useNotification();
42
+ const { formatMessage } = useIntl();
43
+ const applyPublicConfig = (settings) => {
44
+ setConfig({ ...DEFAULT_CONFIG, ...settings, apiKey: "" });
45
+ };
46
+ const notifySaveFailed = (error) => {
47
+ const serverError = error?.response?.data?.error;
48
+ const serverMessage = typeof serverError === "string" ? serverError : serverError?.message;
49
+ toggleNotification({
50
+ type: "danger",
51
+ message: serverMessage || formatMessage({
52
+ id: getTranslation("notification.config.save_failed"),
53
+ defaultMessage: "Could not save settings."
54
+ })
55
+ });
56
+ };
57
+ useEffect(() => {
58
+ const fetchData = async () => {
59
+ const response = await get(`/${PLUGIN_ID}/config`);
60
+ applyPublicConfig(response.data);
61
+ };
62
+ fetchData().catch(() => {
63
+ });
64
+ }, []);
65
+ const save = async (next) => {
66
+ try {
67
+ const response = await post(
68
+ `/${PLUGIN_ID}/config`,
69
+ toEditablePayload(next)
70
+ );
71
+ applyPublicConfig(response.data);
72
+ toggleNotification({
73
+ type: "success",
74
+ message: formatMessage({
75
+ id: getTranslation("notification.config.saved"),
76
+ defaultMessage: "Settings saved."
77
+ })
78
+ });
79
+ } catch (error) {
80
+ notifySaveFailed(error);
81
+ }
82
+ };
83
+ const clearApiKey = async () => {
84
+ try {
85
+ const response = await post(`/${PLUGIN_ID}/config`, { apiKey: null });
86
+ applyPublicConfig(response.data);
87
+ toggleNotification({
88
+ type: "success",
89
+ message: formatMessage({
90
+ id: getTranslation("notification.config.key_removed"),
91
+ defaultMessage: "Saved API key removed."
92
+ })
93
+ });
94
+ } catch (error) {
95
+ notifySaveFailed(error);
96
+ }
97
+ };
98
+ const restore = async () => {
99
+ try {
100
+ const response = await post(
101
+ `/${PLUGIN_ID}/config`,
102
+ toEditablePayload(DEFAULT_CONFIG)
103
+ );
104
+ applyPublicConfig(response.data);
105
+ toggleNotification({
106
+ type: "success",
107
+ message: formatMessage({
108
+ id: getTranslation("notification.config.restored"),
109
+ defaultMessage: "Default settings restored."
110
+ })
111
+ });
112
+ } catch (error) {
113
+ notifySaveFailed(error);
114
+ }
115
+ };
116
+ const updateField = (field, value) => {
117
+ setConfig((previous) => ({ ...previous, [field]: value }));
118
+ };
119
+ return { config, save, restore, clearApiKey, updateField };
120
+ };
121
+ const useProviderModels = () => {
122
+ const [models, setModels] = useState([]);
123
+ const [error, setError] = useState(null);
124
+ const { formatMessage } = useIntl();
125
+ const { post } = useFetchClient();
126
+ const requestSeq = useRef(0);
127
+ const load = async (draft = {}) => {
128
+ const seq = ++requestSeq.current;
129
+ setError(null);
130
+ try {
131
+ const response = await post(`/${PLUGIN_ID}/models`, {
132
+ apiKey: draft.apiKey,
133
+ baseUrl: draft.baseUrl
134
+ });
135
+ const next = Array.isArray(response.data?.models) ? response.data.models.filter(
136
+ (model) => model && typeof model.id === "string" && model.id.trim() !== ""
137
+ ) : [];
138
+ if (next.length === 0) {
139
+ throw new Error(
140
+ formatMessage({
141
+ id: getTranslation("settings.form.model_empty"),
142
+ defaultMessage: "No models were returned by the provider."
143
+ })
144
+ );
145
+ }
146
+ if (seq !== requestSeq.current) return;
147
+ setModels(next);
148
+ } catch (e) {
149
+ if (seq !== requestSeq.current) return;
150
+ setModels([]);
151
+ const serverError = e?.response?.data?.error;
152
+ setError(
153
+ serverError?.message || (typeof serverError === "string" ? serverError : "") || e?.message || formatMessage({
154
+ id: getTranslation("settings.form.model_load_failed"),
155
+ defaultMessage: "Failed to load provider models."
156
+ })
157
+ );
158
+ }
159
+ };
160
+ return { models, error, load };
161
+ };
162
+ const ProviderConnectionFields = ({
163
+ config,
164
+ updateField,
165
+ onClearApiKey,
166
+ providerModels,
167
+ providerModelsError,
168
+ onLoadModels
169
+ }) => {
170
+ const { formatMessage } = useIntl();
171
+ const trimmedBaseUrl = config.baseUrl.trim().replace(/\/+$/, "");
172
+ const baseUrlMissingVersion = trimmedBaseUrl !== "" && !/\/v\d+$/.test(trimmedBaseUrl);
173
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
174
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h2", marginBottom: 4, children: formatMessage({
175
+ id: getTranslation("settings.section.connection"),
176
+ defaultMessage: "Provider Connection"
177
+ }) }),
178
+ /* @__PURE__ */ jsxs(
179
+ Field.Root,
180
+ {
181
+ id: "api_key",
182
+ hint: config.apiKeyConfigured ? formatMessage(
183
+ {
184
+ id: getTranslation("settings.form.api_key_hint_configured"),
185
+ defaultMessage: "Currently saved: {preview}. Paste a new key to replace it, or leave blank to keep the current one."
186
+ },
187
+ { preview: config.apiKeyPreview }
188
+ ) : formatMessage({
189
+ id: getTranslation("settings.form.api_key_hint_empty"),
190
+ defaultMessage: "No key saved. Paste an API key to enable translation. Saved keys are never returned by the API."
191
+ }),
192
+ children: [
193
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
194
+ id: getTranslation("settings.form.api_key"),
195
+ defaultMessage: "API Key"
196
+ }) }),
197
+ /* @__PURE__ */ jsx(
198
+ TextInput,
199
+ {
200
+ id: "api_key",
201
+ value: config.apiKey,
202
+ onChange: (e) => updateField("apiKey", e.target.value),
203
+ onBlur: onLoadModels,
204
+ name: "api_key",
205
+ type: "password",
206
+ autoComplete: "new-password",
207
+ placeholder: config.apiKeyConfigured ? formatMessage({
208
+ id: getTranslation("settings.form.api_key_placeholder_configured"),
209
+ defaultMessage: "Configured - enter a new key to replace it"
210
+ }) : formatMessage({
211
+ id: getTranslation("settings.form.api_key_placeholder_empty"),
212
+ defaultMessage: "Not configured"
213
+ })
214
+ }
215
+ ),
216
+ /* @__PURE__ */ jsx(Field.Error, {}),
217
+ /* @__PURE__ */ jsx(Field.Hint, {})
218
+ ]
219
+ }
220
+ ),
221
+ config.apiKeyConfigured ? /* @__PURE__ */ jsx(Box, { marginTop: 2, children: /* @__PURE__ */ jsx(Button, { variant: "danger-light", type: "button", size: "S", onClick: onClearApiKey, children: formatMessage({
222
+ id: getTranslation("settings.form.api_key_remove"),
223
+ defaultMessage: "Remove saved key"
224
+ }) }) }) : null,
225
+ /* @__PURE__ */ jsxs(
226
+ Field.Root,
227
+ {
228
+ id: "llm_base_url",
229
+ marginTop: 6,
230
+ hint: baseUrlMissingVersion ? formatMessage({
231
+ id: getTranslation("settings.form.base_url_hint_missing_version"),
232
+ defaultMessage: "Most OpenAI-compatible APIs require a version segment (e.g. ending with /v1)."
233
+ }) : formatMessage({
234
+ id: getTranslation("settings.form.base_url_hint"),
235
+ defaultMessage: "API endpoint for the model provider."
236
+ }),
237
+ children: [
238
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
239
+ id: getTranslation("settings.form.base_url"),
240
+ defaultMessage: "Base URL"
241
+ }) }),
242
+ /* @__PURE__ */ jsx(
243
+ TextInput,
244
+ {
245
+ id: "llm_base_url",
246
+ value: config.baseUrl,
247
+ onChange: (e) => updateField("baseUrl", e.target.value),
248
+ onBlur: onLoadModels,
249
+ name: "llm_base_url",
250
+ placeholder: "https://api.openai.com/v1"
251
+ }
252
+ ),
253
+ /* @__PURE__ */ jsx(Field.Error, {}),
254
+ /* @__PURE__ */ jsx(Field.Hint, {})
255
+ ]
256
+ }
257
+ ),
258
+ /* @__PURE__ */ jsxs(
259
+ Field.Root,
260
+ {
261
+ id: "llm_model",
262
+ marginTop: 6,
263
+ hint: providerModelsError || formatMessage({
264
+ id: getTranslation("settings.form.model_hint"),
265
+ defaultMessage: "Primary model for translations."
266
+ }),
267
+ children: [
268
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
269
+ id: getTranslation("settings.form.model"),
270
+ defaultMessage: "Model"
271
+ }) }),
272
+ /* @__PURE__ */ jsx(
273
+ Combobox,
274
+ {
275
+ id: "llm_model",
276
+ value: config.model,
277
+ textValue: config.model,
278
+ onChange: (value) => updateField("model", value ?? ""),
279
+ onTextValueChange: (value) => updateField("model", value),
280
+ onClear: () => updateField("model", ""),
281
+ autocomplete: { type: "list", filter: "contains" },
282
+ allowCustomValue: true,
283
+ name: "llm_model",
284
+ children: providerModels.map((model) => /* @__PURE__ */ jsx(ComboboxOption, { value: model.id, children: model.label }, model.id))
285
+ }
286
+ ),
287
+ /* @__PURE__ */ jsx(Field.Error, {}),
288
+ /* @__PURE__ */ jsx(Field.Hint, {})
289
+ ]
290
+ }
291
+ )
292
+ ] });
293
+ };
294
+ const clampNumber = (value, defaultVal, min, max) => {
295
+ if (value === void 0) return defaultVal;
296
+ if (value < min) return min;
297
+ if (value > max) return max;
298
+ return value;
299
+ };
300
+ const TranslationSettingsFields = ({
301
+ config,
302
+ updateField
303
+ }) => {
304
+ const { formatMessage } = useIntl();
305
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
306
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h2", marginTop: 8, marginBottom: 4, children: formatMessage({
307
+ id: getTranslation("settings.section.translation"),
308
+ defaultMessage: "Translation Settings"
309
+ }) }),
310
+ /* @__PURE__ */ jsxs(
311
+ Field.Root,
312
+ {
313
+ id: "system_prompt",
314
+ hint: formatMessage({
315
+ id: getTranslation("settings.form.system_prompt_hint"),
316
+ defaultMessage: "Instructions for the translation provider."
317
+ }),
318
+ required: true,
319
+ children: [
320
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
321
+ id: getTranslation("settings.form.system_prompt"),
322
+ defaultMessage: "System prompt"
323
+ }) }),
324
+ /* @__PURE__ */ jsx(
325
+ Textarea,
326
+ {
327
+ id: "system_prompt",
328
+ value: config.systemPrompt,
329
+ onChange: (e) => updateField("systemPrompt", e.target.value),
330
+ required: true,
331
+ placeholder: DEFAULT_SYSTEM_PROMPT,
332
+ name: "system_prompt"
333
+ }
334
+ ),
335
+ /* @__PURE__ */ jsx(Field.Error, {}),
336
+ /* @__PURE__ */ jsx(Field.Hint, {})
337
+ ]
338
+ }
339
+ ),
340
+ /* @__PURE__ */ jsx(Flex, { gap: 4, marginTop: 6, children: /* @__PURE__ */ jsxs(
341
+ Field.Root,
342
+ {
343
+ id: "llm_temperature",
344
+ flex: 1,
345
+ hint: formatMessage({
346
+ id: getTranslation("settings.form.temperature_hint"),
347
+ defaultMessage: "Higher = more creative, lower = more deterministic."
348
+ }),
349
+ children: [
350
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({
351
+ id: getTranslation("settings.form.temperature"),
352
+ defaultMessage: "Temperature"
353
+ }) }),
354
+ /* @__PURE__ */ jsx(
355
+ NumberInput,
356
+ {
357
+ id: "llm_temperature",
358
+ value: config.temperature,
359
+ name: "llm_temperature",
360
+ step: 0.1,
361
+ min: MIN_PROVIDER_TEMPERATURE,
362
+ max: MAX_PROVIDER_TEMPERATURE,
363
+ onValueChange: (value) => {
364
+ updateField(
365
+ "temperature",
366
+ clampNumber(
367
+ value,
368
+ DEFAULT_PROVIDER_TEMPERATURE,
369
+ MIN_PROVIDER_TEMPERATURE,
370
+ MAX_PROVIDER_TEMPERATURE
371
+ )
372
+ );
373
+ }
374
+ }
375
+ ),
376
+ /* @__PURE__ */ jsx(Field.Error, {}),
377
+ /* @__PURE__ */ jsx(Field.Hint, {})
378
+ ]
379
+ }
380
+ ) })
381
+ ] });
382
+ };
383
+ const SettingsPage = () => {
384
+ const { formatMessage } = useIntl();
385
+ const { config, save, restore, clearApiKey, updateField } = useTranslationConfig();
386
+ const providerModels = useProviderModels();
387
+ const loadModels = () => {
388
+ if (!config.apiKeyConfigured && config.apiKey.trim() === "") return;
389
+ providerModels.load({ apiKey: config.apiKey, baseUrl: config.baseUrl });
390
+ };
391
+ useEffect(() => {
392
+ loadModels();
393
+ }, [config.apiKeyConfigured]);
394
+ return /* @__PURE__ */ jsxs(Main, { children: [
395
+ /* @__PURE__ */ jsxs(
396
+ Box,
397
+ {
398
+ paddingLeft: 10,
399
+ paddingRight: 10,
400
+ paddingBottom: 8,
401
+ paddingTop: 8,
402
+ "data-strapi-header": true,
403
+ background: "neutral100",
404
+ children: [
405
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 3, marginBottom: 4, children: [
406
+ /* @__PURE__ */ jsx(PluginIcon, { width: 42, height: 42 }),
407
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", fontWeight: "bold", children: formatMessage({
408
+ id: getTranslation("settings.page.title"),
409
+ defaultMessage: "AI Translator Configuration"
410
+ }) })
411
+ ] }),
412
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", textColor: "neutral600", fontWeight: "normal", children: formatMessage({
413
+ id: getTranslation("settings.page.description"),
414
+ defaultMessage: "Configure AI translation settings for localized Strapi content."
415
+ }) })
416
+ ]
417
+ }
418
+ ),
419
+ /* @__PURE__ */ jsx(Box, { paddingLeft: 10, paddingRight: 10, children: /* @__PURE__ */ jsx(
420
+ Box,
421
+ {
422
+ paddingLeft: 10,
423
+ paddingRight: 10,
424
+ paddingTop: 8,
425
+ paddingBottom: 8,
426
+ background: "neutral0",
427
+ children: /* @__PURE__ */ jsxs(
428
+ "form",
429
+ {
430
+ onSubmit: (e) => {
431
+ e.preventDefault();
432
+ save(config);
433
+ },
434
+ children: [
435
+ /* @__PURE__ */ jsx(
436
+ ProviderConnectionFields,
437
+ {
438
+ onClearApiKey: clearApiKey,
439
+ config,
440
+ updateField,
441
+ providerModels: providerModels.models,
442
+ providerModelsError: providerModels.error,
443
+ onLoadModels: loadModels
444
+ }
445
+ ),
446
+ /* @__PURE__ */ jsx(TranslationSettingsFields, { config, updateField }),
447
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, marginTop: 6, justifyContent: "flex-end", children: [
448
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", type: "button", onClick: restore, marginTop: 4, children: formatMessage({
449
+ id: getTranslation("settings.form.restore"),
450
+ defaultMessage: "Restore Defaults"
451
+ }) }),
452
+ /* @__PURE__ */ jsx(Button, { variant: "default", type: "submit", marginTop: 4, children: formatMessage({
453
+ id: getTranslation("settings.form.save"),
454
+ defaultMessage: "Save"
455
+ }) })
456
+ ] })
457
+ ]
458
+ }
459
+ )
460
+ }
461
+ ) })
462
+ ] });
463
+ };
464
+ export {
465
+ SettingsPage
466
+ };