Wednesday, February 26, 2020

Eine Oracle APEX Anwendung mehrsprachig erstellen Part 2




Wie in unserem vorherigen Blog erwähnt, möchten wir euch auch noch eine zweite Alternative vorstellen, die in einem Kundenprojekt durchgeführt wurde. Diesmal werden wir nicht den Ansatz von APEX verfolgen, sondern uns einen eigenständigen Übersetzungsprozess erarbeiten.

Wie machen wir das?

Anstelle auf das APEX Repository zuzugreifen, erstellen wir uns ein eigenes Repository in dem wir alle notwendigen Übersetzungen und verfügbaren Sprachen hinterlegen. Dieses kann bereits eine einfache Tabelle im beliebigen Datenbankschema sein.

Warum machen wir das?

Der große Vorteil an dieser Variante ist, dass wir von nun an mit beliebig vielen APEX Anwendungen und darüber hinaus auch Anwendungen aus anderer Herkunft (zum Beispiel ein BI-System, weitere Web-Anwendungen etc.) auf dieses Repository zugreifen können. Wird ein Text aktualisiert, aktualisieren sich alle dahinterliegenden Anwendungen automatisch ohne weiteren Aufwand. Kommt eine weitere Sprache hinzu, so ergänzt man diese zunächst in der Repository-Tabelle und stellt dann in den Anwendungen die neu hinzugefügte Sprache als Auswahlkriterium bereit.

Das klingt erstmal gut…ABER?

Wo viele Vorteile zu erkennen sind, sind meistens auch Nachteile versteckt. Verfolgen wir diesen Ansatz so werden wir schnell feststellen das der Programmieraufwand doch erheblich aufwendiger ist und wesentlich mehr Zeit beansprucht werden muss. Dies sollte euch und/oder dem Kunden zuvor bewusst sein. Zudem sollte man schauen das die Anwendungen nicht allzu groß werden, da sonst an manchen Stellen die Übersicht verloren gehen kann.


Kommen wir zum eigentlichen Prozess

Wie zuvor beschrieben benötigen wir als erstes ein Repository in unserer Datenbank wo wir zukünftig alle Übersetzungen ablegen. Dazu legen wir eine einfache Tabelle an.

CREATE TABLE TRANSLATIONS
   (TST_ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY
    TST_LANGUAGE VARCHAR2(10),
    TST_TEXT_ID NUMBER
    TST_TEXT VARCHAR2(100),
    CONSTRAINT translations_pk PRIMARY KEY (tst_id),
    CONSTRAINT translations_uk UNIQUE (TST_LANGUAGE, TST_TEXT_ID)
  );

In dieser Tabelle können wir jetzt unsere Übersetzungen hinterlegen, die wir später in unserer Anwendung anzeigen werden. Die Kombination „TST_LANGUAGE“ und TST_TEXT_ID“ ist dabei unser eindeutiger Indikator. Für unser Beispiel fügen wir ein paar Dummywerte der Tabelle hinzu.

INSERT INTO TRANSLATIONS (TST_LANGUAGE, TST_TEXT_ID, TST_TEXT) VALUES ('en'1'Employee Number');
INSERT INTO TRANSLATIONS (TST_LANGUAGE, TST_TEXT_ID, TST_TEXT) VALUES ('en'2'Name');
INSERT INTO TRANSLATIONS (TST_LANGUAGE, TST_TEXT_ID, TST_TEXT) VALUES ('en'3'Job');
INSERT INTO TRANSLATIONS (TST_LANGUAGE, TST_TEXT_ID, TST_TEXT) VALUES ('en'4'Salary');

INSERT INTO TRANSLATIONS (TST_LANGUAGE, TST_TEXT_ID, TST_TEXT) VALUES ('de'1'Personalnummer');
INSERT INTO TRANSLATIONS (TST_LANGUAGE, TST_TEXT_ID, TST_TEXT) VALUES ('de'2'Name');
INSERT INTO TRANSLATIONS (TST_LANGUAGE, TST_TEXT_ID, TST_TEXT) VALUES ('de'3'Job');
INSERT INTO TRANSLATIONS (TST_LANGUAGE, TST_TEXT_ID, TST_TEXT) VALUES ('de'4'Gehalt');

