Skip to content

Commit

Permalink
init project
Browse files Browse the repository at this point in the history
  • Loading branch information
CGuether committed Apr 26, 2023
0 parents commit a93702b
Show file tree
Hide file tree
Showing 34 changed files with 19,882 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
dist/
.idea
coverage
.DS_Store

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Dependency directories
node_modules/
jspm_packages/
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Translation UI Plugin

## General

Manage your translations in Cumulocity IoT for all supported languages easily using the Translation UI Plugin. Translations can be added and updated using the Translation table. Translations are globally available in all Cumulocity applications running on the same Cumulocity tenant via the `public-options` application and its `options.json` file.

The Translation UI Plugin automatically register a new navigation entry in the left side navigation menu. The Translation Plugin can be accessed by navigating to `Configuration` -> `Localization`.

> **Important:** If the Plugin is used in an application it needs to check if the `public-options` application exists. The `public-options` application is used to store and share translations to the different applications. In case the application doesn't exist, it will automatically be created in the Cumulocity tenant via the Translation UI Plugin. Creating this application requires `Admin` permission on `Application management`. Therefore, make sure the plugin is initally called with a user having the necessary permission, otherwise an error will be thrown.
## Add new term with translations

To add a new term and corresponding translations follow these steps:

1. Click on the button `Add term` at the top-right in the action bar.

2. In the upcoming modal dialog provide the term (key), which should be translated, and the related translations.

3. Click on `Save` to store the changes locally.

4. In order for the translations to be applied to Cumulocity and its application, click on the `Apply` button in the action bar.

5. A success notification appears if the changes could be applied successfully

6. The translations are directly applied to your application.

![alt translation plugin demo](/assets/translation_plugin.gif)

## Update existing translations

Translations can be updated directly in the table or via the modal dialog.

**Table**

1. Hover over the cell in the table for the translation, which you want to update.

2. Click on the `Pencil` icon to edit the translation

3. Update the translation and click on the `Checkmark` icon to save the translation locally

4. Click on `Apply` in the action bar to apply the update to Cumulocity and its applications.

![alt translation plugin update translation directly in table](/assets/translation-update-table_plugin.gif)

**Modal dialog**

1. For the term you want to update the translations for, click on the `Edit` button at the right of the corresponding row.

2. Update the translations for the relevant languages

3. Click on `Save` to store the changes locally.

4. In order for the translations to be applied to Cumulocity and its application, click on the `Apply` button in the action bar.

5. A success notification appears if the changes could be applied successfully

![alt translation plugin update translation via modal dialog](/assets/translation-update-modal_plugin.gif)

## Delete a term

1. Hover over the row of the term you want to delete.

2. Click on the `Delete` button, which is displayed on hovering over the row.

3. In the upcoming confirmation dialog click on the `Confirm` button.

4. In order for the changes to take affect click on the `Apply` button in the action bar.

5. A success notification appears if the changes could be applied successfully.

![alt translation plugin delete a translation](/assets/translation-delete_plugin.gif)

## Local development

### Recommended version

* node v 14.x
* npm v 6.x

### Plugin versions

* Angular v 14.x
* WebSDK v 1016.0.x

**How to start**
Change the target tenant and application you want to run this plugin on in the `package.json`.

```
c8ycli server -u https://{{your-tenant}}.cumulocity.com/ --shell {{cockpit}}
```
Keep in mind that this plugin needs to have an app (e.g. cockpit) running with at least the same version as this plugin. if your tenant contains an older version, use the c8ycli to create a cockpit clone running with at least v 1016.0.59! Upload this clone to the target tenant (e.g. cockpit-1016) and reference this name in the --shell command.

The widget plugin can be locally tested via the start script:

```
npm start
```

In the Module Federation terminology, `widget` plugin is called `remote` and the `cokpit` is called `shell`. Modules provided by this `widget` will be loaded by the `cockpit` application at the runtime. This plugin provides a basic custom widget that can be accessed through the `Add widget` menu.

