diff --git a/webapp/src/ts/app.component.ts b/webapp/src/ts/app.component.ts
index a0079893d52..f7d23bdfd63 100644
--- a/webapp/src/ts/app.component.ts
+++ b/webapp/src/ts/app.component.ts
@@ -50,6 +50,7 @@ import { BrowserCompatibilityComponent } from '@mm-modals/browser-compatibility/
import { PerformanceService } from '@mm-services/performance.service';
import { UserSettings, UserSettingsService } from '@mm-services/user-settings.service';
import { OLD_NAV_PERMISSION } from '@mm-components/header/header.component';
+import { Directionality } from '@angular/cdk/bidi';
const SYNC_STATUS = {
inProgress: {
@@ -95,6 +96,7 @@ export class AppComponent implements OnInit, AfterViewInit {
androidAppVersion;
hasOldNav = false;
initialisationComplete = false;
+ direction;
private readonly SVG_ICONS = new Map([
['icon-close', './img/icon-close.svg'],
['icon-filter', './img/icon-filter.svg'],
@@ -463,18 +465,21 @@ export class AppComponent implements OnInit, AfterViewInit {
this.store.select(Selectors.getCurrentTab),
this.store.select(Selectors.getSelectMode),
this.store.select(Selectors.getSearchBar),
+ this.store.select(Selectors.getDirection),
]).subscribe(([
replicationStatus,
androidAppVersion,
currentTab,
selectMode,
searchBar,
+ direction,
]) => {
this.replicationStatus = replicationStatus;
this.androidAppVersion = androidAppVersion;
this.currentTab = currentTab || '';
this.selectMode = selectMode;
this.openSearch = !!searchBar?.isOpen;
+ this.direction = direction;
});
combineLatest([
diff --git a/webapp/src/ts/app.module.ts b/webapp/src/ts/app.module.ts
index a49334e2c34..b8dc59d3b0d 100644
--- a/webapp/src/ts/app.module.ts
+++ b/webapp/src/ts/app.module.ts
@@ -41,6 +41,7 @@ import { GlobalEffects } from '@mm-effects/global.effects';
import { ReportsEffects } from '@mm-effects/reports.effects';
import { ContactsEffects } from '@mm-effects/contacts.effects';
import { reducers } from '@mm-reducers/index';
+import { LanguageService } from '@mm-services/language.service';
const logger = reducer => {
// default, no options
@@ -74,8 +75,9 @@ export class MissingTranslationHandlerLog implements MissingTranslationHandler {
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
- useFactory: (db: DbService) => new TranslationLoaderProvider(db),
- deps: [DbService],
+ useFactory:
+ (db: DbService, languageService: LanguageService) => new TranslationLoaderProvider(db, languageService),
+ deps: [DbService, LanguageService],
},
missingTranslationHandler: {
provide: MissingTranslationHandler,
diff --git a/webapp/src/ts/components/fast-action-button/fast-action-button.component.ts b/webapp/src/ts/components/fast-action-button/fast-action-button.component.ts
index 05da87c9f15..538a225488b 100644
--- a/webapp/src/ts/components/fast-action-button/fast-action-button.component.ts
+++ b/webapp/src/ts/components/fast-action-button/fast-action-button.component.ts
@@ -28,6 +28,7 @@ export class FastActionButtonComponent implements OnInit, OnDestroy {
iconTypeResource = IconType.RESOURCE;
iconTypeFontAwesome = IconType.FONT_AWESOME;
buttonTypeFlat = ButtonType.FLAT;
+ direction;
constructor(
private store: Store,
@@ -35,7 +36,11 @@ export class FastActionButtonComponent implements OnInit, OnDestroy {
private responsiveService: ResponsiveService,
private matBottomSheet: MatBottomSheet,
private matDialog: MatDialog,
- ) { }
+ ) {
+ this.store.select(Selectors.getDirection).subscribe(direction => {
+ this.direction = direction;
+ });
+ }
ngOnInit() {
this.subscribeToStore();
@@ -87,6 +92,7 @@ export class FastActionButtonComponent implements OnInit, OnDestroy {
autoFocus: false,
minWidth: 300,
minHeight: 150,
+ direction: this.direction,
});
}
diff --git a/webapp/src/ts/modules/reports/reports.component.ts b/webapp/src/ts/modules/reports/reports.component.ts
index da6786dce96..50990c54930 100644
--- a/webapp/src/ts/modules/reports/reports.component.ts
+++ b/webapp/src/ts/modules/reports/reports.component.ts
@@ -25,6 +25,7 @@ import { XmlFormsService } from '@mm-services/xml-forms.service';
import { PerformanceService } from '@mm-services/performance.service';
import { ExtractLineageService } from '@mm-services/extract-lineage.service';
import { ButtonType } from '@mm-components/fast-action-button/fast-action-button.component';
+import { Directionality } from '@angular/cdk/bidi';
const PAGE_SIZE = 50;
const CAN_DEFAULT_FACILITY_FILTER = 'can_default_facility_filter';
@@ -65,6 +66,7 @@ export class ReportsComponent implements OnInit, AfterViewInit, OnDestroy {
userParentPlace;
fastActionList?: FastAction[];
userLineageLevel;
+ isRtl;
LIMIT_SELECT_ALL_REPORTS = 500;
@@ -88,9 +90,11 @@ export class ReportsComponent implements OnInit, AfterViewInit, OnDestroy {
private xmlFormsService:XmlFormsService,
private performanceService: PerformanceService,
private extractLineageService: ExtractLineageService,
+ dir: Directionality,
) {
this.globalActions = new GlobalActions(store);
this.reportsActions = new ReportsActions(store);
+ this.isRtl = dir.value === 'rtl';
}
ngOnInit() {
diff --git a/webapp/src/ts/providers/translation-loader.provider.ts b/webapp/src/ts/providers/translation-loader.provider.ts
index dc21cb6aaf6..1d8846dc067 100644
--- a/webapp/src/ts/providers/translation-loader.provider.ts
+++ b/webapp/src/ts/providers/translation-loader.provider.ts
@@ -5,10 +5,14 @@ import { from } from 'rxjs';
import { DbService } from '@mm-services/db.service';
import * as translationUtils from '@medic/translation-utils';
import { TranslationDocsMatcherProvider } from '@mm-providers/translation-docs-matcher.provider';
+import { LanguageService } from '@mm-services/language.service';
@Injectable()
export class TranslationLoaderProvider implements TranslateLoader {
- constructor(private db:DbService) {}
+ constructor(
+ private db: DbService,
+ private languageService: LanguageService
+ ) {}
private loadingPromises = {};
@@ -38,11 +42,12 @@ export class TranslationLoaderProvider implements TranslateLoader {
const promise = this.db
.get()
.get(translationsDocId)
- .then(doc => {
+ .then((doc:{ generic:{}; custom:{}; rtl: boolean }) => {
const values = Object.assign(doc.generic || {}, doc.custom || {});
if (testing) {
mapTesting(values);
}
+ doc.rtl && this.languageService.setRtlLanguage(locale);
return translationUtils.loadTranslations(values);
})
.catch(err => {
diff --git a/webapp/src/ts/reducers/global.ts b/webapp/src/ts/reducers/global.ts
index c94c7124b9c..e17f17c447d 100644
--- a/webapp/src/ts/reducers/global.ts
+++ b/webapp/src/ts/reducers/global.ts
@@ -42,6 +42,7 @@ const initialState: GlobalState = {
translationsLoaded: false,
userFacilityIds: [],
userContactId: null,
+ language: null,
};
const setShowContent = (state, showContent) => {
@@ -177,6 +178,9 @@ const _globalReducer = createReducer(
on(Actions.setTrainingCard, (state, { payload: { trainingCard } }) => {
return { ...state, trainingCard: { ...state.trainingCard, ...trainingCard } };
}),
+ on(Actions.setLanguage, (state, { payload: { language } }) => {
+ return { ...state, language };
+ }),
);
export const globalReducer = (state, action) => {
@@ -211,6 +215,7 @@ export interface GlobalState {
translationsLoaded: boolean;
userFacilityIds: null | string[];
userContactId: null | string;
+ language: null | Record
;
}
interface SidebarMenuState {
diff --git a/webapp/src/ts/selectors/index.ts b/webapp/src/ts/selectors/index.ts
index 95c5fc52eb1..0e91cabe916 100644
--- a/webapp/src/ts/selectors/index.ts
+++ b/webapp/src/ts/selectors/index.ts
@@ -38,6 +38,8 @@ export const Selectors = {
getUserContactId: createSelector(getGlobalState, (globalState) => globalState.userContactId),
getTrainingCardFormId: createSelector(getGlobalState, (globalState) => globalState.trainingCard?.formId),
getTrainingCard: createSelector(getGlobalState, (globalState) => globalState.trainingCard),
+ getLanguage: createSelector(getGlobalState, (globalState) => globalState.language),
+ getDirection: createSelector(getGlobalState, (globalState) => globalState.language?.rtl ? 'rtl' : 'ltr'),
// enketo
getEnketoStatus: createSelector(getGlobalState, (globalState) => globalState.enketoStatus),
diff --git a/webapp/src/ts/services/language.service.ts b/webapp/src/ts/services/language.service.ts
index f465d8b9f88..69dbc5e852c 100644
--- a/webapp/src/ts/services/language.service.ts
+++ b/webapp/src/ts/services/language.service.ts
@@ -6,6 +6,9 @@ import { TranslateService as NgxTranslateService } from '@ngx-translate/core';
import { SettingsService } from '@mm-services/settings.service';
import { FormatDateService } from '@mm-services/format-date.service';
import { TelemetryService } from '@mm-services/telemetry.service';
+import { Store } from '@ngrx/store';
+import { ServicesActions } from '@mm-actions/services';
+import { GlobalActions } from '@mm-actions/global';
@Injectable({
providedIn: 'root'
@@ -46,12 +49,17 @@ export class LanguageCookieService {
providedIn: 'root'
})
export class SetLanguageService {
+ private globalActions;
+
constructor(
private ngxTranslateService:NgxTranslateService,
private telemetryService:TelemetryService,
private languageCookieService:LanguageCookieService,
private formatDateService:FormatDateService,
+ private languageService:LanguageService,
+ private store:Store,
) {
+ this.globalActions = new GlobalActions(this.store);
}
private setDatepickerLanguage(language) {
@@ -71,6 +79,7 @@ export class SetLanguageService {
// formatDateService depends on the cookie, so also wait for the cookie to be updated
await this.formatDateService.init();
+ this.globalActions.setLanguage({ code, rtl: this.languageService.useRtl(code) });
this.telemetryService.record(`user_settings:language:${code}`);
}
}
@@ -89,11 +98,17 @@ export class LanguageService {
private readonly DEFAULT_LOCALE = 'en';
private readonly NEPALI_LOCALE = 'ne';
+ private rtlLanguages:string[] = [];
+
private async fetchLocale() {
const settings = await this.settingsService.get();
return settings.locale || this.DEFAULT_LOCALE;
}
+ setRtlLanguage(code:string) {
+ this.rtlLanguages.push(code);
+ }
+
async get() {
const cookieVal = this.languageCookieService.get();
if (cookieVal) {
@@ -108,4 +123,8 @@ export class LanguageService {
const language = this.languageCookieService.get();
return language === this.NEPALI_LOCALE;
}
+
+ useRtl(code:string) {
+ return this.rtlLanguages.includes(code);
+ }
}
diff --git a/webapp/src/ts/services/modal.service.ts b/webapp/src/ts/services/modal.service.ts
index 0e124425bf1..4ea9b2648de 100644
--- a/webapp/src/ts/services/modal.service.ts
+++ b/webapp/src/ts/services/modal.service.ts
@@ -5,6 +5,7 @@ import { Store } from '@ngrx/store';
import { ResponsiveService } from '@mm-services/responsive.service';
import { GlobalActions } from '@mm-actions/global';
+import { Selectors } from '@mm-selectors/index';
@Injectable({
providedIn: 'root'
@@ -12,6 +13,7 @@ import { GlobalActions } from '@mm-actions/global';
export class ModalService {
private globalActions: GlobalActions;
+ private direction;
constructor(
private matDialog: MatDialog,
@@ -19,6 +21,9 @@ export class ModalService {
private responsiveService: ResponsiveService,
) {
this.globalActions = new GlobalActions(this.store);
+ this.store.select(Selectors.getDirection).subscribe(direction => {
+ this.direction = direction;
+ });
}
show(component: ComponentType, config?: Record): MatDialogRef {
@@ -42,6 +47,7 @@ export class ModalService {
width: '600px',
maxWidth: '90vw',
minHeight: '100px',
+ direction: this.direction,
...config,
});
}
diff --git a/webapp/src/ts/services/select2-search.service.ts b/webapp/src/ts/services/select2-search.service.ts
index 9723b428645..8000f0f60e4 100644
--- a/webapp/src/ts/services/select2-search.service.ts
+++ b/webapp/src/ts/services/select2-search.service.ts
@@ -11,11 +11,15 @@ import { SettingsService } from '@mm-services/settings.service';
import { ContactMutedService } from '@mm-services/contact-muted.service';
import { TranslateService } from '@mm-services/translate.service';
import { SearchTelemetryService } from '@mm-services/search-telemetry.service';
+import { Store } from '@ngrx/store';
+import { Selectors } from '@mm-selectors/index';
@Injectable({
providedIn: 'root'
})
export class Select2SearchService {
+ private direction;
+
constructor(
private readonly route: ActivatedRoute,
private readonly formatProvider: FormatProvider,
@@ -26,7 +30,12 @@ export class Select2SearchService {
private readonly settingsService: SettingsService,
private readonly contactMutedService: ContactMutedService,
private readonly searchTelemetryService: SearchTelemetryService,
- ) { }
+ private store: Store,
+ ) {
+ this.store.select(Selectors.getDirection).subscribe(direction => {
+ this.direction = direction;
+ });
+ }
private defaultTemplateResult(row) {
if (!row.doc) {
@@ -154,7 +163,8 @@ export class Select2SearchService {
templateResult: options.templateResult,
templateSelection: options.templateSelection,
width: '100%',
- minimumInputLength: this.sessionService.isOnlineOnly() ? 3 : 0
+ minimumInputLength: this.sessionService.isOnlineOnly() ? 3 : 0,
+ dir: this.direction,
});
if (options.allowNew) {