Skip to content

Commit

Permalink
GDB-10443 Implement i18n in the new workbench (#1485)
Browse files Browse the repository at this point in the history
* Move the services to a separate directory.

* Implements Service Provider

What
Implemented a service provider.

Why
The new service provider is responsible for managing shared service instances across microfrontends. Frameworks like Angular and React provide dependency injection, allowing a single instance to be injected into all components. In a single-spa application, such functionality does not exist by default. The service provider addresses this need.

How
The service provider includes lazy loading functionality that returns an instance of an already instantiated service. If the service is not present, a new one is created and stored in a static map. The static modifier ensures that only one instance of each service class is used across the microfrontends.

* GDB-10443: Implement i18n in the New Workbench

## What
- Implemented core functionality for emitting events across all microfrontends.
- Implemented global-context.service.ts.
- Implemented language.service.ts.
## Why
- Microfrontends are separate modules and should not be aware of each other. The API is a shared module across all of them. The event emitting functionality enables communication between these modules.
- The global context will maintain shared state between all microfrontends.
- The service will manage language functionality.
## How
- Utilized the browser's built-in custom event propagation. Events are emitted from the document body, ensuring that all microfrontends can listen to them.
- The global state is designed to be extendable with additional data as needed. Currently, it includes a language field that holds the user's selected language. This can be accessed through the onLanguageChanged function, which returns a Subject<string> and emits a value every time the language changes.
- The language service manages language-related functionality. All microfrontends can use it to fetch and update the language.

* GDB-10443: Implement i18n in the New Workbench

## What
- Implemented core functionality for emitting events across all microfrontends;
- Implemented global-context.service.ts;
- Implemented language.service.ts;
- Implemented translated label component in shared-components.

## Why
- Microfrontends are separate modules and should not be aware of each other. The API is a shared module across all of them. The event emitting functionality enables communication between these modules;
- The global context will maintain shared state between all microfrontends;
- The service will manage language functionality;
- To translate labels when the language is changed.

## How
- Utilized the browser's built-in custom event propagation. Events are emitted from the document body, ensuring that all microfrontends can listen to them;
- The global state is designed to be extendable with additional data as needed. Currently, it includes a language field that holds the user's selected language. This can be accessed through the onLanguageChanged function, which returns a Subject<string> and emits a value every time the language changes;
- The language service manages language-related functionality. All microfrontends can use it to fetch and update the language.

* GDB-10443: Implement i18n in the Legacy Workbench

What
Integrated the translation functionality of the legacy workbench with the new language change implementation.

Why
The language selection functionality has been moved to the shared-component microfrontend, centralizing language changes across the application.

How
Added a listener for the language change event. When the event is triggered, the new language value is set in the translation service, which triggers the re-translating the entire workbench.

* Removes the Workbench prefix.

* Remove usage of global context

* Add some missing documentation and changed the event emitter to use the document.body as a source for the emitted events instead of just `div`

---------

Co-authored-by: svilen.velikov <[email protected]>
  • Loading branch information
boyan-tonchev and svilenvelikov authored Jul 29, 2024
1 parent 1cdc543 commit 026f047
Show file tree
Hide file tree
Showing 36 changed files with 652 additions and 36 deletions.
17 changes: 17 additions & 0 deletions packages/api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,30 @@
"@babel/eslint-parser": "^7.23.3",
"@babel/plugin-transform-runtime": "^7.23.3",
"@babel/preset-env": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@babel/runtime": "^7.23.3",
"babel-jest": "^27.5.1",
"concurrently": "^6.2.1",
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-ts-important-stuff": "^1.1.0",
"eslint-plugin-prettier": "^3.4.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.5.1",
"jest-cli": "^27.5.1",
"prettier": "^2.3.2",
"pretty-quick": "^3.1.1",
"ts-config-single-spa": "^3.0.0",
"typescript": "^4.3.5",
"webpack": "^5.89.0",
"webpack-merge": "^5.8.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.0.0",
"@babel/preset-typescript": "^7.23.3",
"eslint-config-ts-important-stuff": "^1.1.0",
"typescript": "^4.3.5",
"webpack-config-single-spa-ts": "^4.0.0",
"ts-config-single-spa": "^3.0.0"
"webpack-dev-server": "^4.0.0",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"@reactivex/rxjs": "^6.6.7",
"@types/jest": "^27.0.1",
"@types/systemjs": "^6.1.1",
"@types/webpack-env": "^1.16.2",
Expand Down
5 changes: 0 additions & 5 deletions packages/api/src/authentication-service.ts

This file was deleted.

24 changes: 24 additions & 0 deletions packages/api/src/models/events/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* A generic shape of our internal event data payloads.
*/
export class Event {
/**
* The name of the event.
*/
readonly NAME;
/**
* The payload of the event.
*/
readonly payload;
/**
* Creates a new instance of the event.
*
* @param name - the name of the event.
* @param payload - the payload of the event. This is optional and if omitted, the event will have no payload,
* just a name.
*/
constructor(name: string, payload?: any) {
this.NAME = name;
this.payload = payload;
}
}
7 changes: 5 additions & 2 deletions packages/api/src/ontotext-workbench-api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Anything exported from this file is importable by other in-browser modules.
export {AuthenticationService} from './authentication-service';
export {RepositoryService} from './repository-service';
export {AuthenticationService} from './services/authentication.service';
export {RepositoryService} from './services/repository.service';
export {ServiceProvider} from './service.provider'
export {EventService} from './services/event.service';
export {LanguageService} from './services/language.service';
5 changes: 0 additions & 5 deletions packages/api/src/repository-service.ts

This file was deleted.

23 changes: 23 additions & 0 deletions packages/api/src/service.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Service} from './services/service';

