1
1
/*
2
2
Copyright 2017 MTRNord and Cooperative EITA
3
3
Copyright 2017 Vector Creations Ltd.
4
- Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
5
4
Copyright 2019 Michael Telatynski <[email protected] >
5
+ Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
6
6
7
7
Licensed under the Apache License, Version 2.0 (the "License");
8
8
you may not use this file except in compliance with the License.
@@ -21,11 +21,13 @@ import request from 'browser-request';
21
21
import counterpart from 'counterpart' ;
22
22
import React from 'react' ;
23
23
import { logger } from "matrix-js-sdk/src/logger" ;
24
+ import { Optional } from "matrix-events-sdk" ;
24
25
25
26
import SettingsStore from "./settings/SettingsStore" ;
26
27
import PlatformPeg from "./PlatformPeg" ;
27
28
import { SettingLevel } from "./settings/SettingLevel" ;
28
29
import { retry } from "./utils/promise" ;
30
+ import SdkConfig from "./SdkConfig" ;
29
31
30
32
// @ts -ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
31
33
import webpackLangJsonUrl from "$webapp/i18n/languages.json" ;
@@ -394,10 +396,11 @@ export function setLanguage(preferredLangs: string | string[]) {
394
396
}
395
397
396
398
return getLanguageRetry ( i18nFolder + availLangs [ langToUse ] . fileName ) ;
397
- } ) . then ( ( langData ) => {
399
+ } ) . then ( async ( langData ) => {
398
400
counterpart . registerTranslations ( langToUse , langData ) ;
401
+ await registerCustomTranslations ( ) ;
399
402
counterpart . setLocale ( langToUse ) ;
400
- SettingsStore . setValue ( "language" , null , SettingLevel . DEVICE , langToUse ) ;
403
+ await SettingsStore . setValue ( "language" , null , SettingLevel . DEVICE , langToUse ) ;
401
404
// Adds a lot of noise to test runs, so disable logging there.
402
405
if ( process . env . NODE_ENV !== "test" ) {
403
406
logger . log ( "set language to " + langToUse ) ;
@@ -407,8 +410,9 @@ export function setLanguage(preferredLangs: string | string[]) {
407
410
if ( langToUse !== "en" ) {
408
411
return getLanguageRetry ( i18nFolder + availLangs [ 'en' ] . fileName ) ;
409
412
}
410
- } ) . then ( ( langData ) => {
413
+ } ) . then ( async ( langData ) => {
411
414
if ( langData ) counterpart . registerTranslations ( 'en' , langData ) ;
415
+ await registerCustomTranslations ( ) ;
412
416
} ) ;
413
417
}
414
418
@@ -581,3 +585,83 @@ function getLanguage(langPath: string): Promise<object> {
581
585
) ;
582
586
} ) ;
583
587
}
588
+
589
+ export interface ICustomTranslations {
590
+ // Format is a map of english string to language to override
591
+ [ str : string ] : {
592
+ [ lang : string ] : string ;
593
+ } ;
594
+ }
595
+
596
+ let cachedCustomTranslations : Optional < ICustomTranslations > = null ;
597
+ let cachedCustomTranslationsExpire = 0 ; // zero to trigger expiration right away
598
+
599
+ // This awkward class exists so the test runner can get at the function. It is
600
+ // not intended for practical or realistic usage.
601
+ export class CustomTranslationOptions {
602
+ public static lookupFn : ( url : string ) => ICustomTranslations ;
603
+
604
+ private constructor ( ) {
605
+ // static access for tests only
606
+ }
607
+ }
608
+
609
+ /**
610
+ * If a custom translations file is configured, it will be parsed and registered.
611
+ * If no customization is made, or the file can't be parsed, no action will be
612
+ * taken.
613
+ *
614
+ * This function should be called *after* registering other translations data to
615
+ * ensure it overrides strings properly.
616
+ */
617
+ export async function registerCustomTranslations ( ) {
618
+ const lookupUrl = SdkConfig . get ( ) . custom_translations_url ;
619
+ if ( ! lookupUrl ) return ; // easy - nothing to do
620
+
621
+ try {
622
+ let json : ICustomTranslations ;
623
+ if ( Date . now ( ) >= cachedCustomTranslationsExpire ) {
624
+ json = CustomTranslationOptions . lookupFn
625
+ ? CustomTranslationOptions . lookupFn ( lookupUrl )
626
+ : ( await ( await fetch ( lookupUrl ) ) . json ( ) as ICustomTranslations ) ;
627
+ cachedCustomTranslations = json ;
628
+
629
+ // Set expiration to the future, but not too far. Just trying to avoid
630
+ // repeated, successive, calls to the server rather than anything long-term.
631
+ cachedCustomTranslationsExpire = Date . now ( ) + ( 5 * 60 * 1000 ) ;
632
+ } else {
633
+ json = cachedCustomTranslations ;
634
+ }
635
+
636
+ // If the (potentially cached) json is invalid, don't use it.
637
+ if ( ! json ) return ;
638
+
639
+ // We convert the operator-friendly version into something counterpart can
640
+ // consume.
641
+ const langs : {
642
+ // same structure, just flipped key order
643
+ [ lang : string ] : {
644
+ [ str : string ] : string ;
645
+ } ;
646
+ } = { } ;
647
+ for ( const [ str , translations ] of Object . entries ( json ) ) {
648
+ for ( const [ lang , newStr ] of Object . entries ( translations ) ) {
649
+ if ( ! langs [ lang ] ) langs [ lang ] = { } ;
650
+ langs [ lang ] [ str ] = newStr ;
651
+ }
652
+ }
653
+
654
+ // Finally, tell counterpart about our translations
655
+ for ( const [ lang , translations ] of Object . entries ( langs ) ) {
656
+ counterpart . registerTranslations ( lang , translations ) ;
657
+ }
658
+ } catch ( e ) {
659
+ // We consume all exceptions because it's considered non-fatal for custom
660
+ // translations to break. Most failures will be during initial development
661
+ // of the json file and not (hopefully) at runtime.
662
+ logger . warn ( "Ignoring error while registering custom translations: " , e ) ;
663
+
664
+ // Like above: trigger a cache of the json to avoid successive calls.
665
+ cachedCustomTranslationsExpire = Date . now ( ) + ( 5 * 60 * 1000 ) ;
666
+ }
667
+ }
0 commit comments