> Note that the `--shell` flag creates a proxy to the cockpit application and provides` AdvancedMapWidgetModule` as an `remote` via URL options.
Also deploying needs no special handling and can be simply done via `npm run deploy`. As soon as the application has exports it will be uploaded as a plugin.

------------------------------
These tools are provided as-is and without warranty or support. They do not constitute part of the Software AG product suite. Users are free to use, fork and modify them, subject to the license agreement. While Software AG welcomes contributions, we cannot guarantee to include every contribution in the master project.
_____________________
For more information you can Ask a Question in the [TECHcommunity Forums](http://tech.forums.softwareag.com/techjforum/forums/list.page?product=cumulocity).
You can find additional information in the [Software AG TECHcommunity](http://techcommunity.softwareag.com/home/-/product/name/cumulocity).
14 changes: 14 additions & 0 deletions app.module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
describe('Example test', () => {
/*let testComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ExampleModule]
});
testComponent = TestBed.createComponent(TestComponent);
});*/

test('Always true', () => {
expect(true).toBe(true);
});
});
22 changes: 22 additions & 0 deletions app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as ngRouterModule } from '@angular/router';
import { BootstrapComponent, CoreModule, RouterModule } from '@c8y/ngx-components';
import { BsModalRef } from 'ngx-bootstrap/modal';
import { TranslationDirectoryModule } from './modules/translation-manager/translation-directory.module';

// Translations
import './locales/de.po';

@NgModule({
imports: [
BrowserAnimationsModule,
ngRouterModule.forRoot([], { enableTracing: false, useHash: true }),
RouterModule.forRoot(),
CoreModule.forRoot(),
TranslationDirectoryModule,
],
providers: [BsModalRef],
bootstrap: [BootstrapComponent]
})
export class AppModule {}
Binary file added assets/translation-delete_plugin.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/translation-update-modal_plugin.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/translation-update-table_plugin.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/translation_plugin.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Internationalizing files in po format (https://en.wikipedia.org/wiki/Gettext#Translating)
* You can always add additional strings by adding your own po file. All po files are
* combined to one JSON file per language and are loaded if the specific language is needed.
*/
import './locales/de.po'; // <- adding additional strings to the german translation.
17 changes: 17 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import './polyfills';
import './i18n';

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

declare const __MODE__: string;
if (__MODE__ === 'production') {
enableProdMode();
}

export function bootstrap() {
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.log(err));
}
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// jest.config.js
module.exports = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.js'],
transformIgnorePatterns: ['/!node_modules\\/lodash-es/']
};
31 changes: 31 additions & 0 deletions locales/de.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
msgid ""
msgstr ""
"Project-Id-Version: c8y.plugin\n"
"Language: de\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

msgid "Terms"
msgstr "Begriffe"

msgid "Add term"
msgstr "Begriff hinzufügen"

msgid "Add translation"
msgstr "Übersetzung hinzufügen"

msgid "Edit translation"
msgstr "Übersetzung bearbeiten"

msgid "Localization"
msgstr "Lokalisierung"

msgid "Add term and translations"
msgstr "Begriff und Übersetzungen hinzufügen"

msgid "Please provide atleast one translation to save the key."
msgstr "Bitte mindestens eine Übersetzung angeben, damit der Pfad gespeichert werden kann"

msgid "e.g. Group"
msgstr "z.B. Gruppe"
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<div
(click)="editCell()"
*ngIf="!isCellEditable"
class="text-truncate pointer d-flex"
title="{{ context.value }}"
>
<span *ngIf="!isCellEditable && context.value !== ''" class="text-truncate">
{{ context.value }}
</span>
<span
*ngIf="!isCellEditable && context.value === ''"
class="text-truncate"
title="{{ 'Add translation' | translate }}"
>
<em class="text-muted">{{ 'Add translation' | translate }}</em>
</span>
<i
c8yIcon="pencil"
title="{{ 'Edit translation' | translate }}"
class="showOnHover text-primary m-l-4"
></i>
</div>