Datenbankseitig sind nun alle Vorbereitungen getroffen, so dass wir nun zur eigentlichen APEX Anwendung kommen. In diesem Beispiel werden wir einen einfachen Report erstellen, der über das Sprachmenü in der jeweiligen Sprache ausgegeben werden kann. Hierzu erstellen wir eine neue Anwendung und fügen einen Classic Report hinzu. Als SQL-Query für den Report verwenden wir folgendes Statement.

SELECT EMPNO, ENAME, JOB, SAL FROM EMP;

Als nächstes müssen wir ein Application-Item unter den „Shared Components“ anlegen. Dieses nennen wir „APP_LANGUAGE“ und wählen den Security Level „Checksum Required – Session Level“. Zu dem Application-Item hinterlegen wir noch eine Application-Computation die wir ebenfalls unter den „Shared Componnents“ finden.

  • Computation Item: APP_LANGUAGE
  • Computation Type: Static Assignment
  • Computation: en


Die Computation wird benötigt damit das Application-Item „APP_LANGUAGE“ zu Begin einen Wert hat und die Anwendung weiß in welcher Sprache Sie angezeigt werden soll. Hier könnte man auch anstelle einer statischen Angabe die Browsersprache oder eine in der Datenbank hinterlegte Sprache vom User hinterlegen. Aber für unser kleines Beispiel reicht uns dieser einfache Weg.

Um unsere Übersetzungen in der Anwendung ausgeben zu können müssen wir jetzt für jeden übersetzten Titel ein weiteres Application-Item anlegen. In unserer Demo wären das:

  • APP_EMPLOYEE_NUMBER_LABEL
  • APP_NAME_LABEL
  • APP_JOB_LABEL
  • APP_SALARY_LABEL


Diese werden wir jedoch nicht statisch über eine Computation füllen, sondern über einen Application-Process, der die Übersetzungen aus der Datenbank lädt und den entsprechenden Item´s zuordnet.

Hierzu legen wir einen neuen Application-Process an den wir „get_labels“ nennen und als Point „After Authentication“ einstellen. Als PL/SQL Code verwenden wir folgendes Statement.

