11import AdminForth , { AdminForthPlugin , Filters , suggestIfTypo , AdminForthDataTypes } from "adminforth" ;
22import type { IAdminForth , IHttpServer , AdminForthComponentDeclaration , AdminForthResourceColumn , AdminForthResource , BeforeLoginConfirmationFunction } from "adminforth" ;
33import type { PluginOptions } from './types.js' ;
4- import iso6391 from 'iso-639-1' ;
4+ import iso6391 , { LanguageCode } from 'iso-639-1' ;
5+ import path from 'path' ;
6+ import fs from 'fs-extra' ;
7+ import chokidar from 'chokidar' ;
8+
9+ interface ICachingAdapter {
10+ get ( key : string ) : Promise < any > ;
11+ set ( key : string , value : any ) : Promise < void > ;
12+ clear ( key : string ) : Promise < void > ;
13+ }
14+
15+ class CachingAdapterMemory implements ICachingAdapter {
16+ cache : Record < string , any > = { } ;
17+ async get ( key : string ) {
18+ return this . cache [ key ] ;
19+ }
20+ async set ( key : string , value : any ) {
21+ this . cache [ key ] = value ;
22+ }
23+ async clear ( key : string ) {
24+ delete this . cache [ key ] ;
25+ }
26+ }
27+
528
629export default class OpenSignupPlugin extends AdminForthPlugin {
730 options : PluginOptions ;
831 emailField : AdminForthResourceColumn ;
932 passwordField : AdminForthResourceColumn ;
1033 authResource : AdminForthResource ;
1134 emailConfirmedField ?: AdminForthResourceColumn ;
12-
35+ trFieldNames : Partial < Record < LanguageCode , string > > ;
36+ enFieldName : string ;
37+ cache : ICachingAdapter ;
38+
1339 adminforth : IAdminForth ;
1440
1541 constructor ( options : PluginOptions ) {
1642 super ( options , import . meta. url ) ;
1743 this . options = options ;
44+ this . cache = new CachingAdapterMemory ( ) ;
45+ this . trFieldNames = { } ;
1846 }
1947
2048 async modifyResourceConfig ( adminforth : IAdminForth , resourceConfig : AdminForthResource ) {
2149 super . modifyResourceConfig ( adminforth , resourceConfig ) ;
2250
2351 // check each supported language is valid ISO 639-1 code
2452 this . options . supportedLanguages . forEach ( ( lang ) => {
25- console . log ( 'lang' , lang ) ;
2653 if ( ! iso6391 . validate ( lang ) ) {
2754 throw new Error ( `Invalid language code ${ lang } , please define valid ISO 639-1 language code (2 lowercase letters)` ) ;
2855 }
2956 } ) ;
3057
3158
59+ // parse trFieldNames
60+ for ( const lang of this . options . supportedLanguages ) {
61+ if ( lang === 'en' ) {
62+ continue ;
63+ }
64+ if ( this . options . translationFieldNames ?. [ lang ] ) {
65+ this . trFieldNames [ lang ] = this . options . translationFieldNames [ lang ] ;
66+ } else {
67+ this . trFieldNames [ lang ] = lang + '_string' ;
68+ }
69+ // find column by name
70+ const column = resourceConfig . columns . find ( c => c . name === this . trFieldNames [ lang ] ) ;
71+ if ( ! column ) {
72+ throw new Error ( `Field ${ this . trFieldNames [ lang ] } not found for storing translation for language ${ lang }
73+ in resource ${ resourceConfig . resourceId } , consider adding it to columns or change trFieldNames option to remap it to existing column` ) ;
74+ }
75+ }
76+
77+ this . enFieldName = this . trFieldNames [ 'en' ] || 'en_string' ;
3278
79+ // if not enFieldName column is not found, throw error
80+ const enColumn = resourceConfig . columns . find ( c => c . name === this . enFieldName ) ;
81+ if ( ! enColumn ) {
82+ throw new Error ( `Field ${ this . enFieldName } not found column to store english original string in resource ${ resourceConfig . resourceId } ` ) ;
83+ }
84+ // for faster performance it should be a primary key
85+ if ( ! enColumn . primaryKey ) {
86+ throw new Error ( `Field ${ this . enFieldName } should be primary key in resource ${ resourceConfig . resourceId } ` ) ;
87+ }
88+
89+ // if sourceFieldName defined, check it exists
90+ if ( this . options . sourceFieldName ) {
91+ if ( ! resourceConfig . columns . find ( c => c . name === this . options . sourceFieldName ) ) {
92+ throw new Error ( `Field ${ this . options . sourceFieldName } not found in resource ${ resourceConfig . resourceId } ` ) ;
93+ }
94+ }
3395
3496 // add underLogin component
3597 ( adminforth . config . customization . loginPageInjections . underInputs as AdminForthComponentDeclaration [ ] ) . push ( {
@@ -48,9 +110,78 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
48110 } ) ;
49111
50112 }
113+
114+ async processExtractedMessages ( adminforth : IAdminForth , filePath : string ) {
115+ // messages file is in i18n-messages.json
116+ let messages ;
117+ try {
118+ messages = await fs . readJson ( filePath ) ;
119+ process . env . HEAVY_DEBUG && console . info ( '🐛 Messages file found' ) ;
120+
121+ } catch ( e ) {
122+ process . env . HEAVY_DEBUG && console . error ( '🐛 Messages file not yet exists, probably npm run i18n:extract not finished/started yet, might be ok' ) ;
123+ return ;
124+ }
125+ console . log ( '🪲messages' , messages ) ;
126+ // loop over missingKeys[i].path and add them to database if not exists
127+ await Promise . all ( messages . missingKeys . map ( async ( missingKey : any ) => {
128+ const key = missingKey . path ;
129+ const file = missingKey . file ;
130+ const source = 'frontend' ;
131+ const exists = await adminforth . resource ( this . resourceConfig . resourceId ) . count ( Filters . EQ ( this . enFieldName , key ) ) ;
132+ console . log ( '🪲exists' , exists ) ;
133+ if ( exists ) {
134+ return ;
135+ }
136+ const record = {
137+ [ this . enFieldName ] : key ,
138+ ...( this . options . sourceFieldName ? { [ this . options . sourceFieldName ] : `${ source } :${ file } ` } : { } ) ,
139+ } ;
140+ await adminforth . resource ( this . resourceConfig . resourceId ) . create ( record ) ;
141+ } ) )
142+
143+
144+ }
145+
146+
147+ async tryProcessAndWatch ( adminforth : IAdminForth ) {
148+ const spaDir = adminforth . codeInjector . spaTmpPath ( ) ;
149+ // messages file is in i18n-messages.json
150+ const messagesFile = path . join ( spaDir , '..' , 'spa_tmp' , 'i18n-messages.json' ) ;
151+ console . log ( '🪲messagesFile' , messagesFile ) ;
152+ this . processExtractedMessages ( adminforth , messagesFile ) ;
153+ // we use watcher because file can't be yet created when we start - bundleNow can be done in build time or can be done now
154+ // that is why we make attempt to process it now and then watch for changes
155+ const w = chokidar . watch ( messagesFile , { persistent : true } ) ;
156+ w . on ( 'change' , ( ) => {
157+ this . processExtractedMessages ( adminforth , messagesFile ) ;
158+ } ) ;
159+ w . on ( 'add' , ( ) => {
160+ this . processExtractedMessages ( adminforth , messagesFile ) ;
161+ } ) ;
162+
163+ }
51164
52165 validateConfigAfterDiscover ( adminforth : IAdminForth , resourceConfig : AdminForthResource ) {
53166 // optional method where you can safely check field types after database discovery was performed
167+ // ensure each trFieldName (apart from enFieldName) is nullable column of type string
168+ for ( const lang of this . options . supportedLanguages ) {
169+ if ( lang === 'en' ) {
170+ continue ;
171+ }
172+ const column = resourceConfig . columns . find ( c => c . name === this . trFieldNames [ lang ] ) ;
173+ if ( ! column ) {
174+ throw new Error ( `Field ${ this . trFieldNames [ lang ] } not found for storing translation for language ${ lang }
175+ in resource ${ resourceConfig . resourceId } , consider adding it to columns or change trFieldNames option to remap it to existing column` ) ;
176+ }
177+ if ( column . required . create || column . required . edit ) {
178+ throw new Error ( `Field ${ this . trFieldNames [ lang ] } should be not required in resource ${ resourceConfig . resourceId } ` ) ;
179+ }
180+ }
181+
182+ // in this plugin we will use plugin to fill the database with missing language messages
183+ this . tryProcessAndWatch ( adminforth ) ;
184+
54185 }
55186
56187 instanceUniqueRepresentation ( pluginOptions : any ) : string {
0 commit comments