<div class="input-group input-group-sm" *ngIf="isCellEditable">
<input
type="text"
placeholder="{{ 'Add translation' | translate }}"
class="form-control"
#cellInput
[(ngModel)]="cellValue"
/>
<div class="input-group-btn">
<button
class="btn btn-clean"
type="button"
title="{{ 'Cancel' | translate }}"
(click)="cancel()"
>
<i c8yIcon="times" class="text-danger"></i>
</button>
<button class="btn btn-clean" type="button" title="{{ 'Save' | translate }}" (click)="save()">
<i c8yIcon="check" class="text-primary"></i>
</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { CellRendererContext } from '@c8y/ngx-components';
import { ManageTranslationCellRendererComponent } from './manage-translation-cell-renderer.component';

describe('ManageTranslationCellRendererComponent', () => {
let component: ManageTranslationCellRendererComponent;
let translationDirectoryServiceMock: any;
let contextMock: CellRendererContext;
let c8yModalServiceMock: any;
let translateServiceMock: any;

beforeEach(() => {
translationDirectoryServiceMock = {
saveTranslationLocally: jest.fn(),
validateTranslationIsProvided: jest.fn(),
deleteTranslationItem: { emit: jest.fn() },
saveTranslationItem: { emit: jest.fn() },
normalizeTranslationValues: jest.fn(),
isAssetTypeOrCustomProperty: jest.fn()
};

c8yModalServiceMock = {
confirm: jest.fn()
};

translateServiceMock = {
instant: jest.fn()
};

contextMock = new CellRendererContext();
contextMock.item = { translationKey: 'asset' };
contextMock.property = { name: 'de', path: 'de' };

component = new ManageTranslationCellRendererComponent(
contextMock,
translationDirectoryServiceMock,
c8yModalServiceMock,
translateServiceMock
);

jest.useFakeTimers();
});

describe('ManageTranslationCellRendererComponent test suite', () => {
it('User clicking on Edit icon to update the translation values', () => {
// given
const cellVal = 'Deutsch translation';
component.context.value = cellVal;
component.cellInput = { nativeElement: {} };
component.cellInput.nativeElement.focus = jest.fn();

// when
component.editCell();
jest.runAllTimers();

// expect
expect(component.cellValue).toBe(cellVal);
expect(component.isCellEditable).toBeTruthy();
});

it('User clicking on Save icon without providing any input values', () => {
// given
component.cellValue = '';

// when
component.save();

// expect
expect(component.isCellEditable).not.toBeTruthy();
expect(component.context.value).toBe('');
});

it('should display popup if no translation is provided for the key, on click of cancel should remove the popup', async () => {
// given
component.cellValue = '';
const modalSpy = spyOn(c8yModalServiceMock, 'confirm').and.returnValue(Promise.reject());

// when
await component.save();

// expect
expect(modalSpy).toHaveBeenCalled();
expect(component.isCellEditable).not.toBeTruthy();
});

it('User clicking on Save icon by providing input values', () => {
// given
component.cellValue = 'sample';

// when
component.save();

// expect
expect(component.context.value).toBe('sample');
});

it('If translation value is not provided while saving the cell', () => {
// given
component.context.value = '';
const spyDirectoryService = jest
.spyOn(translationDirectoryServiceMock, 'validateTranslationIsProvided')
.mockReturnValue(true);

// when
component.save();

// expect
expect(translationDirectoryServiceMock.validateTranslationIsProvided('sample')).toBe(true);
expect(spyDirectoryService).toHaveBeenCalled();
expect(component.context.item).toStrictEqual({ de: '', translationKey: 'asset' });
});

it('User clicking on Cancel icon should cancel the changes', () => {
// when
component.cancel();

// expect
expect(component.isCellEditable).not.toBeTruthy();
});
});
});
Loading

0 comments on commit a93702b

Please sign in to comment.