SELECT MAX(CASE WHEN TST_TEXT_ID = 1 THEN TST_TEXT ENDAS APP_EMPLOYEE_NUMBER_LABEL,
       MAX(CASE WHEN TST_TEXT_ID = 2 THEN TST_TEXT ENDAS APP_NAME_LABEL,
       MAX(CASE WHEN TST_TEXT_ID = 3 THEN TST_TEXT ENDAS APP_JOB_LABEL,
       MAX(CASE WHEN TST_TEXT_ID = 4 THEN TST_TEXT ENDAS APP_SALARY_LABEL
  INTO :APP_EMPLOYEE_NUMBER_LABEL,       
       :APP_NAME_LABEL,
       :APP_JOB_LABEL,
       :APP_SALARY_LABEL
  FROM TRANSLATIONS
 WHERE TST_LANGUAGE = :APP_LANGUAGE;

Jetzt haben wir erreicht das nach erfolgreichem Login die benötigten Übersetzungen aus der Datenbank geladen wurden und den entsprechenden Items zugeordnet sind.

Starten wir jetzt die Anwendung und schauen in den Session States uns die Application-Items an, sehen wir folgendes Ergebnis.



Aktuell passiert jedoch sonst nichts Weiteres mit unseren Labels. Somit müssen wir diese als nächstes in unserer Anwendung positionieren. Dazu wechseln wir zu unserem Classic Report und gehen einzeln die Columns durch. Bei jeder Column muss das statisch hinterlegte „Heading“ durch einen Substitution-String ersetzt werden der dann auf unser Application–Item verweist. Substitution-Strings beginnen immer mit einem „&“ gefolgt vom Item-Name und endend mit einem „.“. Zum Beispiel schreiben wir für die Column „EMPNO“ im Heading „&APP_EMPLOYEE_NUMBER_LABEL.“, für „ENAME“ -> „&APP_NAME_LABEL.“ usw.

Jetzt haben wir die Übersetzungstexte erfolgreich aus der Datenbank geladen, Application-Items zugeordnet und in der Anwendung positioniert. Laden wir erneut die Seite mit dem Classic Report sehen wir jetzt das die Überschriften die Texte aus der Datenbank bzw. aus den Application-Items ausgeben.

Zuletzt müssen wir nun noch die Auswahl der Sprache integrieren, so dass der Anwender zwischen den Sprachen wählen kann. Dazu erstellen wir eine Auswahlliste mit den Sprachen in der Navigationsleiste. Gehe hierzu zu den „Shared Components“ > „Navigation“ > „Navigation Bar List“ > „Desktop Navigation Bar“. Zunächst benötigen wir den übergeordneten Eintrag „Language“. Hierzu über „Create Entry“ einen neuen Eintrag anlegen und bei „List Entry Label“ „Language“ eingeben. Alles weitere kann außer Acht gelassen werden. Nun legen wir für jede Sprache (in unserem Beispiel „Englisch“ und „Deutsch“) weitere untergeordnete Einträge an. Also wieder „Create Entry“ und nun folgendes eingeben.

  • List Entry Label: English (German)
  • Parent List Entry: Language
  • Target type: page in this application
  • Page: &APP_PAGE_ID.
  • Request: LANG
  • Set these items: APP_LANGUAGE
  • With these values: en (de)


Damit nach Auswahl der Sprache die Übersetzungen neu geladen werden benötigen wir noch einen weiteren Application-Process der auf den zuvor hinterlegten Request „LANG“ reagiert.
Zurück zu den „Shared Components“ und unter Application-Processes einen neuen Process erstellen. Als Name verwenden wir jetzt „get_labels_change_language“ und als Process-Point wird „On load: Before Header (page template header)“ verwendet. Der PL/SQL Code ist der identische vom vorherigen Application-Process. Wichtig ist noch, dass wir den Condition Type „Request = Expression1“ auswählen und als Expression „LANG“ eingeben, so dass der Application-Process startet sobald der Request „LANG“ ausgeführt wird.


Geschafft! 

Unsere Anwendung ist jetzt mehrsprachig und der Classic Report kann in unterschiedlichen Sprachen ausgegeben werden. Dieses Szenario kann natürlich noch auf weitere Texte ausgeübt werden (zum Beispiel Sign Out -> Abmelden oder Language -> Sprache). Einfach wieder Datenbankeinträge vornehmen, Application Items anlegen, den Application Process erweitern und die Substitution-Strings an gewählter Stelle hinterlegen.


Mein persönliches Fazit:

Wie man sieht gibt es immer verschiedene Möglichkeiten Aufgaben umzusetzen. Welcher Weg der Beste ist kann an dieser Stelle nicht pauschal gesagt werden. Persönlich würde ich die in Part 1 beschriebene Variante vorziehen, sicherlich gibt es jedoch auch Anwendungsfälle wo dieser beschriebene Weg sinnvoller ist. Auch wenn hier der Entwicklungsprozess im ersten Moment aufwendiger wirkt kann auf langer Sicht gesehen dieser Prozess deutlich mehr Geschwindigkeit mit sich bringen. Man könnte kurzerhand eine kleine Anwendung zur Verfügung stellen, um die Übersetzungen in einem schönen Web-Formular zu pflegen. So bräuchte der Anwender nichts mehr in der Datenbank vornehmen, oder wie in Part 1 beschrieben den Prozess mit den XLIF Dateien durchgehen. So kommt man ganz ohne Admin-Rechte zum gewünschten Ziel.

Sicherlich gibt es auch noch weitere Methoden, auf die ich mich freuen würde wenn sie uns jemand mitteilen würde.

Labels: ,

Sunday, February 16, 2020

Drag and Drop Regions in Oracle Apex Application



Recently we have worked on a project and we needed to implement drag and drop functionality into a classic report region to enable our users to drag some items and drop them in a drop region. The objective was to give the users a good User Experience when they work on the Application. At that time, we implemented the functionality using some JavaScript and CSS tricks. It worked fine, but it was not the best practice solution to use in Apex. By the time at working in Apex, we have learned some cool features which will make our drag and drop regions more efficient and it was worth to rebuild the regions again.

Now we would like to share our solution with you and show you how we have implemented this cool feature into our Application.

You will need a Workspace to follow with us. If you don't have one, go to apex.oracle.com and request a free Workspace.

To demonstrate the Idea we will work on a simple Project. Suppose we have a To-do list, and we want to change the To-do's status by dragging the Finished To-do from the Open To-do's Region and drop it into the Finished To-do's Region.

Here is the Table which we will work on:

CREATE TABLE TB_TODOS (
ID NUMBER GENERATED ALWAYS AS IDENTITY(START with 1 INCREMENT by 1),
TITLE VARCHAR2(50),
STATUS VARCHAR2(10)
);

Let's insert some dummy data to work with:

INSERT INTO TB_TODOS (TITLE, STATUS) values ('Buy bananas', 'open');
INSERT INTO TB_TODOS (TITLE, STATUS) values ('Buy apple', 'open');
INSERT INTO TB_TODOS (TITLE, STATUS) values ('Go to doctor', 'open');
INSERT INTO TB_TODOS (TITLE, STATUS) values ('Call Mohamad', 'open');
INSERT INTO TB_TODOS (TITLE, STATUS) values ('Go to Bank', 'open');
INSERT INTO TB_TODOS (TITLE, STATUS) values ('Buy orange', 'open');

So with this done let's create a new Project in Apex, navigate to the Project, go to Shared Component, choose Templates then click Create.

We will start by creating a custom template, the template type we need is a Report type and we will create it from scratch.



make sure to choose "Named Column (row template)"



In this template, we will define the structure of our Report.

Each row in the Report will have the structure:

<div>
<li data-id='#ID#' data-status='#STATUS#' class='todo-el' draggable=true>#TITLE#</li>
</div>

but we have to open an Html unordered list tag before all the list elements and close the tag after we finish rendering all the list items, we can achieve that by adding a ul open tag in Before Rows and ul close tag in After Rows like this:


Notice that all the list elements will have the class todo-el, and the unordered list element will have the class todos so we can target those elements in CSS. Also, each list element has the following attributes:
  • data-id: to store custom data in each li element. In this case, we are storing an ID that will be replaced by the ID value of the Todo from our table. we will use this attribute later in Javascript.
  • data-status: to store the To-do's status.
  • draggable: specifies whether an element is draggable or not (Html5).
The unordered list element ul has ondragstart Event attribute that fires when the user starts dragging an element, we will handle this event later in Javascript using handleDrag function.

We can now create a classic report for the Open Todos based on our template.
Navigate to Page1 in our Application and add a new Classic Report region.
For the Report source choose the Table which we have already created:


Notice the Where Clause, we are querying only the Todos that have the status open.
Then go to the Report attributes, under the appearance options choose the template which we have already created:


(optional) Add the following CSS code in the page Inline Css:

.todos{
list-style-type: none;
}
.todo-el {
padding: 10px;
}

the CSS code is simply removing the default list style of the unordered list and it adds some padding to each list element.


Now we create the Finished Todo's Region, to do that we follow the same steps we did to create the Open Todo's Region but for the new Region we want only to show the Finished Todos:



We want to give the Region static Id finished and do some changes in the Layout so the two regions will be displayed next to each other: 



We should now listen to the drop event, to achieve that we will add ondragover and ondrop event attributes for both Region, select both regions, go to Advanced > Custome Attributes and add the code below:
ondragover='handleDragover(event)'
ondrop='handleDrop(event)'

  • ondragover: fires when a draggable element is being dragged over a valid drop target.
  • ondrop: occurs when a draggable element is dropped on a valid drop target.


With this done, we can now start handling the drag and drop events...

First, let's handle the drag functionality by defining the handledrag function at the page level. Copy the code below into the Global Functions in your page:
function handleDrag(event) {
var data = {id: event.target.dataset.id, status: event.target.dataset.status};
var json_data = JSON.stringify(data);
event.dataTransfer.setData('data', json_data);
}
in this function we are simply fetching our data from the event object, creating a JSON object from this data and converting the JSON object to a string, then we are setting the drag operation's data using setData method.

Before we handle the drop event we should create an Ajax callback process which we will call when we drop a Todo item. The process then will update the Todo's status in the Database.
Go to the page processes and create an Ajax Process:
Add the code below:
IF apex_application.g_x01 = 'open' THEN
UPDATE TB_TODOS SET STATUS = 'finished' WHERE ID = apex_application.g_x02;
ELSE
UPDATE TB_TODOS SET STATUS = 'open' WHERE ID = apex_application.g_x02;
END IF;
the g_x01,g_x02 are a Global variable and they will be passed from the Javascript.

Now go back to the page's global function and add the code below to handle the drop event:
function handleDragover(event){
event.preventDefault();
}
function handleDrop(event){
var id = JSON.parse(event.dataTransfer.getData('data')).id;
var status = JSON.parse(event.dataTransfer.getData('data')).status;
apex.server.process(
'Update Todo',
{
x01: status,
x02: id
},
{
success: function(){
apex.event.trigger('#open', 'apexrefresh');
apex.event.trigger('#finished', 'apexrefresh')
},
dataType: 'text'
});
}
in handleDrop function we are parsing the string data that we have sent back into JSON object and then accessing the id and status values. In the next step we are making an Ajax request to update the Todo's status, after the Ajax request is successfully finished we are refreshing the regions to get the updated data.

So that's it :), we hope you liked it. In the next post, we will configure the functionality to make it works on the Touch screen devices.
Here is a demo application if you want to test the functionality demo.
You can find the source code on Github.

we also have published a video on Youtube while we are creating a demo application, you can also watch it:



Some References:

Labels: , ,

Tuesday, February 11, 2020

Eine Oracle APEX Anwendung mehrsprachig erstellen Part 1




Heutzutage ist es immer wichtiger Webbasierte Anwendungen mehrsprachig zu gestallten. APEX bietet hier einen standardisierten Prozess, der die Umsetzung dem Entwickler deutlich erleichtert.

Wird der Prozess über APEX ausgeführt, so erstellt APEX automatisch für jede Sprache eine Kopie der Hauptanwendung („Shadow-Anwendung“). Shadow-Anwendungen werden nicht im Application-Builder aufgeführt und können somit nicht bearbeitet werden. Alle Codeänderungen werden weiterhin in der Haupt-Anwendung vorgenommen. Diese müssen dann anschließend wieder neu in die übersetzten Shadow-Anwendungen übertragen werden.

Nun aber zur eigentlichen Umsetzung :-)

Als erstes müssen wir der Anwendung beibringen, dass diese Mehrsprachigkeit unterstützt. Hierzu muss unter „Shared Components“ > „Globalization Attributes“ der Wert „Item Preference (use item containing preference)“ für “Application Language Derived From” gesetzt werden.


Als nächstes müssen wir ein Application-Item unter den „Shared Components“ anlegen. Dieses nennen wir „FSP_LANGUAGE_PREFERENCE“ und wählen den Security Level „Checksum Required – Session Level“. „FSP_LANGUAGE_PREFERENCE“ ist ein von APEX reserviertes Item welches nach dem rendern einer Seite feststellt in welcher Sprache die Seite angezeigt werden soll.

Jetzt benötigen wir noch einen Prozess, der die Seite neu lädt, nachdem die Sprache geändert wird. Hierzu legen wir unter „Shared Components“ einen neuen Apllication-Process an. Diesen nennen wir z.B. „set_language“ und wählen den Process-Point „On load: Before Header (page template header)“. In dem Source-Block geben wir folgenden PL/SQL Code ein:

begin
    apex_util.redirect_url(apex_page.get_url(p_page => :APP_PAGE_ID));
end;

Jetzt noch den Condition Type „Request = Expression1“ auswählen und als Expression „LANG“ eingeben.

Als nächstes müssen wir die Möglichkeit der Sprachauswahl implementieren. Dazu erstellen wir eine Auswahlliste mit den Sprachen in der Navigationsleiste. Gehe hierzu zu den „Shared Components“ > „Navigation“ > „Navigation Bar List“ > „Desktop Navigation Bar“. Zunächst benötigen wir den übergeordneten Eintrag „Sprachen“. Hierzu über „Create Entry“ einen neuen Eintrag anlegen und bei „List Entry Label“ „Sprachen“ eingeben. Alles weitere kann außer Acht gelassen werden. Nun legen wir für jede Sprache (in unserem Beispiel „Deutsch“) weitere Untergeordnete Einträge an. Also wieder „Create Entry“ und nun folgendes eingeben.

  • List Entry Label: Deutsch
  • Parent List Entry: Sprachen
  • Target type: page in this application
  • Page: &APP_PAGE_ID.
  • Request: LANG
  • Set these items: FSP_LANGUAGE_PREFERENCE
  • With these values: de


Falls weitere Sprachen hinzugefügt werden sollen, müssen diese Schritte wiederholt werden.

Nun sind alle Vorbereitungen getroffen, so dass wir mit den eigentlichen Übersetzungen beginnen können ;-)

APEX bietet für diesen Prozess einen Übersetzungsworkflow der aus einigen Schritten besteht, die bei jedem Rollout einer neuen Version wiederholt werden müssen.

Den Prozess findet man unter den „Shared Components“ > „Globalization“ > „Translate Application.



1) Define Application Languages

Unter diesem Punkt erstellen wir die „Shadow-Anwendungen“ und teilen der Haupt-Anwendung mit in welchen Sprachen übersetzt werden soll. Klickt „Create“ und vergebt der „Shadow-Anwendung“ eine eindeutige ID (es empfiehlt sich die ID der Hauptanwendung zu verwenden und diese um 01,02,03…zu ergänzen). Dann noch unter „Language“ die Sprache auswählen und mit „Create“ bestätigen.

2) Seed Translatable Text

