@raketa-cloud/admin 1.10.9 → 1.10.11
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/package.json +2 -2
- package/src/cloud/Resources.js +1 -0
- package/src/cloud/documents/contentTasksDocuments.js +9 -0
- package/src/handlers/resources/TranslationsHandler.js +30 -1
- package/src/helpers/content_tasks_helper.js +16 -0
- package/src/helpers/utils.js +16 -1
- package/src/magicPreview.js +4 -2
- package/src/routeResourceModule.js +2 -1
- package/src/tests/cloud/content_tasks_documents.test.js +11 -0
- package/src/tests/handlers/resources/TranslationsHandler.test/create.test.js +32 -0
- package/src/tests/handlers/resources/TranslationsHandler.test/edge_cases.test.js +4 -1
- package/src/tests/helpers/content_tasks_helper.test.js +29 -0
- package/src/tests/helpers/utils.test.js +7 -4
- package/src/tests/magicPreview.test.js +10 -1
- package/src/tests/routeResourceModule.test.js +6 -2
- package/src/tests/views/content_tasks.test.js +36 -0
- package/src/tests/views/due_date.test.js +10 -0
- package/src/tests/views/partial_translations.test.js +16 -1
- package/src/tests/views/translations_create_form.test.js +68 -0
- package/src/views/_shared/_content_tasks.ejs +3 -2
- package/src/views/resources/_shared/_due_date.ejs +3 -3
- package/src/views/resources/partial-translations/create_form.ejs +1 -1
- package/src/views/resources/translations/create_form.ejs +11 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raketa-cloud/admin",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.11",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"typescript": "5.9.3"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@hono/node-server": "2.
|
|
40
|
+
"@hono/node-server": "2.1.5",
|
|
41
41
|
"chart.js": "4.5.1",
|
|
42
42
|
"ejs": "6.0.1",
|
|
43
43
|
"google-auth-library": "11.3.1",
|
package/src/cloud/Resources.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
function matchResourceIdAndLocale(resources) {
|
|
2
|
+
const idLocaleMap = new Map();
|
|
3
|
+
|
|
4
|
+
resources.forEach((r) => {
|
|
5
|
+
Object.keys(r.translations).forEach((locale) => {
|
|
6
|
+
const translation = r.translations[locale];
|
|
7
|
+
|
|
8
|
+
if (!translation.id) {
|
|
9
|
+
throw new Error("Missing id in translation!");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
idLocaleMap.set(translation.id.toString(), locale);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return idLocaleMap;
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
const index = async (ctx) => {
|
|
2
20
|
const { parent_id } = ctx.params;
|
|
3
21
|
const { translations } = ctx.query;
|
|
@@ -222,12 +240,23 @@ const create = async (ctx) => {
|
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
const revisionInputs = [];
|
|
243
|
+
const resourceIdLocaleMap =
|
|
244
|
+
resources.length === selectedLocales.length
|
|
245
|
+
? null
|
|
246
|
+
: matchResourceIdAndLocale(resources);
|
|
225
247
|
|
|
226
248
|
for (let i = 0; i < resources.length; i++) {
|
|
227
249
|
const translated = resources[i];
|
|
228
|
-
const locale = selectedLocales[i];
|
|
229
250
|
|
|
230
251
|
if (translated) {
|
|
252
|
+
const locale = resourceIdLocaleMap
|
|
253
|
+
? resourceIdLocaleMap.get(translated.id.toString())
|
|
254
|
+
: selectedLocales[i];
|
|
255
|
+
|
|
256
|
+
if (!selectedLocales.includes(locale)) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
231
260
|
blueprint.translations = {
|
|
232
261
|
...blueprint.translations,
|
|
233
262
|
[locale]: {
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
export const TRANSLATION_WORKFLOWS = [
|
|
2
|
+
"LOKALISE_TT_AI_RT_HUM",
|
|
3
|
+
"LOKALISE_TT_HUM",
|
|
4
|
+
"LOKALISE_TT_AI",
|
|
5
|
+
"PAYHAWK",
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export const workflowProvider = (workflow) =>
|
|
9
|
+
workflow.startsWith("LOKALISE_") ? "lokalise" : workflow.toLowerCase();
|
|
10
|
+
|
|
11
|
+
export const enabledTranslationWorkflows = (contentTasks) =>
|
|
12
|
+
TRANSLATION_WORKFLOWS.filter(
|
|
13
|
+
(workflow) => contentTasks?.[workflowProvider(workflow)] === true,
|
|
14
|
+
);
|
|
15
|
+
|
|
1
16
|
const WORKFLOWS_MAP = {
|
|
2
17
|
lokalise_tt_ai_rt_hum: "Lokalise: AI translation (Pro AI) & Human review",
|
|
3
18
|
lokalise_tt_hum: "Lokalise: Human translation",
|
|
4
19
|
lokalise_tt_ai: "Lokalise: AI translation (Pro AI)",
|
|
20
|
+
payhawk: "Payhawk: AI & Human translation",
|
|
5
21
|
};
|
|
6
22
|
|
|
7
23
|
export const workflowsMap = (workflowType) => {
|
package/src/helpers/utils.js
CHANGED
|
@@ -33,8 +33,22 @@ function formattedDate(dateTimeString) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function isFeatureEnabled(value) {
|
|
37
|
+
if (typeof value === "object" && value !== null) {
|
|
38
|
+
return Object.values(value).some(
|
|
39
|
+
(providerEnabled) => providerEnabled === true,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return value === true;
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
function feature(featureName, context) {
|
|
37
|
-
return context?.globalConfig?.features?.[featureName]
|
|
47
|
+
return isFeatureEnabled(context?.globalConfig?.features?.[featureName]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function contentTaskProviderEnabled(provider, context) {
|
|
51
|
+
return context?.globalConfig?.features?.contentTasks?.[provider] === true;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
export default {
|
|
@@ -42,4 +56,5 @@ export default {
|
|
|
42
56
|
formattedDate,
|
|
43
57
|
formattedDateTime,
|
|
44
58
|
feature,
|
|
59
|
+
contentTaskProviderEnabled,
|
|
45
60
|
};
|
package/src/magicPreview.js
CHANGED
|
@@ -57,10 +57,11 @@ export function unchunkToken(token, chunkSize = CHUNK_SIZE) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export function generateMagicPreviewToken(
|
|
60
|
-
{ slug, resourceType, locale } = {},
|
|
60
|
+
{ id, slug, resourceType, locale } = {},
|
|
61
61
|
config = {},
|
|
62
62
|
) {
|
|
63
63
|
const payload = {
|
|
64
|
+
id,
|
|
64
65
|
slug,
|
|
65
66
|
resourceType,
|
|
66
67
|
locale,
|
|
@@ -138,7 +139,7 @@ export function decipherMagicPreviewToken(token, config) {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
export function buildMagicPreviewAddress(
|
|
141
|
-
{ slug, resourceType, locale },
|
|
142
|
+
{ id, slug, resourceType, locale },
|
|
142
143
|
globalConfig,
|
|
143
144
|
) {
|
|
144
145
|
if (!slug || !resourceType || !locale) {
|
|
@@ -149,6 +150,7 @@ export function buildMagicPreviewAddress(
|
|
|
149
150
|
|
|
150
151
|
const token = generateMagicPreviewToken(
|
|
151
152
|
{
|
|
153
|
+
id,
|
|
152
154
|
slug,
|
|
153
155
|
resourceType,
|
|
154
156
|
locale,
|
|
@@ -24,6 +24,7 @@ import URLsHandler from "./handlers/resources/URLsHandler";
|
|
|
24
24
|
import routeCollection from "./routeCollection";
|
|
25
25
|
import routeNestedCollection from "./routeNestedCollection";
|
|
26
26
|
import routeNestedMember from "./routeNestedMember";
|
|
27
|
+
import { isFeatureEnabled } from "./helpers/utils";
|
|
27
28
|
|
|
28
29
|
function routeResourceModule(
|
|
29
30
|
adapter,
|
|
@@ -94,7 +95,7 @@ function routeResourceModule(
|
|
|
94
95
|
{ ...resourceConfig, viewsFallback: "resources/translations" },
|
|
95
96
|
);
|
|
96
97
|
|
|
97
|
-
if (adapter.globalConfig?.features?.contentTasks) {
|
|
98
|
+
if (isFeatureEnabled(adapter.globalConfig?.features?.contentTasks)) {
|
|
98
99
|
routeNestedCollection(
|
|
99
100
|
adapter,
|
|
100
101
|
baseResource,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ContentTasksDocument } from "../../cloud/documents/contentTasksDocuments";
|
|
2
|
+
|
|
3
|
+
describe("ContentTasksDocument", () => {
|
|
4
|
+
test("requests Payhawk workflow details", () => {
|
|
5
|
+
const query = ContentTasksDocument;
|
|
6
|
+
|
|
7
|
+
expect(query).toContain("payhawkWorkflow");
|
|
8
|
+
expect(query).toContain("reference");
|
|
9
|
+
expect(query).toContain("partial");
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -324,6 +324,38 @@ describe("Resource Translations Handler", () => {
|
|
|
324
324
|
});
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
+
test("creates revisions and redirect pairs from returned locales when the backend omits a requested locale", async () => {
|
|
328
|
+
const resource = Factory.build("resource", {
|
|
329
|
+
translations: {},
|
|
330
|
+
categories: [{ id: "cat1" }],
|
|
331
|
+
});
|
|
332
|
+
const ctx = buildCtx({ languages: ["de", "en-us", "fr"] }, resource);
|
|
333
|
+
|
|
334
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => resource);
|
|
335
|
+
jest
|
|
336
|
+
.spyOn(ctx.client.resources, "multiTranslate")
|
|
337
|
+
.mockImplementation(() => ({
|
|
338
|
+
resources: [
|
|
339
|
+
{ id: "de-1", translations: { fr: { id: "fr-1" } } },
|
|
340
|
+
{ id: "fr-1", translations: { de: { id: "de-1" } } },
|
|
341
|
+
],
|
|
342
|
+
errors: null,
|
|
343
|
+
}));
|
|
344
|
+
const createBatch = jest
|
|
345
|
+
.spyOn(ctx.client.resourceRevisions, "createBatch")
|
|
346
|
+
.mockImplementation(() => [{ errors: null }, { errors: null }]);
|
|
347
|
+
|
|
348
|
+
const result = await TranslationsHandler.create(ctx);
|
|
349
|
+
|
|
350
|
+
expect(createBatch).toHaveBeenCalledWith([
|
|
351
|
+
{ resourceId: "de-1", resourceType: "article", locale: "de" },
|
|
352
|
+
{ resourceId: "fr-1", resourceType: "article", locale: "fr" },
|
|
353
|
+
]);
|
|
354
|
+
expect(result).toEqual({
|
|
355
|
+
redirect: `/en/articles/${resource.id}/translations?translations=de:de-1;fr:fr-1`,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
327
359
|
test("create translation fails with revision errors", async () => {
|
|
328
360
|
const resource = Factory.build("resource", {
|
|
329
361
|
translations: {},
|
|
@@ -156,7 +156,10 @@ describe("Resource Translations Handler create", () => {
|
|
|
156
156
|
jest
|
|
157
157
|
.spyOn(ctx.client.resources, "multiTranslate")
|
|
158
158
|
.mockImplementation(() => ({
|
|
159
|
-
resources: [
|
|
159
|
+
resources: [
|
|
160
|
+
{ id: "en-1", translations: { fr: { id: "fr-1" } } },
|
|
161
|
+
{ id: "fr-1", translations: { en: { id: "en-1" } } },
|
|
162
|
+
],
|
|
160
163
|
errors: null,
|
|
161
164
|
}));
|
|
162
165
|
const mockRevisions = jest.spyOn(
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
TRANSLATION_WORKFLOWS,
|
|
3
|
+
enabledTranslationWorkflows,
|
|
4
|
+
workflowProvider,
|
|
2
5
|
workflowsMap,
|
|
3
6
|
buildResourceUrl,
|
|
4
7
|
hasUnattachedKeys,
|
|
@@ -18,6 +21,7 @@ describe("content_tasks_helper", () => {
|
|
|
18
21
|
expect(workflowsMap("lokalise_tt_ai")).toBe(
|
|
19
22
|
"Lokalise: AI translation (Pro AI)",
|
|
20
23
|
);
|
|
24
|
+
expect(workflowsMap("payhawk")).toBe("Payhawk: AI & Human translation");
|
|
21
25
|
});
|
|
22
26
|
|
|
23
27
|
test("returns 'Unknown workflow' for unknown type", () => {
|
|
@@ -25,6 +29,31 @@ describe("content_tasks_helper", () => {
|
|
|
25
29
|
});
|
|
26
30
|
});
|
|
27
31
|
|
|
32
|
+
test("exports all translation workflows and only Lokalise due-date workflows", () => {
|
|
33
|
+
expect(TRANSLATION_WORKFLOWS).toEqual([
|
|
34
|
+
"LOKALISE_TT_AI_RT_HUM",
|
|
35
|
+
"LOKALISE_TT_HUM",
|
|
36
|
+
"LOKALISE_TT_AI",
|
|
37
|
+
"PAYHAWK",
|
|
38
|
+
]);
|
|
39
|
+
expect(workflowProvider("LOKALISE_TT_AI")).toBe("lokalise");
|
|
40
|
+
expect(workflowProvider("PAYHAWK")).toBe("payhawk");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("enabledTranslationWorkflows", () => {
|
|
44
|
+
test("returns workflows for only the enabled providers", () => {
|
|
45
|
+
expect(
|
|
46
|
+
enabledTranslationWorkflows({ lokalise: true, payhawk: false }),
|
|
47
|
+
).toEqual(["LOKALISE_TT_AI_RT_HUM", "LOKALISE_TT_HUM", "LOKALISE_TT_AI"]);
|
|
48
|
+
expect(
|
|
49
|
+
enabledTranslationWorkflows({ lokalise: false, payhawk: true }),
|
|
50
|
+
).toEqual(["PAYHAWK"]);
|
|
51
|
+
expect(
|
|
52
|
+
enabledTranslationWorkflows({ lokalise: true, payhawk: true }),
|
|
53
|
+
).toEqual(TRANSLATION_WORKFLOWS);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
28
57
|
describe("buildResourceUrl", () => {
|
|
29
58
|
const globalConfig = {
|
|
30
59
|
resourceRoutes: {
|
|
@@ -64,22 +64,25 @@ describe("formattedDate", () => {
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
describe("feature", () => {
|
|
67
|
-
it("returns true when
|
|
67
|
+
it("returns true when at least one content task provider is enabled", () => {
|
|
68
68
|
const context = {
|
|
69
69
|
globalConfig: {
|
|
70
70
|
features: {
|
|
71
|
-
contentTasks: true,
|
|
71
|
+
contentTasks: { lokalise: true, payhawk: false },
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
};
|
|
75
|
+
|
|
75
76
|
expect(utils.feature("contentTasks", context)).toEqual(true);
|
|
77
|
+
expect(utils.contentTaskProviderEnabled("lokalise", context)).toEqual(true);
|
|
78
|
+
expect(utils.contentTaskProviderEnabled("payhawk", context)).toEqual(false);
|
|
76
79
|
});
|
|
77
80
|
|
|
78
|
-
it("returns false when
|
|
81
|
+
it("returns false when all content task providers are disabled", () => {
|
|
79
82
|
const context = {
|
|
80
83
|
globalConfig: {
|
|
81
84
|
features: {
|
|
82
|
-
contentTasks: false,
|
|
85
|
+
contentTasks: { lokalise: false, payhawk: false },
|
|
83
86
|
},
|
|
84
87
|
},
|
|
85
88
|
};
|
|
@@ -31,9 +31,10 @@ describe("magicPreview helper", () => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
describe("buildMagicPreviewAddress", () => {
|
|
34
|
-
it("builds
|
|
34
|
+
it("builds a preview URL with the resource ID in its token", () => {
|
|
35
35
|
const address = buildMagicPreviewAddress(
|
|
36
36
|
{
|
|
37
|
+
id: "resource-42",
|
|
37
38
|
slug: "gocardless",
|
|
38
39
|
resourceType: "customer",
|
|
39
40
|
locale: "en",
|
|
@@ -44,6 +45,12 @@ describe("magicPreview helper", () => {
|
|
|
44
45
|
expect(address).toMatch(
|
|
45
46
|
/^https:\/\/example\.com\/magic-preview\/customer\/[A-Za-z0-9_-]+$/,
|
|
46
47
|
);
|
|
48
|
+
expect(
|
|
49
|
+
decipherMagicPreviewToken(address.split("/").at(-1), config),
|
|
50
|
+
).toMatchObject({
|
|
51
|
+
id: "resource-42",
|
|
52
|
+
slug: "gocardless",
|
|
53
|
+
});
|
|
47
54
|
});
|
|
48
55
|
|
|
49
56
|
it("builds localized preview URL for non-default locale", () => {
|
|
@@ -96,6 +103,7 @@ describe("magicPreview helper", () => {
|
|
|
96
103
|
it("generates an encrypted token and deciphers it back", () => {
|
|
97
104
|
const token = generateMagicPreviewToken(
|
|
98
105
|
{
|
|
106
|
+
id: "resource-123",
|
|
99
107
|
slug: "my-first-ebook",
|
|
100
108
|
resourceType: "ebook",
|
|
101
109
|
locale: "en",
|
|
@@ -115,6 +123,7 @@ describe("magicPreview helper", () => {
|
|
|
115
123
|
|
|
116
124
|
const payload = decipherMagicPreviewToken(token, config);
|
|
117
125
|
expect(payload).not.toBeNull();
|
|
126
|
+
expect(payload.id).toBe("resource-123");
|
|
118
127
|
expect(payload.slug).toBe("my-first-ebook");
|
|
119
128
|
expect(payload.resourceType).toBe("ebook");
|
|
120
129
|
expect(payload.locale).toBe("en");
|
|
@@ -211,8 +211,12 @@ describe("routeResourceModule", () => {
|
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
test("registers partial translations only when content tasks are enabled", () => {
|
|
214
|
-
const enabledAdapter = makeAdapter({
|
|
215
|
-
|
|
214
|
+
const enabledAdapter = makeAdapter({
|
|
215
|
+
features: { contentTasks: { lokalise: true, payhawk: false } },
|
|
216
|
+
});
|
|
217
|
+
const disabledAdapter = makeAdapter({
|
|
218
|
+
features: { contentTasks: { lokalise: false, payhawk: false } },
|
|
219
|
+
});
|
|
216
220
|
|
|
217
221
|
routeResourceModule(enabledAdapter, "article", "articles");
|
|
218
222
|
routeResourceModule(disabledAdapter, "article", "articles");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import ejs from "ejs";
|
|
3
|
+
|
|
4
|
+
const template = readFileSync(
|
|
5
|
+
`${process.cwd()}/src/views/_shared/_content_tasks.ejs`,
|
|
6
|
+
"utf8",
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
function renderContentTasks(task) {
|
|
10
|
+
return ejs.render(template, {
|
|
11
|
+
_context: { globalConfig: {}, locales: {} },
|
|
12
|
+
content_tasks: {
|
|
13
|
+
hasUnattachedKeys: () => false,
|
|
14
|
+
workflowsMap: (workflow) => workflow,
|
|
15
|
+
},
|
|
16
|
+
Inflector: { capitalize: (value) => value, humanize: (value) => value },
|
|
17
|
+
showAllDetails: false,
|
|
18
|
+
tasks: [task],
|
|
19
|
+
utils: { formattedDateTime: (value) => value },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("content tasks", () => {
|
|
24
|
+
test("uses Payhawk workflow details to identify a partial task", () => {
|
|
25
|
+
const rendered = renderContentTasks({
|
|
26
|
+
insertedAt: "2026-10-19T12:30:00Z",
|
|
27
|
+
operatorMeta: {},
|
|
28
|
+
payhawkWorkflow: { partial: true, settings: {} },
|
|
29
|
+
resources: [],
|
|
30
|
+
workflow: "payhawk",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(rendered).toContain("Partial translation");
|
|
34
|
+
expect(rendered).toContain("payhawk");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -87,6 +87,16 @@ describe("resources shared due date", () => {
|
|
|
87
87
|
expect(dueDateInput.value).toBe("");
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test("shows and preserves the due date for the Payhawk workflow", () => {
|
|
91
|
+
const { dueDateWrapper, dueDateInput } = setupDueDateScript({
|
|
92
|
+
workflow: "PAYHAWK",
|
|
93
|
+
dueDate: "2026-10-19T12:30",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(dueDateWrapper.style.display).toBe("block");
|
|
97
|
+
expect(dueDateInput.value).toBe("2026-10-19T12:30");
|
|
98
|
+
});
|
|
99
|
+
|
|
90
100
|
test("shows the due date and defaults it to the minimum weekday", () => {
|
|
91
101
|
jest.useFakeTimers().setSystemTime(new Date("2026-10-16T10:00:00"));
|
|
92
102
|
|
|
@@ -9,13 +9,21 @@ const template = readFileSync(
|
|
|
9
9
|
function renderPartialTranslations(translations) {
|
|
10
10
|
return ejs.render(template, {
|
|
11
11
|
_context: {
|
|
12
|
+
globalConfig: {
|
|
13
|
+
features: { contentTasks: { lokalise: true, payhawk: true } },
|
|
14
|
+
},
|
|
12
15
|
locale: "en",
|
|
13
16
|
maxOpenRevisions: 1,
|
|
14
17
|
locales: { en: { name: "English" }, fr: { name: "French" } },
|
|
15
18
|
resourceConfig: { resourceTypePlural: "pages" },
|
|
16
19
|
},
|
|
17
|
-
content_tasks: {
|
|
20
|
+
content_tasks: {
|
|
21
|
+
TRANSLATION_WORKFLOWS: ["LOKALISE_TT_AI", "PAYHAWK"],
|
|
22
|
+
enabledTranslationWorkflows: () => ["LOKALISE_TT_AI", "PAYHAWK"],
|
|
23
|
+
workflowsMap: (workflow) => workflow,
|
|
24
|
+
},
|
|
18
25
|
errors: {},
|
|
26
|
+
utils: {},
|
|
19
27
|
partialTranslation: { locales: [], widgetIds: ["hero-1"], workflow: {} },
|
|
20
28
|
previewUrl: "https://preview.example/hello",
|
|
21
29
|
render_partial: () => "",
|
|
@@ -49,6 +57,13 @@ describe("partial translations form", () => {
|
|
|
49
57
|
expect(rendered).not.toContain("This resource has no translations.");
|
|
50
58
|
});
|
|
51
59
|
|
|
60
|
+
test("includes Payhawk as a translation workflow", () => {
|
|
61
|
+
const rendered = renderPartialTranslations({ fr: { id: "43" } });
|
|
62
|
+
|
|
63
|
+
expect(rendered).toContain('value="PAYHAWK"');
|
|
64
|
+
expect(rendered).toContain(">payhawk</option>");
|
|
65
|
+
});
|
|
66
|
+
|
|
52
67
|
test("disables translations at the open revision limit", () => {
|
|
53
68
|
const rendered = renderPartialTranslations({
|
|
54
69
|
fr: { id: "43", revisionsCount: 1 },
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import ejs from "ejs";
|
|
3
|
+
|
|
4
|
+
const template = readFileSync(
|
|
5
|
+
`${process.cwd()}/src/views/resources/translations/create_form.ejs`,
|
|
6
|
+
"utf8",
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
function renderTranslationsCreateForm(
|
|
10
|
+
workflow = {},
|
|
11
|
+
providers = { lokalise: true, payhawk: true },
|
|
12
|
+
) {
|
|
13
|
+
return ejs.render(template, {
|
|
14
|
+
_context: {
|
|
15
|
+
defaultLocale: "en",
|
|
16
|
+
globalConfig: {
|
|
17
|
+
features: { contentTasks: { lokalise: true, payhawk: true } },
|
|
18
|
+
},
|
|
19
|
+
locale: "en",
|
|
20
|
+
locales: {
|
|
21
|
+
en: { name: "English" },
|
|
22
|
+
fr: { name: "French" },
|
|
23
|
+
},
|
|
24
|
+
resourceConfig: { resourceType: "page", resourceTypePlural: "pages" },
|
|
25
|
+
},
|
|
26
|
+
blueprint: { id: "42", title: "Hello" },
|
|
27
|
+
content_tasks: {
|
|
28
|
+
TRANSLATION_WORKFLOWS: ["LOKALISE_TT_AI", "PAYHAWK"],
|
|
29
|
+
enabledTranslationWorkflows: () =>
|
|
30
|
+
["LOKALISE_TT_AI", "PAYHAWK"].filter(
|
|
31
|
+
(workflowType) =>
|
|
32
|
+
providers[
|
|
33
|
+
workflowType.startsWith("LOKALISE_")
|
|
34
|
+
? "lokalise"
|
|
35
|
+
: workflowType.toLowerCase()
|
|
36
|
+
] === true,
|
|
37
|
+
),
|
|
38
|
+
workflowsMap: (type) => type,
|
|
39
|
+
},
|
|
40
|
+
errors: {},
|
|
41
|
+
form_for: (_resource, _errors, callback) => callback({ submit: () => "" }),
|
|
42
|
+
Inflector: { downcase: (value) => value.toLowerCase() },
|
|
43
|
+
render_partial: () => "",
|
|
44
|
+
resource: { languages: [], translations: {}, workflow },
|
|
45
|
+
targetLocale: null,
|
|
46
|
+
utils: { feature: () => true },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("translations create form", () => {
|
|
51
|
+
test("hides providers disabled in the content task feature configuration", () => {
|
|
52
|
+
const rendered = renderTranslationsCreateForm(
|
|
53
|
+
{},
|
|
54
|
+
{ lokalise: true, payhawk: false },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(rendered).toContain('value="LOKALISE_TT_AI"');
|
|
58
|
+
expect(rendered).not.toContain('value="PAYHAWK"');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("includes Payhawk and preserves its selected value", () => {
|
|
62
|
+
const rendered = renderTranslationsCreateForm({ type: "PAYHAWK" });
|
|
63
|
+
|
|
64
|
+
expect(rendered).toContain('value="PAYHAWK" selected');
|
|
65
|
+
expect(rendered).toMatch(/>\s*payhawk\s*<\/option>/);
|
|
66
|
+
expect(rendered).toContain("Translation (Payhawk)");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
<tbody>
|
|
20
20
|
<% tasks.forEach(task => { %>
|
|
21
21
|
<% const resources = task.resources || []; %>
|
|
22
|
+
<% const workflow = task.lokaliseWorkflow || task.payhawkWorkflow; %>
|
|
22
23
|
<% const isMarked = showAllDetails && content_tasks.hasUnattachedKeys(task); %>
|
|
23
24
|
|
|
24
25
|
<tr class="<%= isMarked ? 'bg-orange-100 dark:bg-orange-950/40' : '' %>">
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
|
|
62
63
|
<td>
|
|
63
64
|
<span class="body-3 flex items-center gap-0.5">
|
|
64
|
-
<% if (
|
|
65
|
+
<% if (workflow?.partial) { %>
|
|
65
66
|
<svg viewBox="0 0 20 20" class="size-3.5 shrink-0 text-raketa-primary" role="img" aria-label="Partial translation">
|
|
66
67
|
<title>Partial translation</title>
|
|
67
68
|
<path fill="currentColor" d="M10 10V2a8 8 0 1 1-8 8h8Z" />
|
|
@@ -128,7 +129,7 @@
|
|
|
128
129
|
<% if (showAllDetails) { %>
|
|
129
130
|
<td class="whitespace-nowrap">
|
|
130
131
|
<div class="body-3 text-raketa-foreground/50">
|
|
131
|
-
<% const dueDate =
|
|
132
|
+
<% const dueDate = workflow?.settings?.due_date %>
|
|
132
133
|
|
|
133
134
|
<% if (dueDate) { %>
|
|
134
135
|
<div class="flex items-center gap-1 mb-0.5">
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
const minDate = getMinimumDueDate();
|
|
53
53
|
dueDateInput.min = minDate;
|
|
54
54
|
|
|
55
|
-
const
|
|
56
|
-
dueDateWrapper.style.display =
|
|
55
|
+
const hasWorkflow = Boolean(workflowSelect.value);
|
|
56
|
+
dueDateWrapper.style.display = hasWorkflow ? "block" : "none";
|
|
57
57
|
|
|
58
|
-
if (
|
|
58
|
+
if (!hasWorkflow) {
|
|
59
59
|
dueDateInput.value = "";
|
|
60
60
|
} else if (!dueDateInput.value) {
|
|
61
61
|
dueDateInput.value = minDate;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
<label for="workflow" class="font-semibold body-3">Translation workflow</label>
|
|
29
29
|
<select name="partial_translation[workflow][type]" id="workflow" required>
|
|
30
30
|
<option value="">Select workflow</option>
|
|
31
|
-
<%
|
|
31
|
+
<% content_tasks.enabledTranslationWorkflows(_context.globalConfig.features.contentTasks).forEach(workflow => { %>
|
|
32
32
|
<option value="<%= workflow %>" <%= partialTranslation.workflow?.type === workflow ? 'selected' : '' %>><%= content_tasks.workflowsMap(workflow.toLowerCase()) %></option>
|
|
33
33
|
<% }) %>
|
|
34
34
|
</select>
|
|
@@ -36,20 +36,14 @@
|
|
|
36
36
|
<select name="resource[workflow][type]" id="workflow">
|
|
37
37
|
<option value="">Default (no workflow)</option>
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<option value="LOKALISE_TT_HUM" <%= resource.workflow?.type === 'LOKALISE_TT_HUM' ? 'selected' : '' %>>
|
|
44
|
-
<%= content_tasks.workflowsMap("LOKALISE_TT_HUM".toLowerCase()) %>
|
|
45
|
-
</option>
|
|
46
|
-
|
|
47
|
-
<option value="LOKALISE_TT_AI" <%= resource.workflow?.type === 'LOKALISE_TT_AI' ? 'selected' : '' %>>
|
|
48
|
-
<%= content_tasks.workflowsMap("LOKALISE_TT_AI".toLowerCase()) %>
|
|
39
|
+
<% content_tasks.enabledTranslationWorkflows(_context.globalConfig.features.contentTasks).forEach(workflow => { %>
|
|
40
|
+
<option value="<%= workflow %>" <%= resource.workflow?.type === workflow ? 'selected' : '' %>>
|
|
41
|
+
<%= content_tasks.workflowsMap(workflow.toLowerCase()) %>
|
|
49
42
|
</option>
|
|
43
|
+
<% }) %>
|
|
50
44
|
</select>
|
|
51
45
|
<p class="text-sm text-raketa-foreground/50 mt-1">
|
|
52
|
-
Select a workflow for the translation process.
|
|
46
|
+
Select a workflow for the translation process. An external workflow initiates a translation task with the selected provider.
|
|
53
47
|
</p>
|
|
54
48
|
</div>
|
|
55
49
|
|
|
@@ -124,15 +118,18 @@
|
|
|
124
118
|
<li>
|
|
125
119
|
<strong>Human Translation (Lokalise):</strong> A task is sent to Lokalise where a human translator fully translates and reviews the content.
|
|
126
120
|
</li>
|
|
121
|
+
<li>
|
|
122
|
+
<strong>Translation (Payhawk):</strong> A task is sent to Payhawk for translation in configured target languages.
|
|
123
|
+
</li>
|
|
127
124
|
</ul>
|
|
128
125
|
|
|
129
126
|
<br />
|
|
130
|
-
<strong>Notes:</strong> For all
|
|
127
|
+
<strong>Notes:</strong> For all external workflows:
|
|
131
128
|
<ul>
|
|
132
|
-
<li>Content is required to start
|
|
129
|
+
<li>Content is required to start an external translation task.</li>
|
|
133
130
|
<li>The private copy and new revision will be automatically created here in the CMS only after the external task is completed and returned.</li>
|
|
134
131
|
<li>You can track translation progress in the <a href="<%= `/${_context.locale}/content_tasks` %>" target="_blank">Translation tasks</a> module.</li>
|
|
135
|
-
<li>Translations will only be created for languages
|
|
132
|
+
<li>Translations will only be created for languages configured for the selected provider.</li>
|
|
136
133
|
</ul>
|
|
137
134
|
</div>
|
|
138
135
|
</div>
|