An APEX Item plugin for storing multi-language translations in a JSON column
An Oracle APEX Item plugin for storing multi-language translations (MLS) in a JSON column. Now with IG support!
This plugin allows you to store multiple language translations on a single table column. Optionally, the user can popup a dialog and edit all the available translations.
Add a column with a _JTL
suffix to your table (or any other suffix you like). For example, a NAME
column would become NAME_JTL
:
create table px_projects (
id number generated by default on null as identity (start with 1) primary key not null
, name_jtl varchar2(500) not null constraint px_projects_tl_ck CHECK (name_jtl is json(strict))
, alias varchar2(32)
, active_ind varchar2(1) not null
, created_by varchar2(60) default
coalesce(
sys_context('APEX$SESSION','app_user')
, regexp_substr(sys_context('userenv','client_identifier'),'^[^:]*')
, sys_context('userenv','session_user')
)
not null
, created_on date default sysdate not null
, updated_by varchar2(60)
, updated_on date
, constraint px_projects_ck_active
check (active_ind in ('Y', 'N'))
)
enable primary key using index
/
Use a size that is {Required Size} x {# of Languages} + offset
. The offset is around 18 per language.
When you create the APEX item, select a size for the item that matches your desired size per translated entry.
Optionally, use the APEX.PAGE_ITEM_IS_REQUIRED
message to translate your “#LABEL# must have some value.” to your correct language. The plugin validate
function will fetch this value (via wwv_flow_lang.system_message
) when the “Required” attribute is set.
The stored JSON format structure for a translation column is of this form:
[ {"l": "us", "tl": "Project Analysis"}
, {"l": "fr", "tl": "Analyse de projet"}
, {"l": "es", "tl": "Analisis de projecto"}]
Where l
is the language code, and tl
is the translation.
In general, the only time you care about the stored format is when you need to seed data in your tables. Otherwise, the plugin handles the JSON structure for you.
Thanks to the new JSON funtionality on 12c we can extract our JSON data with a JSON_TABLE
command. if you’re on 11g see here.
create or replace view px_projects_vl
as
select t.id
, t.name_jtl
, jd.lang
, jd.tl name
, t.alias
, t.active_ind
, t.created_by
, t.created_on
, t.updated_by
, t.updated_on
from px_projects t
, json_table(t.name_jtl, '$[*]'
columns (
lang varchar2(10) path '$.l'
, tl varchar2(60 char) path '$.tl'
)) jd
where jd.lang = (select nvl(apex_util.get_session_lang,'en') from dual)
/
The provided package tk_jtl_plugin
contains several useful functions and procedures.
For example, use tk_jtl_plugin.get_tl_value
on a validation to verify a name is unique within a language.
select 1
from px_projects_vl
where id <> nvl(:P25_ID, -1)
and name = tk_jtl_plugin.get_tl_value(:P25_NAME_JTL)
Also, the plugin render
and validate
functions are part of the tk_jtl_plugin
package. You’ll want to remove the code from the Plugin Source and change the render and validate functions to tk_jtl_plugin.render
and tk_jtl_plugin.validate
respectively. Moving the code to the database will greatly increase the load speed of all your plugins.
If you ever had to create a multi-language app, you know that a typical approach is to store the translated values on a translation table. Therefore, every translated entity (table) will require a child table with translations. This design pattern is cumbersome to work with and comes with significan application overhead.
The JSON Translation concept, or JTL for short, has several benefits for APEX applications:
_VL
)tk_jtl_plugin
package includes several useful functions and procedures for manipulating the JSON object in a programmatic way.You can still run this plugin on 11g without the JSON column support. Simply declare your column as varchar2
or clob
. Then use apex_json.to_xmltype
to convert the JSON to XML and xmltype
to extract the JSON.
select /*+ no_merge */ p.id
, t.lang
, t.tl
from px_projects p
, xmltable('/json/row' passing apex_json.to_xmltype(p.name_jtl)
columns
lang varchar2(10 char) path 'l'
, tl varchar2(50 char) path 'tl'
) t
You will definitely incur in a performance hit, both from the context switch when you call apex_json.to_xmltype
and from the conversion itself. However, this may be acceptable in your situation.
You’ll also miss out of the JSON constraint on the column, but this is not a concern when using the plugin, only if you manually insert data into the column.
_VL
view (as explained here) on the LOV to display the correct language._JTL
column datatype does not match required column size. Columns need to be as large as the desired size times the languages plus the JSON overhead. Use a size {Required Size} x {# of Languages} + offset
_JTL
columns be updated with the new language._VL
views as part of the JSON_TABLE
.CLOB
and then extract a VARCHAR2(4000)
with JSON_TABLE
. However, at this time, JSON_TABLE
cannot return CLOBs, so you will be limited to 4000 chars. I have not tested with the extended 32k varchars, that may be a way around this.If you’re upgrading from v1.0.0 or v1.1.0 to v1.2.0 the upgrade should be seamless.
In v1.2.0 the plugin did switch from the deprecated render function to the new render procedure. Significant changes were required to support IG. Chielfy, the JSON is not render as a hidden item any more. Instead, for regular items the initial values is set via a data-value
attribute and later maintain in the plugin memory but in sync with the data-value
attribute.
If you’ve implemented some jQuery or JavaScript calls into the plugin, be aware that the widget name changed to the more standard name ui.jtlItem
.
Starting with v1.1.0, the “Edit Languages” attribute defaults to true, but existing items will not be affected (this is standard APEX plugin upgrade procedure).
If you’re converting existing data to use the JTL column, see this Data Conversion for more details.
Although we successfully use this plugin and technique in production internally at Insum Solutions, use at your own risk and without any warranties.
Thanks to Insum Solutions for sponsoring this project.
The original concept for using JSON columns to store language is thanks to Bruno Mailloux.
The jQuery widget code based in part on code from, the professor, Dan McGhan.