/**
* Service provider for all {@link Service} instances.
* This provider caches all workbench services created on demand, ensuring that all micro frontends share a single instance of each service.
*/
export class ServiceProvider {

/**
* The static modifier ensures the map is the same for all ServiceProviders. Each micro-frontend will have its
* own instance of {@see ServiceFactoryService}, but the map with instances will be shared.
*
* @private
*/
private static readonly SERVICE_INSTANCES = new Map<string, Service>

public static get<T extends Service>(type: { new(service: Service): T; }): T {
if (!ServiceProvider.SERVICE_INSTANCES.has(type.name)) {
ServiceProvider.SERVICE_INSTANCES.set(type.name, new type(this));
}
return this.SERVICE_INSTANCES.get(type.name) as T;
}
}
7 changes: 7 additions & 0 deletions packages/api/src/services/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Service} from './service';

export class AuthenticationService implements Service {
login(): string {
return "Athentication.login from the API";
}
}
44 changes: 44 additions & 0 deletions packages/api/src/services/event.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {Event} from '../models/events/event';
import {Service} from './service';

/**
* Service used for global communication within all modules. It allows emitting and subscribing to CustomEvents across
* the application where the events are emitted via the <code>document.body</code> element. This allows for the events
* to be caught by any component in any module.
*/
export class EventService implements Service {
/**
* Emits a {@link CustomEvent} of type passed <code>event.NAME</code> and detail <code>event.payload</code>.
*
* @param event - the event to be emitted.
* @return the emitted event.
*/
emit(event: Event): CustomEvent {
const customEvent = new CustomEvent(event.NAME, {detail: event.payload});
this.getHostElement().dispatchEvent(customEvent);
return customEvent;
}

/**
* Subscribes for event of type <code>eventName</code>.
*
* @param eventName - type of subscription event.
* @param callback - callback function that will be called when the event occurred.
*
* @return unsubscribe function which can be used for manual unsubscription.
*/
subscribe(eventName: string, callback: (payload: any) => void): () => void {
const listener = (event) => {
if (event instanceof CustomEvent) {
callback(event.detail);
}
};
this.getHostElement().addEventListener(eventName, listener);

return () => this.getHostElement().removeEventListener(eventName, listener);
}

private getHostElement(): HTMLElement {
return document.body;
}
}
44 changes: 44 additions & 0 deletions packages/api/src/services/language.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {Service} from './service';
import {ReplaySubject, Subject} from '@reactivex/rxjs/dist/package';

/**
* The LanguageService class manages the application's language settings.
*/
export class LanguageService implements Service {

static readonly DEFAULT_LANGUAGE = 'en';
private readonly selectedLanguage = new ReplaySubject<string>(1);

/**
* Constructs a new LanguageService instance.
* Reads the initial language setting from local storage (not yet implemented)
* and sets it as the first value of the ReplaySubject.
*/
constructor() {
// TODO read it from local store and pass it as first value
}

/**
* Changes the current language of the application.
* This method updates the language setting and notifies all subscribers
* about the language change. The new language is also intended to be saved
* to local storage (not yet implemented).
*
* @param {string} locale - The new language code to set (e.g., 'en', 'fr', 'de').
*/
changeLanguage(locale: string): void {
// TODO save it to local store.
this.selectedLanguage.next(locale);
}

/**
* Returns an observable that emits the current language whenever it changes.
* Subscribers to this observable will receive updates whenever the language
* is changed using the `changeLanguage` method.
*
* @returns {Subject<string>} An observable stream of language changes.
*/
onLanguageChanged(): Subject<string> {
return this.selectedLanguage;
}
}
7 changes: 7 additions & 0 deletions packages/api/src/services/repository.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Service} from './service';

