@@ -10,12 +10,34 @@ for different account related information
10
10
and configuration.
11
11
*/
12
12
13
+ import { DownOutlined } from "@ant-design/icons" ;
14
+ import { Button , Dropdown , MenuProps , Modal , Space , Tooltip } from "antd" ;
15
+ import { useIntl } from "react-intl" ;
16
+
13
17
import { SignOut } from "@cocalc/frontend/account/sign-out" ;
14
18
import { AntdTabItem , Col , Row , Tabs } from "@cocalc/frontend/antd-bootstrap" ;
15
- import { React , redux , useTypedRedux } from "@cocalc/frontend/app-framework" ;
16
- import { Icon , Loading } from "@cocalc/frontend/components" ;
19
+ import {
20
+ React ,
21
+ redux ,
22
+ useTypedRedux ,
23
+ useWindowDimensions ,
24
+ } from "@cocalc/frontend/app-framework" ;
25
+ import { useLocalizationCtx } from "@cocalc/frontend/app/localize" ;
26
+ import { Icon , Loading , Paragraph } from "@cocalc/frontend/components" ;
27
+ import { cloudFilesystemsEnabled } from "@cocalc/frontend/compute" ;
28
+ import CloudFilesystems from "@cocalc/frontend/compute/cloud-filesystem/cloud-filesystems" ;
29
+ import {
30
+ getLocale ,
31
+ labels ,
32
+ Locale ,
33
+ LOCALIZATIONS ,
34
+ OTHER_SETTINGS_LOCALE_KEY ,
35
+ } from "@cocalc/frontend/i18n" ;
17
36
import { LandingPage } from "@cocalc/frontend/landing-page/landing-page" ;
18
37
import { local_storage_length } from "@cocalc/frontend/misc/local-storage" ;
38
+ import PurchasesPage from "@cocalc/frontend/purchases/purchases-page" ;
39
+ import StatementsPage from "@cocalc/frontend/purchases/statements-page" ;
40
+ import SubscriptionsPage from "@cocalc/frontend/purchases/subscriptions-page" ;
19
41
import { SupportTickets } from "@cocalc/frontend/support" ;
20
42
import {
21
43
KUCALC_COCALC_COM ,
@@ -26,13 +48,15 @@ import { LicensesPage } from "./licenses/licenses-page";
26
48
import { PublicPaths } from "./public-paths/public-paths" ;
27
49
import { SSHKeysPage } from "./ssh-keys/global-ssh-keys" ;
28
50
import { UpgradesPage } from "./upgrades/upgrades-page" ;
29
- import PurchasesPage from "@cocalc/frontend/purchases/purchases-page" ;
30
- import SubscriptionsPage from "@cocalc/frontend/purchases/subscriptions-page" ;
31
- import StatementsPage from "@cocalc/frontend/purchases/statements-page" ;
32
- import { cloudFilesystemsEnabled } from "@cocalc/frontend/compute" ;
33
- import CloudFilesystems from "@cocalc/frontend/compute/cloud-filesystem/cloud-filesystems" ;
34
51
35
52
export const AccountPage : React . FC = ( ) => {
53
+ const intl = useIntl ( ) ;
54
+ const { setLocale, locale } = useLocalizationCtx ( ) ;
55
+
56
+ const { width : windowWidth } = useWindowDimensions ( ) ;
57
+ const isWide = windowWidth > 800 ;
58
+
59
+ const other_settings = useTypedRedux ( "account" , "other_settings" ) ;
36
60
const active_page = useTypedRedux ( "account" , "active_page" ) ;
37
61
const is_logged_in = useTypedRedux ( "account" , "is_logged_in" ) ;
38
62
const account_id = useTypedRedux ( "account" , "account_id" ) ;
@@ -61,6 +85,7 @@ export const AccountPage: React.FC = () => {
61
85
const ssh_gateway = useTypedRedux ( "customize" , "ssh_gateway" ) ;
62
86
const is_commercial = useTypedRedux ( "customize" , "is_commercial" ) ;
63
87
const get_api_key = useTypedRedux ( "page" , "get_api_key" ) ;
88
+ const i18n_enabled = useTypedRedux ( "customize" , "i18n" ) ;
64
89
65
90
// for each exclusive domain, tell the user which strategy to use
66
91
const exclusive_sso_domains = React . useMemo ( ( ) => {
@@ -116,7 +141,7 @@ export const AccountPage: React.FC = () => {
116
141
key : "account" ,
117
142
label : (
118
143
< span >
119
- < Icon name = "wrench" /> Preferences
144
+ < Icon name = "wrench" /> { intl . formatMessage ( labels . preferences ) }
120
145
</ span >
121
146
) ,
122
147
children : ( active_page == null || active_page === "account" ) && (
@@ -137,7 +162,7 @@ export const AccountPage: React.FC = () => {
137
162
key : "purchases" ,
138
163
label : (
139
164
< span >
140
- < Icon name = "money" /> Purchases
165
+ < Icon name = "money" /> { intl . formatMessage ( labels . purchases ) }
141
166
</ span >
142
167
) ,
143
168
children : active_page === "purchases" && < PurchasesPage /> ,
@@ -146,7 +171,7 @@ export const AccountPage: React.FC = () => {
146
171
key : "subscriptions" ,
147
172
label : (
148
173
< span >
149
- < Icon name = "calendar" /> Subscriptions
174
+ < Icon name = "calendar" /> { intl . formatMessage ( labels . subscriptions ) }
150
175
</ span >
151
176
) ,
152
177
children : active_page === "subscriptions" && < SubscriptionsPage /> ,
@@ -155,7 +180,7 @@ export const AccountPage: React.FC = () => {
155
180
key : "statements" ,
156
181
label : (
157
182
< span >
158
- < Icon name = "money" /> Statements
183
+ < Icon name = "money" /> { intl . formatMessage ( labels . statements ) }
159
184
</ span >
160
185
) ,
161
186
children : active_page === "statements" && < StatementsPage /> ,
@@ -171,7 +196,7 @@ export const AccountPage: React.FC = () => {
171
196
key : "licenses" ,
172
197
label : (
173
198
< span >
174
- < Icon name = "key" /> Licenses
199
+ < Icon name = "key" /> { intl . formatMessage ( labels . licenses ) }
175
200
</ span >
176
201
) ,
177
202
children : active_page === "licenses" && < LicensesPage /> ,
@@ -183,7 +208,7 @@ export const AccountPage: React.FC = () => {
183
208
key : "ssh-keys" ,
184
209
label : (
185
210
< span >
186
- < Icon name = "key" /> SSH Keys
211
+ < Icon name = "key" /> { intl . formatMessage ( labels . ssh_keys ) }
187
212
</ span >
188
213
) ,
189
214
children : active_page === "ssh-keys" && < SSHKeysPage /> ,
@@ -194,7 +219,7 @@ export const AccountPage: React.FC = () => {
194
219
key : "support" ,
195
220
label : (
196
221
< span >
197
- < Icon name = "medkit" /> Support
222
+ < Icon name = "medkit" /> { intl . formatMessage ( labels . support ) }
198
223
</ span >
199
224
) ,
200
225
children : active_page === "support" && < SupportTickets /> ,
@@ -204,7 +229,8 @@ export const AccountPage: React.FC = () => {
204
229
key : "public-files" ,
205
230
label : (
206
231
< span >
207
- < Icon name = "share-square" /> Public Files
232
+ < Icon name = "share-square" /> { " " }
233
+ { intl . formatMessage ( labels . published_files ) }
208
234
</ span >
209
235
) ,
210
236
children : active_page === "public-files" && < PublicPaths /> ,
@@ -214,7 +240,8 @@ export const AccountPage: React.FC = () => {
214
240
key : "upgrades" ,
215
241
label : (
216
242
< span >
217
- < Icon name = "arrow-circle-up" /> Upgrades
243
+ < Icon name = "arrow-circle-up" /> { " " }
244
+ { intl . formatMessage ( labels . upgrades ) }
218
245
</ span >
219
246
) ,
220
247
children : active_page === "upgrades" && < UpgradesPage /> ,
@@ -225,7 +252,8 @@ export const AccountPage: React.FC = () => {
225
252
key : "cloud-filesystems" ,
226
253
label : (
227
254
< >
228
- < Icon name = "disk-round" /> Cloud File Systems
255
+ < Icon name = "disk-round" /> { " " }
256
+ { intl . formatMessage ( labels . cloud_file_system ) }
229
257
</ >
230
258
) ,
231
259
children : < CloudFilesystems /> ,
@@ -235,6 +263,122 @@ export const AccountPage: React.FC = () => {
235
263
return items ;
236
264
}
237
265
266
+ function renderI18N ( ) : JSX . Element | null {
267
+ if (
268
+ i18n_enabled == null ||
269
+ i18n_enabled . isEmpty ( ) ||
270
+ ( i18n_enabled . size === 1 && i18n_enabled . includes ( "en" ) )
271
+ ) {
272
+ return null ;
273
+ }
274
+
275
+ const i18n : Locale = getLocale ( other_settings ) ;
276
+
277
+ const items : MenuProps [ "items" ] =
278
+ Object . entries ( LOCALIZATIONS )
279
+ . filter ( ( [ key , _ ] ) => i18n_enabled . includes ( key as any ) )
280
+ . map ( ( [ key , { name, trans, native, flag } ] ) => {
281
+ const other = key === locale ? name : intl . formatMessage ( trans ) ;
282
+ return { key, label : `${ flag } ${ native } (${ other } )` } ;
283
+ } ) ?? [ ] ;
284
+
285
+ items . push ( { type : "divider" } ) ;
286
+ items . push ( {
287
+ key : "help" ,
288
+ label : (
289
+ < Space >
290
+ < Icon name = "translation-outlined" />
291
+ { intl . formatMessage ( {
292
+ id : "account.account_page.translation.info.label" ,
293
+ defaultMessage : "Translation Info..." ,
294
+ description : "Label of translation information modal in dropdown" ,
295
+ } ) }
296
+ </ Space >
297
+ ) ,
298
+ onClick : ( ) =>
299
+ Modal . info ( {
300
+ width : "min(90vw, 600px)" ,
301
+ title : intl . formatMessage ( {
302
+ id : "account.account_page.translation.info.title" ,
303
+ defaultMessage : "Translation Information" ,
304
+ description : "Title of translation information modal" ,
305
+ } ) ,
306
+ content : (
307
+ < Paragraph >
308
+ { intl . formatMessage ( {
309
+ id : "account.account_page.translation.info.content" ,
310
+ defaultMessage : `
311
+ We're excited to start offering our application in multiple languages! Here's what you need to know:
312
+
313
+ <ul>
314
+ <li><b>Work in Progress</b>: Our translation effort is just beginning. Many parts of the application are not yet translated.</li>
315
+ <li><b>Gradual Improvement</b>: We're continuously working to expand our language coverage. You'll see more content translated over time.</li>
316
+ <li><b>Your Help is Welcome</b>: We value our community's input. If you're fluent in multiple languages and would like to contribute to our translation efforts, we'd love to hear from you!</li>
317
+ <li><b>Contact Us</b>: To learn more about contributing to translations or to report any issues, please reach out to our support team.</li>
318
+ </ul>
319
+
320
+ Thank you for your patience and understanding as we work to make our application accessible to a global audience!` ,
321
+ description : "Content of translation information modal" ,
322
+ } ) }
323
+ </ Paragraph >
324
+ ) ,
325
+ } ) ,
326
+ } ) ;
327
+
328
+ const menu : MenuProps = {
329
+ items,
330
+ onClick : ( { key } ) => {
331
+ if ( key in LOCALIZATIONS ) {
332
+ redux
333
+ . getActions ( "account" )
334
+ . set_other_settings ( OTHER_SETTINGS_LOCALE_KEY , key ) ;
335
+ setLocale ( key ) ;
336
+ }
337
+ } ,
338
+ } ;
339
+
340
+ const lang_icon = LOCALIZATIONS [ i18n ] ?. flag ;
341
+
342
+ const title =
343
+ i18n in LOCALIZATIONS
344
+ ? intl . formatMessage ( LOCALIZATIONS [ i18n ] . trans )
345
+ : i18n ;
346
+
347
+ const cur = `${ title } (${ LOCALIZATIONS [ i18n ] ?. name ?? i18n } )` ;
348
+ const msg = intl . formatMessage ( labels . account_language_tooltip ) ;
349
+ const tooltip = (
350
+ < >
351
+ { cur }
352
+ < br />
353
+ { msg }
354
+ < br /> ({ labels . account_language_tooltip . defaultMessage } )
355
+ </ >
356
+ ) ;
357
+
358
+ return (
359
+ < Tooltip title = { tooltip } trigger = { [ "hover" ] } >
360
+ < Dropdown menu = { menu } trigger = { [ "click" ] } >
361
+ < Button >
362
+ < Space >
363
+ { lang_icon }
364
+ { isWide ? title : undefined }
365
+ < DownOutlined />
366
+ </ Space >
367
+ </ Button >
368
+ </ Dropdown >
369
+ </ Tooltip >
370
+ ) ;
371
+ }
372
+
373
+ function renderExtraContent ( ) {
374
+ return (
375
+ < Space >
376
+ { renderI18N ( ) }
377
+ < SignOut everywhere = { false } highlight = { true } narrow = { ! isWide } />
378
+ </ Space >
379
+ ) ;
380
+ }
381
+
238
382
function render_logged_in_view ( ) : JSX . Element {
239
383
if ( ! account_id ) {
240
384
return (
@@ -263,7 +407,7 @@ export const AccountPage: React.FC = () => {
263
407
activeKey = { active_page ?? "account" }
264
408
onSelect = { handle_select }
265
409
animation = { false }
266
- tabBarExtraContent = { < SignOut everywhere = { false } highlight = { true } /> }
410
+ tabBarExtraContent = { renderExtraContent ( ) }
267
411
items = { tabs }
268
412
/>
269
413
</ Col >
0 commit comments