Hierüber werden die Texte zur Übersetzung in einer Datei exportiert. Die „Translated Application“ muss hierzu ausgewählt werden und dann per klick auf „Seed“ exportiert werden. Jetzt erstellt APEX eine XLIFF Datei die im nächsten Schritt heruntergeladen werden kann.

3) Download XLIFF translation files

Zurück zu Translate geht es nun mit dem nächsten Schritt „Download XLIFF translation files“ weiter. Auf dieser Seite haben wir nun 2 Möglichkeiten. Entweder man exportiert expliziet eine Seite der Anwendung oder die komplette Anwendung. Jetzt muss nur noch die gewünschte Sprache gewählt werden und auf „Export XLIFF file for Application/Page“ geklickt werden. Jetzt kann die Datei lokal gespeichert und bearbeitet werden. Die Datei ähnelt von der Struktur her einer XML Datei. Unter Source findet man die Originalsprache und unter Target die Zielsprache. Nachdem alle Übersetzungen vorgenommen wurden kann die Datei gespeichert und geschlossen werden.

4) Apply XLIFF Translation Files

Hier wird die Datei anschließend wieder hochgeladen und alle Übersetzungen werden importiert. Das geht über den „Upload Files“ Button. Nach erfolgreichem Upload dann die Datei auswählen und der Zielanwendung zuordnen (Apply to Translation). Anschließend „Apply Checked“ klicken.