export class RepositoryService implements Service {
getRepositories(): Promise<Response> {
return fetch("http://localhost:9000/rest/repositories/all");
}
}
4 changes: 4 additions & 0 deletions packages/api/src/services/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Interface for identifying a service as part of the workbench system.
*/
export interface Service {}
3 changes: 1 addition & 2 deletions packages/api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"extends": "ts-config-single-spa",
"files": [
"src/authentication-service.ts", "src/ontotext-workbench-api.ts"],
"files": ["src/ontotext-workbench-api.ts"],
"compilerOptions": {
"declarationDir": "dist",
"outDir": "dist",
Expand Down
18 changes: 18 additions & 0 deletions packages/legacy-workbench/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'angular/core/directives/operations-statuses-monitor/operations-statuses-
import 'angular/core/directives/autocomplete/autocomplete.directive';
import 'angular/core/directives/prop-indeterminate/prop-indeterminate.directive';
import {defineCustomElements} from 'ontotext-yasgui-web-component/loader';
import {ServiceProvider, LanguageService} from "@ontotext/workbench-api";

// $translate.instant converts <b> from strings to &lt;b&gt
// and $sce.trustAsHtml could not recognise that this is valid html
Expand Down Expand Up @@ -157,6 +158,7 @@ const moduleDefinition = function (productInfo) {
// to construct version/edition-specific links.
$menuItemsProvider.setProductInfo(productInfo);

// TODO this remove this when clean code. The main menu is processed in shared-components
let mainMenu = PluginRegistry.get('main.menu');
mainMenu.forEach(function (menu) {
menu.items.forEach(function (item) {
Expand Down Expand Up @@ -218,6 +220,22 @@ const moduleDefinition = function (productInfo) {
ThemeService.applyDarkThemeMode();

GuidesService.init();

// =========================
// Functions and configurations for integration with the shared-components module.
// =========================
const languageService = ServiceProvider.get(LanguageService);

const languageChangeSubscriptions = languageService.onLanguageChanged()
.subscribe((language) => {
$translate.use(language);
});

$rootScope.$on('destroy', () => {
if (languageChangeSubscriptions) {
languageChangeSubscriptions.unsubscribe();
}
})
}]);

workbench.filter('titlecase', function() {
Expand Down
9 changes: 0 additions & 9 deletions packages/legacy-workbench/src/js/angular/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import './guides/directives';
import {GUIDE_PAUSE} from './guides/tour-lib-services/shepherd.service';
import 'angular-pageslide-directive/dist/angular-pageslide-directive';
import 'angularjs-slider/dist/rzslider.min';
// import {AuthenticationService, RepositoryService} from "@ontotext/workbench-api";

angular
.module('graphdb.workbench.se.controllers', [
Expand Down Expand Up @@ -61,14 +60,6 @@ homeCtrl.$inject = ['$scope', '$rootScope', '$http', '$repositories', '$jwtAuth'

function homeCtrl($scope, $rootScope, $http, $repositories, $jwtAuth, $licenseService, AutocompleteRestService, LicenseRestService, RepositoriesRestService, RDF4JRepositoriesRestService, toastr) {

// console.log(`LOGIN TS API in new WB`, AuthenticationService.login());
// RepositoryService.getRepositories().then((response) => {
// console.log(`response`, response);
// return response.json();
// }).then((data) => {
// console.log(`REPOSITORIES TS API in new WB`, data);
// });

$scope.doClear = false;

$scope.getActiveRepositorySize = function () {
Expand Down
1 change: 1 addition & 0 deletions packages/shared-components/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ $RECYCLE.BIN/
Thumbs.db
UserInterfaceState.xcuserstate
.env
api/
17 changes: 17 additions & 0 deletions packages/shared-components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/shared-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"puppeteer": "^21.9.0"
},
"dependencies": {
"@reactivex/rxjs": "^6.6.7",
"font-awesome": "^4.7.0",
"single-spa": "^6.0.1"
}
Expand Down
Loading

0 comments on commit 026f047

Please sign in to comment.