@strapi/i18n 0.0.0-next.d9724d67b33363354d7171a9f2265e1c42485e13 → 0.0.0-next.da19c0501ff87d14fb664b55b8e0630d3c548485
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/dist/admin/components/CMHeaderActions.js +132 -1
- package/dist/admin/components/CMHeaderActions.js.map +1 -1
- package/dist/admin/components/CMHeaderActions.mjs +135 -5
- package/dist/admin/components/CMHeaderActions.mjs.map +1 -1
- package/dist/admin/components/CreateLocale.js +1 -1
- package/dist/admin/components/CreateLocale.js.map +1 -1
- package/dist/admin/components/CreateLocale.mjs +1 -1
- package/dist/admin/components/CreateLocale.mjs.map +1 -1
- package/dist/admin/components/EditLocale.js +1 -1
- package/dist/admin/components/EditLocale.js.map +1 -1
- package/dist/admin/components/EditLocale.mjs +1 -1
- package/dist/admin/components/EditLocale.mjs.map +1 -1
- package/dist/admin/components/LocaleListCell.js +65 -45
- package/dist/admin/components/LocaleListCell.js.map +1 -1
- package/dist/admin/components/LocaleListCell.mjs +66 -46
- package/dist/admin/components/LocaleListCell.mjs.map +1 -1
- package/dist/admin/components/LocalePicker.js +18 -11
- package/dist/admin/components/LocalePicker.js.map +1 -1
- package/dist/admin/components/LocalePicker.mjs +19 -12
- package/dist/admin/components/LocalePicker.mjs.map +1 -1
- package/dist/admin/contentManagerHooks/editView.js +6 -3
- package/dist/admin/contentManagerHooks/editView.js.map +1 -1
- package/dist/admin/contentManagerHooks/editView.mjs +7 -4
- package/dist/admin/contentManagerHooks/editView.mjs.map +1 -1
- package/dist/admin/contentManagerHooks/listView.js +2 -1
- package/dist/admin/contentManagerHooks/listView.js.map +1 -1
- package/dist/admin/contentManagerHooks/listView.mjs +2 -1
- package/dist/admin/contentManagerHooks/listView.mjs.map +1 -1
- package/dist/admin/hooks/useAILocalizationJobsPolling.js +91 -0
- package/dist/admin/hooks/useAILocalizationJobsPolling.js.map +1 -0
- package/dist/admin/hooks/useAILocalizationJobsPolling.mjs +70 -0
- package/dist/admin/hooks/useAILocalizationJobsPolling.mjs.map +1 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +2 -1
- package/dist/admin/index.mjs.map +1 -1
- package/dist/admin/pages/SettingsPage.js +121 -46
- package/dist/admin/pages/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage.mjs +124 -30
- package/dist/admin/pages/SettingsPage.mjs.map +1 -1
- package/dist/admin/services/aiLocalizationJobs.js +26 -0
- package/dist/admin/services/aiLocalizationJobs.js.map +1 -0
- package/dist/admin/services/aiLocalizationJobs.mjs +24 -0
- package/dist/admin/services/aiLocalizationJobs.mjs.map +1 -0
- package/dist/admin/services/api.js +3 -1
- package/dist/admin/services/api.js.map +1 -1
- package/dist/admin/services/api.mjs +3 -1
- package/dist/admin/services/api.mjs.map +1 -1
- package/dist/admin/services/settings.js +29 -0
- package/dist/admin/services/settings.js.map +1 -0
- package/dist/admin/services/settings.mjs +26 -0
- package/dist/admin/services/settings.mjs.map +1 -0
- package/dist/admin/src/components/CMHeaderActions.d.ts +7 -1
- package/dist/admin/src/components/LocaleListCell.d.ts +2 -1
- package/dist/admin/src/hooks/useAILocalizationJobsPolling.d.ts +9 -0
- package/dist/admin/src/services/aiLocalizationJobs.d.ts +6 -0
- package/dist/admin/src/services/api.d.ts +1 -1
- package/dist/admin/src/services/locales.d.ts +1 -1
- package/dist/admin/src/services/relations.d.ts +1 -1
- package/dist/admin/src/services/settings.d.ts +5 -0
- package/dist/admin/translations/en.json.js +9 -0
- package/dist/admin/translations/en.json.js.map +1 -1
- package/dist/admin/translations/en.json.mjs +9 -0
- package/dist/admin/translations/en.json.mjs.map +1 -1
- package/dist/admin/utils/clean.js +2 -2
- package/dist/admin/utils/clean.js.map +1 -1
- package/dist/admin/utils/clean.mjs +2 -2
- package/dist/admin/utils/clean.mjs.map +1 -1
- package/dist/server/bootstrap.js +2 -0
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/server/bootstrap.mjs +2 -0
- package/dist/server/bootstrap.mjs.map +1 -1
- package/dist/server/constants/iso-locales.json.js +4 -0
- package/dist/server/constants/iso-locales.json.js.map +1 -1
- package/dist/server/constants/iso-locales.json.mjs +4 -0
- package/dist/server/constants/iso-locales.json.mjs.map +1 -1
- package/dist/server/controllers/ai-localization-jobs.js +47 -0
- package/dist/server/controllers/ai-localization-jobs.js.map +1 -0
- package/dist/server/controllers/ai-localization-jobs.mjs +45 -0
- package/dist/server/controllers/ai-localization-jobs.mjs.map +1 -0
- package/dist/server/controllers/index.js +5 -1
- package/dist/server/controllers/index.js.map +1 -1
- package/dist/server/controllers/index.mjs +5 -1
- package/dist/server/controllers/index.mjs.map +1 -1
- package/dist/server/controllers/settings.js +24 -0
- package/dist/server/controllers/settings.js.map +1 -0
- package/dist/server/controllers/settings.mjs +22 -0
- package/dist/server/controllers/settings.mjs.map +1 -0
- package/dist/server/models/ai-localization-job.js +60 -0
- package/dist/server/models/ai-localization-job.js.map +1 -0
- package/dist/server/models/ai-localization-job.mjs +57 -0
- package/dist/server/models/ai-localization-job.mjs.map +1 -0
- package/dist/server/register.js +3 -1
- package/dist/server/register.js.map +1 -1
- package/dist/server/register.mjs +3 -1
- package/dist/server/register.mjs.map +1 -1
- package/dist/server/routes/admin.js +40 -0
- package/dist/server/routes/admin.js.map +1 -1
- package/dist/server/routes/admin.mjs +40 -0
- package/dist/server/routes/admin.mjs.map +1 -1
- package/dist/server/services/ai-localization-jobs.js +64 -0
- package/dist/server/services/ai-localization-jobs.js.map +1 -0
- package/dist/server/services/ai-localization-jobs.mjs +62 -0
- package/dist/server/services/ai-localization-jobs.mjs.map +1 -0
- package/dist/server/services/ai-localizations.js +210 -0
- package/dist/server/services/ai-localizations.js.map +1 -0
- package/dist/server/services/ai-localizations.mjs +208 -0
- package/dist/server/services/ai-localizations.mjs.map +1 -0
- package/dist/server/services/index.js +7 -1
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/index.mjs +7 -1
- package/dist/server/services/index.mjs.map +1 -1
- package/dist/server/services/settings.js +25 -0
- package/dist/server/services/settings.js.map +1 -0
- package/dist/server/services/settings.mjs +23 -0
- package/dist/server/services/settings.mjs.map +1 -0
- package/dist/server/src/bootstrap.d.ts.map +1 -1
- package/dist/server/src/controllers/ai-localization-jobs.d.ts +17 -0
- package/dist/server/src/controllers/ai-localization-jobs.d.ts.map +1 -0
- package/dist/server/src/controllers/index.d.ts +10 -0
- package/dist/server/src/controllers/index.d.ts.map +1 -1
- package/dist/server/src/controllers/settings.d.ts +7 -0
- package/dist/server/src/controllers/settings.d.ts.map +1 -0
- package/dist/server/src/index.d.ts +44 -1
- package/dist/server/src/index.d.ts.map +1 -1
- package/dist/server/src/models/ai-localization-job.d.ts +5 -0
- package/dist/server/src/models/ai-localization-job.d.ts.map +1 -0
- package/dist/server/src/models/index.d.ts +5 -0
- package/dist/server/src/models/index.d.ts.map +1 -0
- package/dist/server/src/register.d.ts +1 -1
- package/dist/server/src/register.d.ts.map +1 -1
- package/dist/server/src/routes/admin.d.ts.map +1 -1
- package/dist/server/src/services/ai-localization-jobs.d.ts +26 -0
- package/dist/server/src/services/ai-localization-jobs.d.ts.map +1 -0
- package/dist/server/src/services/ai-localizations.d.ts +18 -0
- package/dist/server/src/services/ai-localizations.d.ts.map +1 -0
- package/dist/server/src/services/index.d.ts +33 -0
- package/dist/server/src/services/index.d.ts.map +1 -1
- package/dist/server/src/services/settings.d.ts +13 -0
- package/dist/server/src/services/settings.d.ts.map +1 -0
- package/dist/server/src/utils/index.d.ts +7 -1
- package/dist/server/src/utils/index.d.ts.map +1 -1
- package/dist/server/src/validation/settings.d.ts +12 -0
- package/dist/server/src/validation/settings.d.ts.map +1 -0
- package/dist/server/utils/index.js.map +1 -1
- package/dist/server/utils/index.mjs.map +1 -1
- package/dist/server/validation/settings.js +11 -0
- package/dist/server/validation/settings.js.map +1 -0
- package/dist/server/validation/settings.mjs +9 -0
- package/dist/server/validation/settings.mjs.map +1 -0
- package/dist/shared/contracts/ai-localization-jobs.d.ts +27 -0
- package/dist/shared/contracts/ai-localization-jobs.d.ts.map +1 -0
- package/dist/shared/contracts/settings.d.ts +40 -0
- package/dist/shared/contracts/shared.d.ts.map +1 -0
- package/package.json +10 -8
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var aiLocalizationJob = require('../models/ai-localization-job.js');
|
|
4
|
+
|
|
5
|
+
const createAILocalizationJobsService = ({ strapi })=>({
|
|
6
|
+
/**
|
|
7
|
+
* Create a new AI localizations job or update an existing one for a document
|
|
8
|
+
* Ensures only one job exists per document
|
|
9
|
+
*/ async upsertJobForDocument ({ documentId, contentType, sourceLocale, targetLocales, status = 'processing' }) {
|
|
10
|
+
// Check if job already exists for this document
|
|
11
|
+
const existingJob = await this.getJobByDocument(contentType, documentId);
|
|
12
|
+
if (existingJob) {
|
|
13
|
+
strapi.log.info(`[AI Localizations Job] Updated existing job for document ${documentId} with status: ${status}`);
|
|
14
|
+
// Update existing job with new data and status
|
|
15
|
+
return strapi.db.query(aiLocalizationJob.AI_LOCALIZATION_JOB_UID).update({
|
|
16
|
+
where: {
|
|
17
|
+
id: existingJob.id
|
|
18
|
+
},
|
|
19
|
+
data: {
|
|
20
|
+
contentType,
|
|
21
|
+
sourceLocale,
|
|
22
|
+
targetLocales,
|
|
23
|
+
status,
|
|
24
|
+
updatedAt: new Date()
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
strapi.log.info(`[AI Localizations Job] Created new job for document ${documentId} with status: ${status}`);
|
|
29
|
+
// Create new AI localizations job
|
|
30
|
+
return strapi.db.query(aiLocalizationJob.AI_LOCALIZATION_JOB_UID).create({
|
|
31
|
+
data: {
|
|
32
|
+
contentType,
|
|
33
|
+
relatedDocumentId: documentId,
|
|
34
|
+
sourceLocale,
|
|
35
|
+
targetLocales,
|
|
36
|
+
status,
|
|
37
|
+
createdAt: new Date(),
|
|
38
|
+
updatedAt: new Date()
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Get job by document ID
|
|
44
|
+
*/ async getJobByDocument (contentType, documentId) {
|
|
45
|
+
return strapi.db.query(aiLocalizationJob.AI_LOCALIZATION_JOB_UID).findOne({
|
|
46
|
+
where: {
|
|
47
|
+
relatedDocumentId: documentId,
|
|
48
|
+
contentType
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Get job by content type
|
|
54
|
+
*/ async getJobByContentType (contentType) {
|
|
55
|
+
return strapi.db.query(aiLocalizationJob.AI_LOCALIZATION_JOB_UID).findOne({
|
|
56
|
+
where: {
|
|
57
|
+
contentType
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
exports.createAILocalizationJobsService = createAILocalizationJobsService;
|
|
64
|
+
//# sourceMappingURL=ai-localization-jobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-localization-jobs.js","sources":["../../../server/src/services/ai-localization-jobs.ts"],"sourcesContent":["import type { Core } from '@strapi/types';\nimport { AI_LOCALIZATION_JOB_UID } from '../models/ai-localization-job';\nimport type { AILocalizationJobs } from '../../../shared/contracts/ai-localization-jobs';\n\nexport const createAILocalizationJobsService = ({ strapi }: { strapi: Core.Strapi }) => ({\n /**\n * Create a new AI localizations job or update an existing one for a document\n * Ensures only one job exists per document\n */\n async upsertJobForDocument({\n documentId,\n contentType,\n sourceLocale,\n targetLocales,\n status = 'processing',\n }: {\n documentId: string;\n contentType: string;\n sourceLocale: string;\n targetLocales: string[];\n status?: AILocalizationJobs['status'];\n }) {\n // Check if job already exists for this document\n const existingJob = await this.getJobByDocument(contentType, documentId);\n\n if (existingJob) {\n strapi.log.info(\n `[AI Localizations Job] Updated existing job for document ${documentId} with status: ${status}`\n );\n // Update existing job with new data and status\n return strapi.db.query(AI_LOCALIZATION_JOB_UID).update({\n where: { id: existingJob.id },\n data: {\n contentType,\n sourceLocale,\n targetLocales,\n status,\n updatedAt: new Date(),\n },\n });\n }\n\n strapi.log.info(\n `[AI Localizations Job] Created new job for document ${documentId} with status: ${status}`\n );\n // Create new AI localizations job\n return strapi.db.query(AI_LOCALIZATION_JOB_UID).create({\n data: {\n contentType,\n relatedDocumentId: documentId,\n sourceLocale,\n targetLocales,\n status,\n createdAt: new Date(),\n updatedAt: new Date(),\n },\n });\n },\n\n /**\n * Get job by document ID\n */\n async getJobByDocument(contentType: string, documentId: string) {\n return strapi.db.query(AI_LOCALIZATION_JOB_UID).findOne({\n where: {\n relatedDocumentId: documentId,\n contentType,\n },\n });\n },\n\n /**\n * Get job by content type\n */\n async getJobByContentType(contentType: string) {\n return strapi.db.query(AI_LOCALIZATION_JOB_UID).findOne({\n where: {\n contentType,\n },\n });\n },\n});\n"],"names":["createAILocalizationJobsService","strapi","upsertJobForDocument","documentId","contentType","sourceLocale","targetLocales","status","existingJob","getJobByDocument","log","info","db","query","AI_LOCALIZATION_JOB_UID","update","where","id","data","updatedAt","Date","create","relatedDocumentId","createdAt","findOne","getJobByContentType"],"mappings":";;;;MAIaA,+BAAkC,GAAA,CAAC,EAAEC,MAAM,EAA2B,IAAM;AACvF;;;AAGC,MACD,MAAMC,oBAAAA,CAAAA,CAAqB,EACzBC,UAAU,EACVC,WAAW,EACXC,YAAY,EACZC,aAAa,EACbC,MAAAA,GAAS,YAAY,EAOtB,EAAA;;AAEC,YAAA,MAAMC,cAAc,MAAM,IAAI,CAACC,gBAAgB,CAACL,WAAaD,EAAAA,UAAAA,CAAAA;AAE7D,YAAA,IAAIK,WAAa,EAAA;gBACfP,MAAOS,CAAAA,GAAG,CAACC,IAAI,CACb,CAAC,yDAAyD,EAAER,UAAW,CAAA,cAAc,EAAEI,MAAAA,CAAO,CAAC,CAAA;;AAGjG,gBAAA,OAAON,OAAOW,EAAE,CAACC,KAAK,CAACC,yCAAAA,CAAAA,CAAyBC,MAAM,CAAC;oBACrDC,KAAO,EAAA;AAAEC,wBAAAA,EAAAA,EAAIT,YAAYS;AAAG,qBAAA;oBAC5BC,IAAM,EAAA;AACJd,wBAAAA,WAAAA;AACAC,wBAAAA,YAAAA;AACAC,wBAAAA,aAAAA;AACAC,wBAAAA,MAAAA;AACAY,wBAAAA,SAAAA,EAAW,IAAIC,IAAAA;AACjB;AACF,iBAAA,CAAA;AACF;YAEAnB,MAAOS,CAAAA,GAAG,CAACC,IAAI,CACb,CAAC,oDAAoD,EAAER,UAAW,CAAA,cAAc,EAAEI,MAAAA,CAAO,CAAC,CAAA;;AAG5F,YAAA,OAAON,OAAOW,EAAE,CAACC,KAAK,CAACC,yCAAAA,CAAAA,CAAyBO,MAAM,CAAC;gBACrDH,IAAM,EAAA;AACJd,oBAAAA,WAAAA;oBACAkB,iBAAmBnB,EAAAA,UAAAA;AACnBE,oBAAAA,YAAAA;AACAC,oBAAAA,aAAAA;AACAC,oBAAAA,MAAAA;AACAgB,oBAAAA,SAAAA,EAAW,IAAIH,IAAAA,EAAAA;AACfD,oBAAAA,SAAAA,EAAW,IAAIC,IAAAA;AACjB;AACF,aAAA,CAAA;AACF,SAAA;AAEA;;AAEC,MACD,MAAMX,gBAAAA,CAAAA,CAAiBL,WAAmB,EAAED,UAAkB,EAAA;AAC5D,YAAA,OAAOF,OAAOW,EAAE,CAACC,KAAK,CAACC,yCAAAA,CAAAA,CAAyBU,OAAO,CAAC;gBACtDR,KAAO,EAAA;oBACLM,iBAAmBnB,EAAAA,UAAAA;AACnBC,oBAAAA;AACF;AACF,aAAA,CAAA;AACF,SAAA;AAEA;;MAGA,MAAMqB,qBAAoBrB,WAAmB,EAAA;AAC3C,YAAA,OAAOH,OAAOW,EAAE,CAACC,KAAK,CAACC,yCAAAA,CAAAA,CAAyBU,OAAO,CAAC;gBACtDR,KAAO,EAAA;AACLZ,oBAAAA;AACF;AACF,aAAA,CAAA;AACF;AACF,KAAA;;;;"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AI_LOCALIZATION_JOB_UID } from '../models/ai-localization-job.mjs';
|
|
2
|
+
|
|
3
|
+
const createAILocalizationJobsService = ({ strapi })=>({
|
|
4
|
+
/**
|
|
5
|
+
* Create a new AI localizations job or update an existing one for a document
|
|
6
|
+
* Ensures only one job exists per document
|
|
7
|
+
*/ async upsertJobForDocument ({ documentId, contentType, sourceLocale, targetLocales, status = 'processing' }) {
|
|
8
|
+
// Check if job already exists for this document
|
|
9
|
+
const existingJob = await this.getJobByDocument(contentType, documentId);
|
|
10
|
+
if (existingJob) {
|
|
11
|
+
strapi.log.info(`[AI Localizations Job] Updated existing job for document ${documentId} with status: ${status}`);
|
|
12
|
+
// Update existing job with new data and status
|
|
13
|
+
return strapi.db.query(AI_LOCALIZATION_JOB_UID).update({
|
|
14
|
+
where: {
|
|
15
|
+
id: existingJob.id
|
|
16
|
+
},
|
|
17
|
+
data: {
|
|
18
|
+
contentType,
|
|
19
|
+
sourceLocale,
|
|
20
|
+
targetLocales,
|
|
21
|
+
status,
|
|
22
|
+
updatedAt: new Date()
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
strapi.log.info(`[AI Localizations Job] Created new job for document ${documentId} with status: ${status}`);
|
|
27
|
+
// Create new AI localizations job
|
|
28
|
+
return strapi.db.query(AI_LOCALIZATION_JOB_UID).create({
|
|
29
|
+
data: {
|
|
30
|
+
contentType,
|
|
31
|
+
relatedDocumentId: documentId,
|
|
32
|
+
sourceLocale,
|
|
33
|
+
targetLocales,
|
|
34
|
+
status,
|
|
35
|
+
createdAt: new Date(),
|
|
36
|
+
updatedAt: new Date()
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* Get job by document ID
|
|
42
|
+
*/ async getJobByDocument (contentType, documentId) {
|
|
43
|
+
return strapi.db.query(AI_LOCALIZATION_JOB_UID).findOne({
|
|
44
|
+
where: {
|
|
45
|
+
relatedDocumentId: documentId,
|
|
46
|
+
contentType
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* Get job by content type
|
|
52
|
+
*/ async getJobByContentType (contentType) {
|
|
53
|
+
return strapi.db.query(AI_LOCALIZATION_JOB_UID).findOne({
|
|
54
|
+
where: {
|
|
55
|
+
contentType
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export { createAILocalizationJobsService };
|
|
62
|
+
//# sourceMappingURL=ai-localization-jobs.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-localization-jobs.mjs","sources":["../../../server/src/services/ai-localization-jobs.ts"],"sourcesContent":["import type { Core } from '@strapi/types';\nimport { AI_LOCALIZATION_JOB_UID } from '../models/ai-localization-job';\nimport type { AILocalizationJobs } from '../../../shared/contracts/ai-localization-jobs';\n\nexport const createAILocalizationJobsService = ({ strapi }: { strapi: Core.Strapi }) => ({\n /**\n * Create a new AI localizations job or update an existing one for a document\n * Ensures only one job exists per document\n */\n async upsertJobForDocument({\n documentId,\n contentType,\n sourceLocale,\n targetLocales,\n status = 'processing',\n }: {\n documentId: string;\n contentType: string;\n sourceLocale: string;\n targetLocales: string[];\n status?: AILocalizationJobs['status'];\n }) {\n // Check if job already exists for this document\n const existingJob = await this.getJobByDocument(contentType, documentId);\n\n if (existingJob) {\n strapi.log.info(\n `[AI Localizations Job] Updated existing job for document ${documentId} with status: ${status}`\n );\n // Update existing job with new data and status\n return strapi.db.query(AI_LOCALIZATION_JOB_UID).update({\n where: { id: existingJob.id },\n data: {\n contentType,\n sourceLocale,\n targetLocales,\n status,\n updatedAt: new Date(),\n },\n });\n }\n\n strapi.log.info(\n `[AI Localizations Job] Created new job for document ${documentId} with status: ${status}`\n );\n // Create new AI localizations job\n return strapi.db.query(AI_LOCALIZATION_JOB_UID).create({\n data: {\n contentType,\n relatedDocumentId: documentId,\n sourceLocale,\n targetLocales,\n status,\n createdAt: new Date(),\n updatedAt: new Date(),\n },\n });\n },\n\n /**\n * Get job by document ID\n */\n async getJobByDocument(contentType: string, documentId: string) {\n return strapi.db.query(AI_LOCALIZATION_JOB_UID).findOne({\n where: {\n relatedDocumentId: documentId,\n contentType,\n },\n });\n },\n\n /**\n * Get job by content type\n */\n async getJobByContentType(contentType: string) {\n return strapi.db.query(AI_LOCALIZATION_JOB_UID).findOne({\n where: {\n contentType,\n },\n });\n },\n});\n"],"names":["createAILocalizationJobsService","strapi","upsertJobForDocument","documentId","contentType","sourceLocale","targetLocales","status","existingJob","getJobByDocument","log","info","db","query","AI_LOCALIZATION_JOB_UID","update","where","id","data","updatedAt","Date","create","relatedDocumentId","createdAt","findOne","getJobByContentType"],"mappings":";;MAIaA,+BAAkC,GAAA,CAAC,EAAEC,MAAM,EAA2B,IAAM;AACvF;;;AAGC,MACD,MAAMC,oBAAAA,CAAAA,CAAqB,EACzBC,UAAU,EACVC,WAAW,EACXC,YAAY,EACZC,aAAa,EACbC,MAAAA,GAAS,YAAY,EAOtB,EAAA;;AAEC,YAAA,MAAMC,cAAc,MAAM,IAAI,CAACC,gBAAgB,CAACL,WAAaD,EAAAA,UAAAA,CAAAA;AAE7D,YAAA,IAAIK,WAAa,EAAA;gBACfP,MAAOS,CAAAA,GAAG,CAACC,IAAI,CACb,CAAC,yDAAyD,EAAER,UAAW,CAAA,cAAc,EAAEI,MAAAA,CAAO,CAAC,CAAA;;AAGjG,gBAAA,OAAON,OAAOW,EAAE,CAACC,KAAK,CAACC,uBAAAA,CAAAA,CAAyBC,MAAM,CAAC;oBACrDC,KAAO,EAAA;AAAEC,wBAAAA,EAAAA,EAAIT,YAAYS;AAAG,qBAAA;oBAC5BC,IAAM,EAAA;AACJd,wBAAAA,WAAAA;AACAC,wBAAAA,YAAAA;AACAC,wBAAAA,aAAAA;AACAC,wBAAAA,MAAAA;AACAY,wBAAAA,SAAAA,EAAW,IAAIC,IAAAA;AACjB;AACF,iBAAA,CAAA;AACF;YAEAnB,MAAOS,CAAAA,GAAG,CAACC,IAAI,CACb,CAAC,oDAAoD,EAAER,UAAW,CAAA,cAAc,EAAEI,MAAAA,CAAO,CAAC,CAAA;;AAG5F,YAAA,OAAON,OAAOW,EAAE,CAACC,KAAK,CAACC,uBAAAA,CAAAA,CAAyBO,MAAM,CAAC;gBACrDH,IAAM,EAAA;AACJd,oBAAAA,WAAAA;oBACAkB,iBAAmBnB,EAAAA,UAAAA;AACnBE,oBAAAA,YAAAA;AACAC,oBAAAA,aAAAA;AACAC,oBAAAA,MAAAA;AACAgB,oBAAAA,SAAAA,EAAW,IAAIH,IAAAA,EAAAA;AACfD,oBAAAA,SAAAA,EAAW,IAAIC,IAAAA;AACjB;AACF,aAAA,CAAA;AACF,SAAA;AAEA;;AAEC,MACD,MAAMX,gBAAAA,CAAAA,CAAiBL,WAAmB,EAAED,UAAkB,EAAA;AAC5D,YAAA,OAAOF,OAAOW,EAAE,CAACC,KAAK,CAACC,uBAAAA,CAAAA,CAAyBU,OAAO,CAAC;gBACtDR,KAAO,EAAA;oBACLM,iBAAmBnB,EAAAA,UAAAA;AACnBC,oBAAAA;AACF;AACF,aAAA,CAAA;AACF,SAAA;AAEA;;MAGA,MAAMqB,qBAAoBrB,WAAmB,EAAA;AAC3C,YAAA,OAAOH,OAAOW,EAAE,CAACC,KAAK,CAACC,uBAAAA,CAAAA,CAAyBU,OAAO,CAAC;gBACtDR,KAAO,EAAA;AACLZ,oBAAAA;AACF;AACF,aAAA,CAAA;AACF;AACF,KAAA;;;;"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var utils = require('@strapi/utils');
|
|
4
|
+
var index = require('../utils/index.js');
|
|
5
|
+
|
|
6
|
+
const createAILocalizationsService = ({ strapi })=>{
|
|
7
|
+
// TODO: add a helper function to get the AI server URL
|
|
8
|
+
const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';
|
|
9
|
+
const aiLocalizationJobsService = index.getService('ai-localization-jobs');
|
|
10
|
+
return {
|
|
11
|
+
// Async to avoid changing the signature later (there will be a db check in the future)
|
|
12
|
+
async isEnabled () {
|
|
13
|
+
// Check if future flag is enabled
|
|
14
|
+
const isFutureFlagEnabled = strapi.features.future.isEnabled('unstableAILocalizations');
|
|
15
|
+
if (!isFutureFlagEnabled) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
// Check if user disabled AI features globally
|
|
19
|
+
const isAIEnabled = strapi.config.get('admin.ai.enabled', true);
|
|
20
|
+
if (!isAIEnabled) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
// Check if the user's license grants access to AI features
|
|
24
|
+
const hasAccess = strapi.ee.features.isEnabled('cms-ai');
|
|
25
|
+
if (!hasAccess) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const settings = index.getService('settings');
|
|
29
|
+
const aiSettings = await settings.getSettings();
|
|
30
|
+
if (!aiSettings?.aiLocalizations) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Checks if there are localizations that need to be generated for the given document,
|
|
37
|
+
* and if so, calls the AI service and saves the results to the database.
|
|
38
|
+
* Works for both single and collection types, on create and update.
|
|
39
|
+
*/ async generateDocumentLocalizations ({ model, document }) {
|
|
40
|
+
const isFeatureEnabled = await this.isEnabled();
|
|
41
|
+
if (!isFeatureEnabled) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const schema = strapi.getModel(model);
|
|
45
|
+
const localeService = index.getService('locales');
|
|
46
|
+
// No localizations needed for content types with i18n disabled
|
|
47
|
+
const isLocalizedContentType = index.getService('content-types').isLocalizedContentType(schema);
|
|
48
|
+
if (!isLocalizedContentType) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Don't trigger localizations if the update is on a derived locale, only do it on the default
|
|
52
|
+
const defaultLocale = await localeService.getDefaultLocale();
|
|
53
|
+
if (document?.locale !== defaultLocale) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const documentId = document.documentId;
|
|
57
|
+
if (!documentId) {
|
|
58
|
+
strapi.log.warn(`AI Localizations: missing documentId for ${schema.uid}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Extract only the localized content from the document
|
|
62
|
+
const translateableContent = await utils.traverseEntity(({ key, attribute }, { remove })=>{
|
|
63
|
+
const hasLocalizedOption = attribute?.pluginOptions?.i18n?.localized === true;
|
|
64
|
+
// Only keep fields that actually need to be localized
|
|
65
|
+
// TODO: remove blocks from this list once the AI server can handle it reliably
|
|
66
|
+
if (!hasLocalizedOption || [
|
|
67
|
+
'media',
|
|
68
|
+
'blocks'
|
|
69
|
+
].includes(attribute.type)) {
|
|
70
|
+
remove(key);
|
|
71
|
+
}
|
|
72
|
+
}, {
|
|
73
|
+
schema,
|
|
74
|
+
getModel: strapi.getModel.bind(strapi)
|
|
75
|
+
}, document);
|
|
76
|
+
// Call the AI server to get the localized content
|
|
77
|
+
const localesList = await localeService.find();
|
|
78
|
+
const targetLocales = localesList.filter((l)=>l.code !== document.locale).map((l)=>l.code);
|
|
79
|
+
if (targetLocales.length === 0) {
|
|
80
|
+
strapi.log.info(`AI Localizations: no target locales for ${schema.uid} document ${documentId}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
84
|
+
contentType: model,
|
|
85
|
+
documentId,
|
|
86
|
+
sourceLocale: document.locale,
|
|
87
|
+
targetLocales,
|
|
88
|
+
status: 'processing'
|
|
89
|
+
});
|
|
90
|
+
let token;
|
|
91
|
+
try {
|
|
92
|
+
const tokenData = await strapi.service('admin::user').getAiToken();
|
|
93
|
+
token = tokenData.token;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
96
|
+
documentId,
|
|
97
|
+
contentType: model,
|
|
98
|
+
sourceLocale: document.locale,
|
|
99
|
+
targetLocales,
|
|
100
|
+
status: 'failed'
|
|
101
|
+
});
|
|
102
|
+
throw new Error('Failed to retrieve AI token', {
|
|
103
|
+
cause: error instanceof Error ? error : undefined
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
strapi.log.http('Contacting AI Server for localizations generation');
|
|
107
|
+
const response = await fetch(`${aiServerUrl}/i18n/generate-localizations`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
Authorization: `Bearer ${token}`
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
content: translateableContent,
|
|
115
|
+
sourceLocale: document.locale,
|
|
116
|
+
targetLocales
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
strapi.log.error(`AI Localizations request failed: ${response.status} ${response.statusText}`);
|
|
121
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
122
|
+
documentId,
|
|
123
|
+
contentType: model,
|
|
124
|
+
sourceLocale: document.locale,
|
|
125
|
+
targetLocales,
|
|
126
|
+
status: 'failed'
|
|
127
|
+
});
|
|
128
|
+
throw new Error(`AI Localizations request failed: ${response.statusText}`);
|
|
129
|
+
} else {
|
|
130
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
131
|
+
documentId,
|
|
132
|
+
contentType: model,
|
|
133
|
+
sourceLocale: document.locale,
|
|
134
|
+
targetLocales,
|
|
135
|
+
status: 'completed'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const aiResult = await response.json();
|
|
139
|
+
// Get all media field names dynamically from the schema
|
|
140
|
+
const mediaFields = Object.entries(schema.attributes)// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
141
|
+
.filter(([_, attr])=>attr.type === 'media').map(([key])=>key);
|
|
142
|
+
try {
|
|
143
|
+
await Promise.allSettled(aiResult.localizations.map(async (localization)=>{
|
|
144
|
+
const { content, locale } = localization;
|
|
145
|
+
// Fetch the derived locale document
|
|
146
|
+
const derivedDoc = await strapi.documents(model).findOne({
|
|
147
|
+
documentId,
|
|
148
|
+
locale,
|
|
149
|
+
populate: mediaFields
|
|
150
|
+
});
|
|
151
|
+
// Merge AI content and media fields
|
|
152
|
+
const mergedData = {
|
|
153
|
+
...content
|
|
154
|
+
};
|
|
155
|
+
for (const field of mediaFields){
|
|
156
|
+
// Only copy media if not already set in derived locale
|
|
157
|
+
if (!derivedDoc || !derivedDoc[field]) {
|
|
158
|
+
mergedData[field] = document[field];
|
|
159
|
+
} else {
|
|
160
|
+
mergedData[field] = derivedDoc[field];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return strapi.documents(model).update({
|
|
164
|
+
documentId,
|
|
165
|
+
locale,
|
|
166
|
+
fields: [],
|
|
167
|
+
data: mergedData
|
|
168
|
+
});
|
|
169
|
+
}));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
172
|
+
documentId,
|
|
173
|
+
contentType: model,
|
|
174
|
+
sourceLocale: document.locale,
|
|
175
|
+
targetLocales,
|
|
176
|
+
status: 'failed'
|
|
177
|
+
});
|
|
178
|
+
strapi.log.error('AI Localizations generation failed', error);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
setupMiddleware () {
|
|
182
|
+
strapi.documents.use(async (context, next)=>{
|
|
183
|
+
const result = await next();
|
|
184
|
+
// Only trigger on create/update actions
|
|
185
|
+
if (![
|
|
186
|
+
'create',
|
|
187
|
+
'update'
|
|
188
|
+
].includes(context.action)) {
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
// Check if AI localizations are enabled before triggering
|
|
192
|
+
const isEnabled = await this.isEnabled();
|
|
193
|
+
if (!isEnabled) {
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
// Don't await since localizations should be done in the background without blocking the request
|
|
197
|
+
strapi.plugin('i18n').service('ai-localizations').generateDocumentLocalizations({
|
|
198
|
+
model: context.contentType.uid,
|
|
199
|
+
document: result
|
|
200
|
+
}).catch((error)=>{
|
|
201
|
+
strapi.log.error('AI Localizations generation failed', error);
|
|
202
|
+
});
|
|
203
|
+
return result;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
exports.createAILocalizationsService = createAILocalizationsService;
|
|
210
|
+
//# sourceMappingURL=ai-localizations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-localizations.js","sources":["../../../server/src/services/ai-localizations.ts"],"sourcesContent":["import type { Core, Modules, UID } from '@strapi/types';\nimport { traverseEntity } from '@strapi/utils';\nimport { getService } from '../utils';\n\nconst createAILocalizationsService = ({ strapi }: { strapi: Core.Strapi }) => {\n // TODO: add a helper function to get the AI server URL\n const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';\n const aiLocalizationJobsService = getService('ai-localization-jobs');\n\n return {\n // Async to avoid changing the signature later (there will be a db check in the future)\n async isEnabled() {\n // Check if future flag is enabled\n const isFutureFlagEnabled = strapi.features.future.isEnabled('unstableAILocalizations');\n if (!isFutureFlagEnabled) {\n return false;\n }\n\n // Check if user disabled AI features globally\n const isAIEnabled = strapi.config.get('admin.ai.enabled', true);\n if (!isAIEnabled) {\n return false;\n }\n\n // Check if the user's license grants access to AI features\n const hasAccess = strapi.ee.features.isEnabled('cms-ai');\n if (!hasAccess) {\n return false;\n }\n\n const settings = getService('settings');\n const aiSettings = await settings.getSettings();\n if (!aiSettings?.aiLocalizations) {\n return false;\n }\n\n return true;\n },\n\n /**\n * Checks if there are localizations that need to be generated for the given document,\n * and if so, calls the AI service and saves the results to the database.\n * Works for both single and collection types, on create and update.\n */\n async generateDocumentLocalizations({\n model,\n document,\n }: {\n model: UID.ContentType;\n document: Modules.Documents.AnyDocument;\n }) {\n const isFeatureEnabled = await this.isEnabled();\n if (!isFeatureEnabled) {\n return;\n }\n\n const schema = strapi.getModel(model);\n const localeService = getService('locales');\n\n // No localizations needed for content types with i18n disabled\n const isLocalizedContentType = getService('content-types').isLocalizedContentType(schema);\n if (!isLocalizedContentType) {\n return;\n }\n\n // Don't trigger localizations if the update is on a derived locale, only do it on the default\n const defaultLocale = await localeService.getDefaultLocale();\n if (document?.locale !== defaultLocale) {\n return;\n }\n\n const documentId = document.documentId;\n\n if (!documentId) {\n strapi.log.warn(`AI Localizations: missing documentId for ${schema.uid}`);\n return;\n }\n\n // Extract only the localized content from the document\n const translateableContent = await traverseEntity(\n ({ key, attribute }, { remove }) => {\n const hasLocalizedOption = attribute?.pluginOptions?.i18n?.localized === true;\n // Only keep fields that actually need to be localized\n // TODO: remove blocks from this list once the AI server can handle it reliably\n if (!hasLocalizedOption || ['media', 'blocks'].includes(attribute.type)) {\n remove(key);\n }\n },\n { schema, getModel: strapi.getModel.bind(strapi) },\n document\n );\n\n // Call the AI server to get the localized content\n const localesList = await localeService.find();\n const targetLocales = localesList\n .filter((l) => l.code !== document.locale)\n .map((l) => l.code);\n\n if (targetLocales.length === 0) {\n strapi.log.info(\n `AI Localizations: no target locales for ${schema.uid} document ${documentId}`\n );\n return;\n }\n\n await aiLocalizationJobsService.upsertJobForDocument({\n contentType: model,\n documentId,\n sourceLocale: document.locale,\n targetLocales,\n status: 'processing',\n });\n\n let token: string;\n try {\n const tokenData = await strapi.service('admin::user').getAiToken();\n token = tokenData.token;\n } catch (error) {\n await aiLocalizationJobsService.upsertJobForDocument({\n documentId,\n contentType: model,\n sourceLocale: document.locale,\n targetLocales,\n status: 'failed',\n });\n\n throw new Error('Failed to retrieve AI token', {\n cause: error instanceof Error ? error : undefined,\n });\n }\n\n strapi.log.http('Contacting AI Server for localizations generation');\n const response = await fetch(`${aiServerUrl}/i18n/generate-localizations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n content: translateableContent,\n sourceLocale: document.locale,\n targetLocales,\n }),\n });\n\n if (!response.ok) {\n strapi.log.error(\n `AI Localizations request failed: ${response.status} ${response.statusText}`\n );\n\n await aiLocalizationJobsService.upsertJobForDocument({\n documentId,\n contentType: model,\n sourceLocale: document.locale,\n targetLocales,\n status: 'failed',\n });\n\n throw new Error(`AI Localizations request failed: ${response.statusText}`);\n } else {\n await aiLocalizationJobsService.upsertJobForDocument({\n documentId,\n contentType: model,\n sourceLocale: document.locale,\n targetLocales,\n status: 'completed',\n });\n }\n\n const aiResult = await response.json();\n\n // Get all media field names dynamically from the schema\n const mediaFields = Object.entries(schema.attributes)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n .filter(([_, attr]) => attr.type === 'media')\n .map(([key]) => key);\n\n try {\n await Promise.allSettled(\n aiResult.localizations.map(async (localization: any) => {\n const { content, locale } = localization;\n\n // Fetch the derived locale document\n const derivedDoc = await strapi.documents(model).findOne({\n documentId,\n locale,\n populate: mediaFields,\n });\n\n // Merge AI content and media fields\n const mergedData = { ...content };\n for (const field of mediaFields) {\n // Only copy media if not already set in derived locale\n if (!derivedDoc || !derivedDoc[field]) {\n mergedData[field] = document[field];\n } else {\n mergedData[field] = derivedDoc[field];\n }\n }\n\n return strapi.documents(model).update({\n documentId,\n locale,\n fields: [],\n data: mergedData,\n });\n })\n );\n } catch (error) {\n await aiLocalizationJobsService.upsertJobForDocument({\n documentId,\n contentType: model,\n sourceLocale: document.locale,\n targetLocales,\n status: 'failed',\n });\n strapi.log.error('AI Localizations generation failed', error);\n }\n },\n setupMiddleware() {\n strapi.documents.use(async (context, next) => {\n const result = await next();\n\n // Only trigger on create/update actions\n if (!['create', 'update'].includes(context.action)) {\n return result;\n }\n\n // Check if AI localizations are enabled before triggering\n const isEnabled = await this.isEnabled();\n if (!isEnabled) {\n return result;\n }\n\n // Don't await since localizations should be done in the background without blocking the request\n strapi\n .plugin('i18n')\n .service('ai-localizations')\n .generateDocumentLocalizations({\n model: context.contentType.uid,\n document: result,\n })\n .catch((error: any) => {\n strapi.log.error('AI Localizations generation failed', error);\n });\n\n return result;\n });\n },\n };\n};\n\nexport { createAILocalizationsService };\n"],"names":["createAILocalizationsService","strapi","aiServerUrl","process","env","STRAPI_AI_URL","aiLocalizationJobsService","getService","isEnabled","isFutureFlagEnabled","features","future","isAIEnabled","config","get","hasAccess","ee","settings","aiSettings","getSettings","aiLocalizations","generateDocumentLocalizations","model","document","isFeatureEnabled","schema","getModel","localeService","isLocalizedContentType","defaultLocale","getDefaultLocale","locale","documentId","log","warn","uid","translateableContent","traverseEntity","key","attribute","remove","hasLocalizedOption","pluginOptions","i18n","localized","includes","type","bind","localesList","find","targetLocales","filter","l","code","map","length","info","upsertJobForDocument","contentType","sourceLocale","status","token","tokenData","service","getAiToken","error","Error","cause","undefined","http","response","fetch","method","headers","Authorization","body","JSON","stringify","content","ok","statusText","aiResult","json","mediaFields","Object","entries","attributes","_","attr","Promise","allSettled","localizations","localization","derivedDoc","documents","findOne","populate","mergedData","field","update","fields","data","setupMiddleware","use","context","next","result","action","plugin","catch"],"mappings":"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"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { traverseEntity } from '@strapi/utils';
|
|
2
|
+
import { getService } from '../utils/index.mjs';
|
|
3
|
+
|
|
4
|
+
const createAILocalizationsService = ({ strapi })=>{
|
|
5
|
+
// TODO: add a helper function to get the AI server URL
|
|
6
|
+
const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';
|
|
7
|
+
const aiLocalizationJobsService = getService('ai-localization-jobs');
|
|
8
|
+
return {
|
|
9
|
+
// Async to avoid changing the signature later (there will be a db check in the future)
|
|
10
|
+
async isEnabled () {
|
|
11
|
+
// Check if future flag is enabled
|
|
12
|
+
const isFutureFlagEnabled = strapi.features.future.isEnabled('unstableAILocalizations');
|
|
13
|
+
if (!isFutureFlagEnabled) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
// Check if user disabled AI features globally
|
|
17
|
+
const isAIEnabled = strapi.config.get('admin.ai.enabled', true);
|
|
18
|
+
if (!isAIEnabled) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
// Check if the user's license grants access to AI features
|
|
22
|
+
const hasAccess = strapi.ee.features.isEnabled('cms-ai');
|
|
23
|
+
if (!hasAccess) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const settings = getService('settings');
|
|
27
|
+
const aiSettings = await settings.getSettings();
|
|
28
|
+
if (!aiSettings?.aiLocalizations) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
/**
|
|
34
|
+
* Checks if there are localizations that need to be generated for the given document,
|
|
35
|
+
* and if so, calls the AI service and saves the results to the database.
|
|
36
|
+
* Works for both single and collection types, on create and update.
|
|
37
|
+
*/ async generateDocumentLocalizations ({ model, document }) {
|
|
38
|
+
const isFeatureEnabled = await this.isEnabled();
|
|
39
|
+
if (!isFeatureEnabled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const schema = strapi.getModel(model);
|
|
43
|
+
const localeService = getService('locales');
|
|
44
|
+
// No localizations needed for content types with i18n disabled
|
|
45
|
+
const isLocalizedContentType = getService('content-types').isLocalizedContentType(schema);
|
|
46
|
+
if (!isLocalizedContentType) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Don't trigger localizations if the update is on a derived locale, only do it on the default
|
|
50
|
+
const defaultLocale = await localeService.getDefaultLocale();
|
|
51
|
+
if (document?.locale !== defaultLocale) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const documentId = document.documentId;
|
|
55
|
+
if (!documentId) {
|
|
56
|
+
strapi.log.warn(`AI Localizations: missing documentId for ${schema.uid}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Extract only the localized content from the document
|
|
60
|
+
const translateableContent = await traverseEntity(({ key, attribute }, { remove })=>{
|
|
61
|
+
const hasLocalizedOption = attribute?.pluginOptions?.i18n?.localized === true;
|
|
62
|
+
// Only keep fields that actually need to be localized
|
|
63
|
+
// TODO: remove blocks from this list once the AI server can handle it reliably
|
|
64
|
+
if (!hasLocalizedOption || [
|
|
65
|
+
'media',
|
|
66
|
+
'blocks'
|
|
67
|
+
].includes(attribute.type)) {
|
|
68
|
+
remove(key);
|
|
69
|
+
}
|
|
70
|
+
}, {
|
|
71
|
+
schema,
|
|
72
|
+
getModel: strapi.getModel.bind(strapi)
|
|
73
|
+
}, document);
|
|
74
|
+
// Call the AI server to get the localized content
|
|
75
|
+
const localesList = await localeService.find();
|
|
76
|
+
const targetLocales = localesList.filter((l)=>l.code !== document.locale).map((l)=>l.code);
|
|
77
|
+
if (targetLocales.length === 0) {
|
|
78
|
+
strapi.log.info(`AI Localizations: no target locales for ${schema.uid} document ${documentId}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
82
|
+
contentType: model,
|
|
83
|
+
documentId,
|
|
84
|
+
sourceLocale: document.locale,
|
|
85
|
+
targetLocales,
|
|
86
|
+
status: 'processing'
|
|
87
|
+
});
|
|
88
|
+
let token;
|
|
89
|
+
try {
|
|
90
|
+
const tokenData = await strapi.service('admin::user').getAiToken();
|
|
91
|
+
token = tokenData.token;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
94
|
+
documentId,
|
|
95
|
+
contentType: model,
|
|
96
|
+
sourceLocale: document.locale,
|
|
97
|
+
targetLocales,
|
|
98
|
+
status: 'failed'
|
|
99
|
+
});
|
|
100
|
+
throw new Error('Failed to retrieve AI token', {
|
|
101
|
+
cause: error instanceof Error ? error : undefined
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
strapi.log.http('Contacting AI Server for localizations generation');
|
|
105
|
+
const response = await fetch(`${aiServerUrl}/i18n/generate-localizations`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
Authorization: `Bearer ${token}`
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
content: translateableContent,
|
|
113
|
+
sourceLocale: document.locale,
|
|
114
|
+
targetLocales
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
strapi.log.error(`AI Localizations request failed: ${response.status} ${response.statusText}`);
|
|
119
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
120
|
+
documentId,
|
|
121
|
+
contentType: model,
|
|
122
|
+
sourceLocale: document.locale,
|
|
123
|
+
targetLocales,
|
|
124
|
+
status: 'failed'
|
|
125
|
+
});
|
|
126
|
+
throw new Error(`AI Localizations request failed: ${response.statusText}`);
|
|
127
|
+
} else {
|
|
128
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
129
|
+
documentId,
|
|
130
|
+
contentType: model,
|
|
131
|
+
sourceLocale: document.locale,
|
|
132
|
+
targetLocales,
|
|
133
|
+
status: 'completed'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const aiResult = await response.json();
|
|
137
|
+
// Get all media field names dynamically from the schema
|
|
138
|
+
const mediaFields = Object.entries(schema.attributes)// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
139
|
+
.filter(([_, attr])=>attr.type === 'media').map(([key])=>key);
|
|
140
|
+
try {
|
|
141
|
+
await Promise.allSettled(aiResult.localizations.map(async (localization)=>{
|
|
142
|
+
const { content, locale } = localization;
|
|
143
|
+
// Fetch the derived locale document
|
|
144
|
+
const derivedDoc = await strapi.documents(model).findOne({
|
|
145
|
+
documentId,
|
|
146
|
+
locale,
|
|
147
|
+
populate: mediaFields
|
|
148
|
+
});
|
|
149
|
+
// Merge AI content and media fields
|
|
150
|
+
const mergedData = {
|
|
151
|
+
...content
|
|
152
|
+
};
|
|
153
|
+
for (const field of mediaFields){
|
|
154
|
+
// Only copy media if not already set in derived locale
|
|
155
|
+
if (!derivedDoc || !derivedDoc[field]) {
|
|
156
|
+
mergedData[field] = document[field];
|
|
157
|
+
} else {
|
|
158
|
+
mergedData[field] = derivedDoc[field];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return strapi.documents(model).update({
|
|
162
|
+
documentId,
|
|
163
|
+
locale,
|
|
164
|
+
fields: [],
|
|
165
|
+
data: mergedData
|
|
166
|
+
});
|
|
167
|
+
}));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await aiLocalizationJobsService.upsertJobForDocument({
|
|
170
|
+
documentId,
|
|
171
|
+
contentType: model,
|
|
172
|
+
sourceLocale: document.locale,
|
|
173
|
+
targetLocales,
|
|
174
|
+
status: 'failed'
|
|
175
|
+
});
|
|
176
|
+
strapi.log.error('AI Localizations generation failed', error);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
setupMiddleware () {
|
|
180
|
+
strapi.documents.use(async (context, next)=>{
|
|
181
|
+
const result = await next();
|
|
182
|
+
// Only trigger on create/update actions
|
|
183
|
+
if (![
|
|
184
|
+
'create',
|
|
185
|
+
'update'
|
|
186
|
+
].includes(context.action)) {
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
// Check if AI localizations are enabled before triggering
|
|
190
|
+
const isEnabled = await this.isEnabled();
|
|
191
|
+
if (!isEnabled) {
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
// Don't await since localizations should be done in the background without blocking the request
|
|
195
|
+
strapi.plugin('i18n').service('ai-localizations').generateDocumentLocalizations({
|
|
196
|
+
model: context.contentType.uid,
|
|
197
|
+
document: result
|
|
198
|
+
}).catch((error)=>{
|
|
199
|
+
strapi.log.error('AI Localizations generation failed', error);
|
|
200
|
+
});
|
|
201
|
+
return result;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export { createAILocalizationsService };
|
|
208
|
+
//# sourceMappingURL=ai-localizations.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-localizations.mjs","sources":["../../../server/src/services/ai-localizations.ts"],"sourcesContent":["import type { Core, Modules, UID } from '@strapi/types';\nimport { traverseEntity } from '@strapi/utils';\nimport { getService } from '../utils';\n\nconst createAILocalizationsService = ({ strapi }: { strapi: Core.Strapi }) => {\n // TODO: add a helper function to get the AI server URL\n const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';\n const aiLocalizationJobsService = getService('ai-localization-jobs');\n\n return {\n // Async to avoid changing the signature later (there will be a db check in the future)\n async isEnabled() {\n // Check if future flag is enabled\n const isFutureFlagEnabled = strapi.features.future.isEnabled('unstableAILocalizations');\n if (!isFutureFlagEnabled) {\n return false;\n }\n\n // Check if user disabled AI features globally\n const isAIEnabled = strapi.config.get('admin.ai.enabled', true);\n if (!isAIEnabled) {\n return false;\n }\n\n // Check if the user's license grants access to AI features\n const hasAccess = strapi.ee.features.isEnabled('cms-ai');\n if (!hasAccess) {\n return false;\n }\n\n const settings = getService('settings');\n const aiSettings = await settings.getSettings();\n if (!aiSettings?.aiLocalizations) {\n return false;\n }\n\n return true;\n },\n\n /**\n * Checks if there are localizations that need to be generated for the given document,\n * and if so, calls the AI service and saves the results to the database.\n * Works for both single and collection types, on create and update.\n */\n async generateDocumentLocalizations({\n model,\n document,\n }: {\n model: UID.ContentType;\n document: Modules.Documents.AnyDocument;\n }) {\n const isFeatureEnabled = await this.isEnabled();\n if (!isFeatureEnabled) {\n return;\n }\n\n const schema = strapi.getModel(model);\n const localeService = getService('locales');\n\n // No localizations needed for content types with i18n disabled\n const isLocalizedContentType = getService('content-types').isLocalizedContentType(schema);\n if (!isLocalizedContentType) {\n return;\n }\n\n // Don't trigger localizations if the update is on a derived locale, only do it on the default\n const defaultLocale = await localeService.getDefaultLocale();\n if (document?.locale !== defaultLocale) {\n return;\n }\n\n const documentId = document.documentId;\n\n if (!documentId) {\n strapi.log.warn(`AI Localizations: missing documentId for ${schema.uid}`);\n return;\n }\n\n // Extract only the localized content from the document\n const translateableContent = await traverseEntity(\n ({ key, attribute }, { remove }) => {\n const hasLocalizedOption = attribute?.pluginOptions?.i18n?.localized === true;\n // Only keep fields that actually need to be localized\n // TODO: remove blocks from this list once the AI server can handle it reliably\n if (!hasLocalizedOption || ['media', 'blocks'].includes(attribute.type)) {\n remove(key);\n }\n },\n { schema, getModel: strapi.getModel.bind(strapi) },\n document\n );\n\n // Call the AI server to get the localized content\n const localesList = await localeService.find();\n const targetLocales = localesList\n .filter((l) => l.code !== document.locale)\n .map((l) => l.code);\n\n if (targetLocales.length === 0) {\n strapi.log.info(\n `AI Localizations: no target locales for ${schema.uid} document ${documentId}`\n );\n return;\n }\n\n await aiLocalizationJobsService.upsertJobForDocument({\n contentType: model,\n documentId,\n sourceLocale: document.locale,\n targetLocales,\n status: 'processing',\n });\n\n let token: string;\n try {\n const tokenData = await strapi.service('admin::user').getAiToken();\n token = tokenData.token;\n } catch (error) {\n await aiLocalizationJobsService.upsertJobForDocument({\n documentId,\n contentType: model,\n sourceLocale: document.locale,\n targetLocales,\n status: 'failed',\n });\n\n throw new Error('Failed to retrieve AI token', {\n cause: error instanceof Error ? error : undefined,\n });\n }\n\n strapi.log.http('Contacting AI Server for localizations generation');\n const response = await fetch(`${aiServerUrl}/i18n/generate-localizations`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n content: translateableContent,\n sourceLocale: document.locale,\n targetLocales,\n }),\n });\n\n if (!response.ok) {\n strapi.log.error(\n `AI Localizations request failed: ${response.status} ${response.statusText}`\n );\n\n await aiLocalizationJobsService.upsertJobForDocument({\n documentId,\n contentType: model,\n sourceLocale: document.locale,\n targetLocales,\n status: 'failed',\n });\n\n throw new Error(`AI Localizations request failed: ${response.statusText}`);\n } else {\n await aiLocalizationJobsService.upsertJobForDocument({\n documentId,\n contentType: model,\n sourceLocale: document.locale,\n targetLocales,\n status: 'completed',\n });\n }\n\n const aiResult = await response.json();\n\n // Get all media field names dynamically from the schema\n const mediaFields = Object.entries(schema.attributes)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n .filter(([_, attr]) => attr.type === 'media')\n .map(([key]) => key);\n\n try {\n await Promise.allSettled(\n aiResult.localizations.map(async (localization: any) => {\n const { content, locale } = localization;\n\n // Fetch the derived locale document\n const derivedDoc = await strapi.documents(model).findOne({\n documentId,\n locale,\n populate: mediaFields,\n });\n\n // Merge AI content and media fields\n const mergedData = { ...content };\n for (const field of mediaFields) {\n // Only copy media if not already set in derived locale\n if (!derivedDoc || !derivedDoc[field]) {\n mergedData[field] = document[field];\n } else {\n mergedData[field] = derivedDoc[field];\n }\n }\n\n return strapi.documents(model).update({\n documentId,\n locale,\n fields: [],\n data: mergedData,\n });\n })\n );\n } catch (error) {\n await aiLocalizationJobsService.upsertJobForDocument({\n documentId,\n contentType: model,\n sourceLocale: document.locale,\n targetLocales,\n status: 'failed',\n });\n strapi.log.error('AI Localizations generation failed', error);\n }\n },\n setupMiddleware() {\n strapi.documents.use(async (context, next) => {\n const result = await next();\n\n // Only trigger on create/update actions\n if (!['create', 'update'].includes(context.action)) {\n return result;\n }\n\n // Check if AI localizations are enabled before triggering\n const isEnabled = await this.isEnabled();\n if (!isEnabled) {\n return result;\n }\n\n // Don't await since localizations should be done in the background without blocking the request\n strapi\n .plugin('i18n')\n .service('ai-localizations')\n .generateDocumentLocalizations({\n model: context.contentType.uid,\n document: result,\n })\n .catch((error: any) => {\n strapi.log.error('AI Localizations generation failed', error);\n });\n\n return result;\n });\n },\n };\n};\n\nexport { createAILocalizationsService };\n"],"names":["createAILocalizationsService","strapi","aiServerUrl","process","env","STRAPI_AI_URL","aiLocalizationJobsService","getService","isEnabled","isFutureFlagEnabled","features","future","isAIEnabled","config","get","hasAccess","ee","settings","aiSettings","getSettings","aiLocalizations","generateDocumentLocalizations","model","document","isFeatureEnabled","schema","getModel","localeService","isLocalizedContentType","defaultLocale","getDefaultLocale","locale","documentId","log","warn","uid","translateableContent","traverseEntity","key","attribute","remove","hasLocalizedOption","pluginOptions","i18n","localized","includes","type","bind","localesList","find","targetLocales","filter","l","code","map","length","info","upsertJobForDocument","contentType","sourceLocale","status","token","tokenData","service","getAiToken","error","Error","cause","undefined","http","response","fetch","method","headers","Authorization","body","JSON","stringify","content","ok","statusText","aiResult","json","mediaFields","Object","entries","attributes","_","attr","Promise","allSettled","localizations","localization","derivedDoc","documents","findOne","populate","mergedData","field","update","fields","data","setupMiddleware","use","context","next","result","action","plugin","catch"],"mappings":"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"}
|