5) Publish Translated Applications

Und nun kommen wir zum letzten finalen Schritt ;-) Wieder zurück zu „Translate“ klicken wir nun auf den letzten Schritt „Publish Translated Applications“. Hier die Sprachen über die Checkbox auswählen und dann zu erst auf „Seed“ klicken und anschließend „Publish“.


Fertig! Die Anwendung steht nun in mehreren Sprachen zur Verfügung und kann per Klick in der Navigation geändert werden.


Noch ein kleiner Tipp:

Im „Translation Repository“ (Shared Components -> Globalization -> Translate Application -> Translation Repository) können die Übersetzungen noch überprüft werden, oder auch geändert werden falls man nicht den Weg über die XLIFF Datei gehen möchte. Sollten hier Änderungen vorgenommen werden muss anschließend immer noch wie unter Punkt 5 beschrieben die Anwendung per „Seed“ und „Publish vordefiniert und veröffentlicht werden.


Mein persönliches Fazit zum Übersetzungsprozess:

Oracle APEX bietet dem Entwickler mit dieser Methode einen wirklich einfachen und intuitiven Weg eine Anwendung schnell und unkompliziert mehrsprachig zu erstellen. Als kleinen Nachteil würde ich erachten, dass man diesen Prozess immer für jede Anwendung separat betrachten und durchführen muss. Eine gesammelte Übersetzungsdatenbank steht hierbei nicht zur Verfügung und auch in der XLIFF Datei müssen gegebenenfalls Texte mehrfach übersetzt werden. Dennoch ist es im Großen und Ganzen sehr einfach und schnell durchzuführen :-)


In meinem nächsten Blogeintrag werde ich über eine weitere Methode schreiben, in der beschrieben wird wie man einen Übersetzungsprozess ohne dem APEX Standard realisieren kann.

Labels: ,