@@ -8,7 +8,7 @@ import fs from 'fs-extra';
88import chokidar from 'chokidar' ;
99import { AsyncQueue } from '@sapphire/async-queue' ;
1010import getFlagEmoji from 'country-flag-svg' ;
11- import { parse } from 'bcp-47'
11+ import { parse } from 'bcp-47' ;
1212
1313const processFrontendMessagesQueue = new AsyncQueue ( ) ;
1414
@@ -172,6 +172,14 @@ export default class I18nPlugin extends AdminForthPlugin {
172172 }
173173 } ) ;
174174
175+ if ( this . options . translateLangAsBCP47Code ) {
176+ for ( const [ lang , bcp47 ] of Object . entries ( this . options . translateLangAsBCP47Code ) ) {
177+ if ( ! this . options . supportedLanguages . includes ( lang as SupportedLanguage ) ) {
178+ throw new Error ( `Invalid language code ${ lang } in translateLangAsBCP47Code. It must be one of the supportedLanguages.` ) ;
179+ }
180+ }
181+ }
182+
175183 this . externalAppOnly = this . options . externalAppOnly === true ;
176184
177185 // find primary key field
@@ -440,46 +448,27 @@ export default class I18nPlugin extends AdminForthPlugin {
440448 }
441449
442450 // add bulk action
443- if ( ! resourceConfig . options . bulkActions ) {
444- resourceConfig . options . bulkActions = [ ] ;
451+
452+ const pageInjection = {
453+ file : this . componentPath ( 'BulkActionButton.vue' ) ,
454+ meta : {
455+ supportedLanguages : this . options . supportedLanguages ,
456+ pluginInstanceId : this . pluginInstanceId ,
457+ }
445458 }
446-
447- if ( this . options . completeAdapter ) {
448- resourceConfig . options . bulkActions . push (
449- {
450- id : 'translate_all' ,
451- label : 'Translate selected' ,
452- icon : 'flowbite:language-outline' ,
453- badge : 'AI' ,
454- // if optional `confirm` is provided, user will be asked to confirm action
455- confirm : 'Are you sure you want to translate selected items? Only empty strings will be translated' ,
456- allowed : async ( { resource, adminUser, selectedIds, allowedActions } ) => {
457- process . env . HEAVY_DEBUG && console . log ( 'allowedActions' , JSON . stringify ( allowedActions ) ) ;
458- return allowedActions . edit ;
459- } ,
460- action : async ( { selectedIds, tr } ) => {
461- let translatedCount = 0 ;
462- try {
463- translatedCount = await this . bulkTranslate ( { selectedIds } ) ;
464- } catch ( e ) {
465- process . env . HEAVY_DEBUG && console . error ( '🪲⛔ bulkTranslate error' , e ) ;
466- if ( e instanceof AiTranslateError ) {
467- return { ok : false , error : e . message } ;
468- }
469- throw e ;
470- }
471- this . updateUntranslatedMenuBadge ( ) ;
472- return {
473- ok : true ,
474- error : undefined ,
475- successMessage : await tr ( `Translated {count} items` , 'backend' , {
476- count : translatedCount ,
477- } ) ,
478- } ;
479- }
480- }
481- ) ;
482- } ;
459+
460+ if ( ! resourceConfig . options . pageInjections ) {
461+ resourceConfig . options . pageInjections = { } ;
462+ }
463+ if ( ! resourceConfig . options . pageInjections . list ) {
464+ resourceConfig . options . pageInjections . list = { } ;
465+ }
466+ if ( ! resourceConfig . options . pageInjections . list . beforeActionButtons ) {
467+ resourceConfig . options . pageInjections . list . beforeActionButtons = [ ] ;
468+ }
469+
470+ ( resourceConfig . options . pageInjections . list . beforeActionButtons as AdminForthComponentDeclaration [ ] ) . push ( pageInjection ) ;
471+
483472
484473 // if there is menu item with resourceId, add .badge function showing number of untranslated strings
485474 const addBadgeCountToMenuItem = ( menuItem : AdminForthConfigMenuItem ) => {
@@ -517,6 +506,7 @@ export default class I18nPlugin extends AdminForthPlugin {
517506 return [ ] ;
518507 }
519508
509+ const replacedLanguageCodeForTranslations = this . options . translateLangAsBCP47Code && langIsoCode . length === 2 ? this . options . translateLangAsBCP47Code [ langIsoCode as any ] : null ;
520510 if ( strings . length > maxKeysInOneReq ) {
521511 let totalTranslated = [ ] ;
522512 for ( let i = 0 ; i < strings . length ; i += maxKeysInOneReq ) {
@@ -527,14 +517,15 @@ export default class I18nPlugin extends AdminForthPlugin {
527517 }
528518 return totalTranslated ;
529519 }
520+ const langCode = replacedLanguageCodeForTranslations ? replacedLanguageCodeForTranslations : langIsoCode ;
530521 const lang = langIsoCode ;
531522 const primaryLang = getPrimaryLanguageCode ( lang ) ;
532523 const langName = iso6391 . getName ( primaryLang ) ;
533524 const requestSlavicPlurals = Object . keys ( SLAVIC_PLURAL_EXAMPLES ) . includes ( primaryLang ) && plurals ;
534525 const region = String ( lang ) . split ( '-' ) [ 1 ] ?. toUpperCase ( ) || '' ;
535526 const prompt = `
536- I need to translate strings in JSON to ${ langName } language ( ISO 639-1 code ${ lang } ) from English for my web app.
537- ${ region ? `Use the regional conventions for ${ lang } (region ${ region } ), including spelling, punctuation, and formatting.` : '' }
527+ I need to translate strings in JSON to ${ langName } language ${ replacedLanguageCodeForTranslations || lang . length > 2 ? `BCP-47 code ${ langCode } ` : ` ISO 639-1 code ${ langIsoCode } ` } from English for my web app.
528+ ${ region ? `Use the regional conventions for ${ langCode } (region ${ region } ), including spelling, punctuation, and formatting.` : '' }
538529 ${ requestSlavicPlurals ? `You should provide 4 slavic forms (in format "zero count | singular count | 2-4 | 5+") e.g. "apple | apples" should become "${ SLAVIC_PLURAL_EXAMPLES [ lang ] } "` : '' }
539530 Keep keys, as is, write translation into values! If keys have variables (in curly brackets), then translated strings should have them as well (variables itself should not be translated). Here are the strings:
540531
@@ -548,11 +539,31 @@ export default class I18nPlugin extends AdminForthPlugin {
548539 \`\`\`
549540 ` ;
550541
542+ const jsonSchemaProperties = { } ;
543+ strings . forEach ( s => {
544+ jsonSchemaProperties [ s . en_string ] = {
545+ type : 'string' ,
546+ minLength : 1 ,
547+ } ;
548+ } ) ;
549+
550+ const jsonSchemaRequired = strings . map ( s => s . en_string ) ;
551+
551552 // call OpenAI
552553 const resp = await this . options . completeAdapter . complete (
553554 prompt ,
554555 [ ] ,
555556 prompt . length * 2 ,
557+ {
558+ json_schema : {
559+ name : "translation_response" ,
560+ schema : {
561+ type : "object" ,
562+ properties : jsonSchemaProperties ,
563+ required : jsonSchemaRequired ,
564+ } ,
565+ } ,
566+ }
556567 ) ;
557568
558569 process . env . HEAVY_DEBUG && console . log ( `🪲🔪LLM resp >> ${ prompt . length } , <<${ resp . content . length } :\n\n` , JSON . stringify ( resp ) ) ;
@@ -568,7 +579,7 @@ export default class I18nPlugin extends AdminForthPlugin {
568579 // ```
569580 let res ;
570581 try {
571- res = resp . content . split ( "```json" ) [ 1 ] . split ( "```" ) [ 0 ] ;
582+ res = resp . content // .split("```json")[1].split("```")[0];
572583 } catch ( e ) {
573584 console . error ( `Error in parsing LLM resp: ${ resp } \n Prompt was: ${ prompt } \n Resp was: ${ JSON . stringify ( resp ) } ` , ) ;
574585 return [ ] ;
@@ -613,7 +624,7 @@ export default class I18nPlugin extends AdminForthPlugin {
613624 }
614625
615626 // returns translated count
616- async bulkTranslate ( { selectedIds } : { selectedIds : string [ ] } ) : Promise < number > {
627+ async bulkTranslate ( { selectedIds, selectedLanguages } : { selectedIds : string [ ] , selectedLanguages ?: SupportedLanguage [ ] } ) : Promise < number > {
617628
618629 const needToTranslateByLang : Partial <
619630 Record <
@@ -626,8 +637,8 @@ export default class I18nPlugin extends AdminForthPlugin {
626637 > = { } ;
627638
628639 const translations = await this . adminforth . resource ( this . resourceConfig . resourceId ) . list ( Filters . IN ( this . primaryKeyFieldName , selectedIds ) ) ;
629-
630- for ( const lang of this . options . supportedLanguages ) {
640+ const languagesToProcess = selectedLanguages || this . options . supportedLanguages ;
641+ for ( const lang of languagesToProcess ) {
631642 if ( lang === 'en' ) {
632643 // all strings are in English, no need to translate
633644 continue ;
@@ -1057,6 +1068,36 @@ export default class I18nPlugin extends AdminForthPlugin {
10571068 }
10581069 } ) ;
10591070
1071+ server . endpoint ( {
1072+ method : 'POST' ,
1073+ path : `/plugin/${ this . pluginInstanceId } /translate-selected-to-languages` ,
1074+ noAuth : false ,
1075+ handler : async ( { body, tr } ) => {
1076+ const selectedLanguages = body . selectedLanguages ;
1077+ const selectedIds = body . selectedIds ;
1078+
1079+ let translatedCount = 0 ;
1080+ try {
1081+ console . log ( '🪲translate-selected-to-languages' , { selectedLanguages, selectedIds } ) ;
1082+ translatedCount = await this . bulkTranslate ( { selectedIds, selectedLanguages } ) ;
1083+ } catch ( e ) {
1084+ process . env . HEAVY_DEBUG && console . error ( '🪲⛔ bulkTranslate error' , e ) ;
1085+ if ( e instanceof AiTranslateError ) {
1086+ return { ok : false , error : e . message } ;
1087+ }
1088+ throw e ;
1089+ }
1090+ this . updateUntranslatedMenuBadge ( ) ;
1091+ return {
1092+ ok : true ,
1093+ error : undefined ,
1094+ successMessage : await tr ( `Translated {count} items` , 'backend' , {
1095+ count : translatedCount ,
1096+ } ) ,
1097+ } ;
1098+ }
1099+ } ) ;
1100+
10601101 }
10611102
10621103}
0 commit comments