15
15
*/
16
16
17
17
import * as React from 'react' ;
18
- import { TYPE } from '@formatjs/icu-messageformat-parser' ;
18
+ import { ReactElement } from 'react' ;
19
+ import { MessageFormatElement , TYPE } from '@formatjs/icu-messageformat-parser' ;
19
20
import { IntlMessageFormat } from 'intl-messageformat' ;
20
21
22
+ declare const TranslatedTextSymbol : unique symbol ;
23
+
24
+ export type TranslatedText = {
25
+ [ TranslatedTextSymbol ] ?: true ;
26
+ expr : string ;
27
+ text : string ;
28
+ } ;
29
+
21
30
const GLOBALS = {
22
31
isCloud : true ,
23
32
instanceQualifier : 'Tolgee' ,
24
33
instanceUrl : 'https://app.tolgee.io' ,
25
34
} ;
26
35
36
+ const SeenIcuXmlIds = new Set < string > ( ) ;
37
+
27
38
function formatDev ( string ?: string , demoParams ?: Record < string , any > ) {
28
39
const formatted = new IntlMessageFormat ( string , 'en-US' ) . format ( {
29
40
...GLOBALS ,
@@ -39,37 +50,128 @@ function formatDev(string?: string, demoParams?: Record<string, any>) {
39
50
return formatted ;
40
51
}
41
52
42
- export default function t (
43
- key : string ,
44
- defaultString ?: string ,
53
+ function processMessageElements (
54
+ id : string ,
55
+ elements : MessageFormatElement [ ] ,
45
56
demoParams ?: Record < string , any >
46
- ) {
47
- const intl = new IntlMessageFormat ( defaultString , 'en-US' ) ;
48
- const ast = intl . getAst ( ) ;
57
+ ) : [ ReactElement [ ] , Set < string > ] {
58
+ const fragments : ReactElement [ ] = [ ] ;
59
+ const stringArguments = new Set < string > ( ) ;
49
60
50
- const stringArguments : string [ ] = [ ] ;
51
-
52
- for ( const node of ast ) {
61
+ for ( const node of elements ) {
53
62
if ( node . type === TYPE . literal || node . type === TYPE . pound ) {
63
+ // Text and what misc ICU syntax; not interesting
54
64
continue ;
55
65
}
56
66
57
67
if ( node . type === TYPE . tag ) {
58
- // TODO: find a way to process the tag
68
+ // Tag: needs to be converted to a Thymeleaf fragment
69
+ const templateId = `${ id } -${ node . value } ` ;
70
+ const [ tagFrags , tagArgs ] = processMessageElements (
71
+ templateId ,
72
+ node . children
73
+ ) ;
74
+
75
+ tagArgs . forEach ( ( a ) => stringArguments . add ( a ) ) ;
76
+ if ( ! SeenIcuXmlIds . has ( templateId ) ) {
77
+ SeenIcuXmlIds . add ( templateId ) ;
78
+ fragments . push (
79
+ ...tagFrags ,
80
+ React . createElement (
81
+ 'th:block' ,
82
+ {
83
+ key : templateId ,
84
+ 'th:fragment' : `${ templateId } (_children)` ,
85
+ } ,
86
+ demoParams [ node . value ] (
87
+ React . createElement ( 'div' , { 'th:replace' : '${_children}' } )
88
+ )
89
+ )
90
+ ) ;
91
+ }
92
+
59
93
continue ;
60
94
}
61
95
62
- stringArguments . push ( `${ node . value } : ${ node . value . replace ( / _ _ / g, '.' ) } ` ) ;
96
+ // Everything else is some form of variable: keep track of them
97
+ stringArguments . add ( node . value ) ;
63
98
}
64
99
100
+ return [ fragments , stringArguments ] ;
101
+ }
102
+
103
+ function renderTranslatedText (
104
+ key : string ,
105
+ defaultString : string ,
106
+ demoParams ?: Record < string , any >
107
+ ) {
108
+ const id = `intl-${ key . replace ( / \. / g, '__' ) } ` ;
109
+ const intl = new IntlMessageFormat ( defaultString , 'en-US' ) ;
110
+ const ast = intl . getAst ( ) ;
111
+
112
+ const [ fragments , stringArguments ] = processMessageElements (
113
+ id ,
114
+ ast ,
115
+ demoParams
116
+ ) ;
117
+
65
118
const text =
66
119
process . env . NODE_ENV === 'production'
67
120
? defaultString
68
121
: formatDev ( defaultString , demoParams ) ;
69
122
70
- const messageExpression = stringArguments . length
71
- ? `#{${ key } (\${ { ${ stringArguments . join ( ', ' ) } } })}`
123
+ const stringArgs = Array . from ( stringArguments ) ;
124
+ const stringArgsMap = stringArgs . map ( ( a ) => `${ a } : ${ a . replace ( / _ _ / g, '.' ) } ` ) ;
125
+
126
+ const messageExpression = stringArgsMap . length
127
+ ? `#{${ key } (\${ { ${ stringArgsMap . join ( ', ' ) } } })}`
72
128
: `#{${ key } }` ;
73
129
74
- return React . createElement ( 'span' , { 'th:utext' : messageExpression } , text ) ;
130
+ return { fragments, text, messageExpression } ;
131
+ }
132
+
133
+ export default function t (
134
+ key : string ,
135
+ defaultString : string ,
136
+ demoParams ?: Record < string , unknown >
137
+ ) {
138
+ const { fragments, text, messageExpression } = renderTranslatedText (
139
+ key ,
140
+ defaultString ,
141
+ demoParams
142
+ ) ;
143
+
144
+ return [
145
+ ...fragments ,
146
+ React . createElement (
147
+ 'span' ,
148
+ { key : 'render-el' , 'th:utext' : messageExpression } ,
149
+ text
150
+ ) ,
151
+ ] ;
75
152
}
153
+
154
+ t . raw = function (
155
+ key : string ,
156
+ defaultString : string ,
157
+ demoParams ?: Record < string , any >
158
+ ) : TranslatedText {
159
+ const { fragments, text, messageExpression } = renderTranslatedText (
160
+ key ,
161
+ defaultString ,
162
+ demoParams
163
+ ) ;
164
+
165
+ if ( fragments . length )
166
+ throw new Error ( 'Invalid raw translation: cannot contain components.' ) ;
167
+
168
+ return {
169
+ expr : messageExpression ,
170
+ text,
171
+ } ;
172
+ } ;
173
+
174
+ t . render = function ( text : TranslatedText | string ) {
175
+ if ( typeof text === 'string' ) return text ;
176
+ return React . createElement ( 'span' , { 'th:utext' : text . expr } , text . text ) ;
177
+ } ;
0 commit comments