2023年1月12日木曜日

XLIFFファイル翻訳支援アプリの作成(2) - 翻訳ページの作成

 翻訳作業に使用するページを完成させます。


APEXコレクション名の設定


翻訳元と翻訳済みの文字列は、APEXコレクションに保存した上で対話グリッドから操作します。APEXコレクション名をSQLやPL/SQLに直書きすることを避けるため、置換文字列を設定します。

アプリケーション定義を開き、置換文字列G_TRANSLATE_COLLECTIONに対して、置換値としてTRANSLATEを設定します。

変更の適用をクリックします。

翻訳に使用するAPEXコレクションの名前はTRANSLATEになります。


ページ・アイテムとボタンの配置


ページ・デザイナでページ番号のページTranslateを開きます。

このページで行う4つの作業を実行するボタンを作成します。ボタン名はそれぞれLOAD_FROM_SOURCESTORE_AS_RESULTLOAD_FROM_RESULTBATCH_TRANSLATEとします。ラベルボタン名から生成されたものを使用します。

作成したボタンを横一列に並べるため、LOAD_FROM_SOURCEを除くボタンのレイアウト新規行の開始OFFにします。


翻訳対象となっている表CWR_XLIFF_FILESの列のIDの値を保持するページ・アイテムを作成します。

ページ・アイテムの識別名前P3_IDタイプとして選択リストを選びます。ラベルXLIFF Nameとします。アプリケーションとしては列IDの値が保存されていれば十分ですが、ユーザーには分かりやすくIDに代えて列Nameの値を表示します。

LOVタイプSQL問合せSQL問合せとして以下を記述します。追加値の表示NULL値の表示は共にOFFです。

select name, id from cwr_xliff_files where id = :P3_ID

ユーザーが設定する値ではないため、読取り専用タイプとして常時を指定します。


XLIFFの属性であるsource_langとtarget_langを画面に表示するために使用する、ページ・アイテムを作成します。

source_langを表示するページ・アイテムは識別名前P3_SOURCE_LANGとします。タイプとして表示のみを選択します。ラベルSource Langとします。

設定ページの送信時に送信OFFにし、ページ・アイテムP3_IDの右隣に配置されるよう、レイアウト新規行の開始OFFにします。


target_langを表示するページ・アイテムP3_TARGET_LANGも同様に作成します。ラベルTarget Langです。


以上でボタンとページ・アイテムの配置は完了です。


対話グリッドの調整



対話グリッドで実施できる作業を、翻訳結果となる文字列の更新に限定します。

対話グリッドTranslateを選択し右ペインで属性を開きます。

編集実行可能な操作から行の追加行の削除チェックを外します。また、ツールバーコントロールにある保存ボタンチェックを外します


対話グリッドの列セレクタ(APEX$ROW_SELECTOR)と行アクション(APEX$ROW_ACTION)は不要なので、構成ビルド・オプションCommented Out(コメント・アウト)します。


APEXコレクションを一意に指定する列はSEQ_IDです。列SEQ_IDを選択し、ソース主キーONにします。


IDを更新することはないため、識別タイプ表示のみとし、ソース問合せのみONにします。


翻訳元となる文字列を保持する列SOURCE_TEXTも更新することはないため、タイプ表示のみに変更し、ソース問合せのみONにします。


以上で対話グリッドの調整は完了です。


対話モード・レポートからリンク



アップロードしたXLIFFファイルの一覧から翻訳作業のページを開くために、リンクを作成します。

ページ番号XLIFF Filesのページを開き、対話モード・レポートの列Nameを選択します。

識別タイプリンクに変更し、リンクの属性を更新します。


ターゲットをクリックし、リンク・ビルダー・ターゲットの画面を開きます。

ターゲットタイプとしてこのアプリケーションのページを選択し、ページ(ページTranslate)を指定します。

アイテムの設定として名前P3_ID#ID#を指定することで、選択したID値をページ・アイテムP3_IDに渡します。

以上でOKをクリックします。




プロセスの作成



対話グリッドに更新した翻訳済み文字列がAPEXコレクションに保存されるように、作成済みのプロセスTranslate - Save Interactive Grid Dataを更新します。

ページ・デザイナにてページ番号のページTranslateを開きます。

左ペインでプロセス・ビューを表示し、プロセスTranslate - Save Interactive Grid Dataを選択します。

設定ターゲット・タイプPL/SQL Codeに変更し、挿入/更新/削除するPL/SQLコードとして以下を記述します。この対話グリッドで許可されている操作は更新のみなので、更新に対応する処理のみ記述します。

-- begin
-- 対話グリッドで許可しているのは編集のみなのでROW_STATUSの確認は不要。
-- if :APEX$ROW_STATUS = 'U' then
apex_collection.update_member_attribute(
p_collection_name => :G_TRANSLATE_COLLECTION
, p_seq => :SEQ_ID
, p_attr_number => 3
, p_attr_value => :TARGET_TEXT
);
-- end if;
-- end;

失われた更新の防止OFF行のロックNoにします。APEXコレクションとして保持されているデータはAPEXセッションにアクセスが限定されているため、これらの保護は必要ありません。

サーバー側の条件としてボタン押下時STORE_AS_RESULTを指定します。


作成済みの4つのボタンの処理を行うプロセスを、それぞれ作成します。

最初にボタンが行う処理を記述したパッケージCWR_UTILを作成します。パッケージCWR_UTILのコードは以下になります。ボタン名がクリックされたときに、同名のプロシージャが呼び出されるようにします。

create or replace package CWR_UTIL as
/*
表CWR_XLIFF_FILESのXLIFF_SOURCEとして保存されているXLIFFファイルを
APEXコレクションに取り込む。
*/
procedure load_from_source (
p_id in number
, p_source_lang out varchar2
, p_target_lang out varchar2
, p_collection_name in varchar2 default 'TRANSLATE'
);
/*
表CWR_XLIFF_FILESのXLIFF_RESULTとして保存されているXLIFFファイルを
APEXコレクションに取り込む。
*/
procedure load_from_result (
p_id in number
, p_source_lang out varchar2
, p_target_lang out varchar2
, p_collection_name in varchar2 default 'TRANSLATE'
);
/*
APEXコレクションに保持されている翻訳結果をXLIFF_SOURCEに適用し、
XLIFF_RESULTに保存する。
*/
procedure store_as_result (
p_id in number
, p_collection_name in varchar2 default 'TRANSLATE'
);
/*
APEXコレクションに保持されている翻訳前の文字列を、表CWR_MESSAGESの
内容を使って、一括で翻訳する。
*/
procedure batch_translate (
p_source_lang in varchar2
, p_target_lang in varchar2
, p_collection_name in varchar2 default 'TRANSLATE'
);
end;
/
create or replace package body cwr_util
as
/*
XLIFFファイルよりsource_langとtarget_langを取り出す。
*/
procedure get_source_and_target_lang(
p_xmlDoc in dbms_xmldom.domdocument
,p_source_lang out varchar2
,p_target_lang out varchar2
)
as
l_attrs dbms_xmldom.domnamednodemap;
begin
/* file要素の属性より、ソース言語とターゲット言語を取得する。 */
l_attrs := dbms_xmldom.getAttributes(
dbms_xmldom.item(
dbms_xmldom.getElementsByTagName(p_xmlDoc, 'file'),
0)
);
p_source_lang := dbms_xmldom.getNodeValue(
dbms_xmldom.getNamedItem(l_attrs, 'source-language')
);
p_target_lang := dbms_xmldom.getNodeValue(
dbms_xmldom.getNamedItem(l_attrs, 'target-language')
);
end get_source_and_target_lang;
/*
BLOBとして渡されるXLIFF(XML)から、翻訳前の文字列と翻訳済みの文字列から
APEXコレクションを作成する。
*/
procedure load_from_blob (
p_blob in blob -- XLIFFの内容
, p_source_lang out varchar2
, p_target_lang out varchar2
, p_collection_name in varchar2
)
as
l_xml xmltype;
l_xmlDoc dbms_xmldom.domdocument;
l_transUnits dbms_xmldom.domnodelist;
l_transUnit dbms_xmldom.domelement;
l_id varchar2(80);
l_source_text cwr_messages.message_text%type;
l_target_text cwr_messages.message_text%type;
begin
/* BLOBよりDOMとして操作可能なXML文書を作成する。 */
l_xml := xmltype.createXML(p_blob, NLS_CHARSET_ID('AL32UTF8'), null);
l_xmlDoc := dbms_xmldom.newDOMDocument(l_xml);
get_source_and_target_lang(
p_xmlDoc => l_xmlDoc
,p_source_lang => p_source_lang
,p_target_lang => p_target_lang
);
/* XMLに含まれるtrans-unitの要素を全て取り出し、APEXコレクションに投入する。 */
apex_collection.create_or_truncate_collection(p_collection_name);
l_transUnits := dbms_xmldom.getElementsByTagName(l_xmlDoc, 'trans-unit');
for i in 0..dbms_xmldom.getlength(l_transUnits)-1
loop
l_transUnit := dbms_xmldom.makeElement(
dbms_xmldom.item(l_transUnits,i)
);
-- trans-unitのidを取り出す。
l_id := dbms_xmldom.getAttribute(l_transUnit, 'id');
-- trans-unitに含まれる要素source(翻訳対象の文字列)の値を取り出す。
l_source_text := dbms_xmldom.getNodeValue(
dbms_xmldom.getFirstChild(
dbms_xmldom.item(
dbms_xmldom.getChildrenByTagName(l_transUnit, 'source'),
0
)
)
);
-- trans-unitに含まれる要素target(翻訳済みの文字列)の値を取り出す。
l_target_text := dbms_xmldom.getNodeValue(
dbms_xmldom.getFirstChild(
dbms_xmldom.item(
dbms_xmldom.getChildrenByTagName(l_transUnit, 'target'),
0
)
)
);
-- 取り出した値をAPEXコレクションに保存する。
apex_collection.add_member(
p_collection_name => p_collection_name
, p_c001 => l_id
, p_c002 => l_source_text
, p_c003 => l_target_text
);
end loop;
end load_from_blob;
procedure load_from_source(
p_id in number
, p_source_lang out varchar2
, p_target_lang out varchar2
, p_collection_name in varchar2
)
as
l_blob blob;
begin
/* 列XLIFF_SOURCEからAPEXコレクションを作成する。 */
select xliff_source into l_blob from cwr_xliff_files where id = p_id;
load_from_blob(
p_blob => l_blob
, p_source_lang => p_source_lang
, p_target_lang => p_target_lang
, p_collection_name => p_collection_name
);
end load_from_source;
procedure load_from_result(
p_id in number
, p_source_lang out varchar2
, p_target_lang out varchar2
, p_collection_name in varchar2
)
as
l_blob blob;
begin
/* 列XLIFF_RESULTからAPEXコレクションを作成する。 */
select xliff_result into l_blob from cwr_xliff_files where id = p_id;
load_from_blob(
p_blob => l_blob
, p_source_lang => p_source_lang
, p_target_lang => p_target_lang
, p_collection_name => p_collection_name
);
end load_from_result;
procedure store_as_result (
p_id in number
, p_collection_name in varchar2
)
as
l_xml xmltype;
l_blob blob;
l_blob_result blob;
l_xmlDoc dbms_xmldom.domdocument;
l_source_lang varchar2(80);
l_target_lang varchar2(80);
l_transUnits dbms_xmldom.domnodelist;
l_transUnit dbms_xmldom.domelement;
l_id varchar2(80);
l_target dbms_xmldom.domnode;
l_target_text cwr_messages.message_text%type;
begin
/* XLIFF_SOURCEを取り出し、一時LOBにコピーした上でDOMを生成する。 */
select xliff_source into l_blob from cwr_xliff_files where id = p_id;
dbms_lob.createtemporary(l_blob_result, true);
dbms_lob.copy(
dest_lob => l_blob_result
, src_lob => l_blob
, amount => dbms_lob.lobmaxsize
);
l_xml := xmltype.createXML(l_blob_result, NLS_CHARSET_ID('AL32UTF8'), null);
l_xmlDoc := dbms_xmldom.newDOMDocument(l_xml);
get_source_and_target_lang(
p_xmlDoc => l_xmlDoc
,p_source_lang => l_source_lang
,p_target_lang => l_target_lang
);
/* ソースとなるXLIFFのtrans-unitを全て取り出し、APEXコレクションに保持されている翻訳済み
* 文字列でtargetを置き換える。
*/
l_transUnits := dbms_xmldom.getElementsByTagName(l_xmlDoc, 'trans-unit');
for i in 0..dbms_xmldom.getlength(l_transUnits)-1
loop
l_transUnit := dbms_xmldom.makeElement(
dbms_xmldom.item(l_transUnits,i)
);
l_id := dbms_xmldom.getAttribute(l_transUnit, 'id');
select c003 into l_target_text from apex_collections where collection_name = p_collection_name and c001 = l_id;
l_target := dbms_xmldom.getFirstChild(
dbms_xmldom.item(
dbms_xmldom.getChildrenByTagName(l_transUnit, 'target'),
0
)
);
dbms_xmldom.setNodeValue(l_target, l_target_text);
end loop;
/* 置き換えたDOMをBLOBに変換し、XLIFF_RESULTに保存する。 */
update cwr_xliff_files
set xliff_result = l_xml.getblobval(NLS_CHARSET_ID('AL32UTF8'))
,xliff_result_filename =
replace(xliff_source_filename,'.xlf','_translated.xlf')
,xliff_result_mimetype = xliff_source_mimetype
,xliff_result_charset = xliff_source_charset
,xliff_result_lastupd = sysdate
,source_lang = l_source_lang
,target_lang = l_target_lang
where id = p_id;
end store_as_result;
procedure batch_translate(
p_source_lang in varchar2
, p_target_lang in varchar2
, p_collection_name in varchar2
)
as
l_target_text cwr_messages.message_text%type;
begin
/* APEXコレクションのC002 - 翻訳対象文字列 - に一致する文字列を表CWR_MESSAGESより探し、
* 一致する文字列があれば、その翻訳結果でC003 - 翻訳済み文字列 - を置き換える。
* 翻訳対象文字列に複数の翻訳済み文字列が存在する場合は(選択する基準がないので)、最初に見つけた
* 結果を採用する。
*/
for c in (select seq_id, c001, c002 from apex_collections where collection_name = p_collection_name)
loop
begin
/* 翻訳済み文字列を探す。 */
select t.message_text into l_target_text
from cwr_messages s join cwr_messages t on s.name = t.name
where s.message_language = p_source_lang
and t.message_language = p_target_lang
and s.message_text = c.c002
order by s.name fetch first 1 rows only;
/* APEXコレクションのC003のみを更新する。 */
apex_collection.update_member_attribute(
p_collection_name => p_collection_name
, p_seq => c.seq_id
, p_attr_number => 3
, p_attr_value => l_target_text
);
exception
when others then
null; -- 翻訳文が見つからなければ、何も変更しない。
end;
end loop;
end batch_translate;
end cwr_util;
/
view raw cwr_util.sql hosted with ❤ by GitHub

パッケージの作成には、SQLワークショップSQLスクリプトなどを使用できます。Autonomous Databaseであれば、この記事による実行も可能です。

プロセスを作成し、識別名前Load From Sourceとします。タイプとしてAPIの呼出しを選択します。

設定パッケージとしてCWR_UTILを選択し、プロシージャまたはファンクションとしてLOAD_FROM_SOURCEを選択します。サーバー側の条件ボタン押下時LOAD_FROM_SOURCEを選択し、このボタンが押された時に実行されるようにします。


パラメータのp_idp_source_langp_target_langには、(p_またはP3_を除いた名前が一致するため)デフォルトでページ・アイテムP3_IDP3_SOURCE_LANGP3_TARGET_LANGが設定されます。

パラメータp_collection_nameタイプAPIデフォルトになっているので(APIのデフォルトはTRANSLATEになっているため、実際はこのままでも動作します)、タイプアイテムに変更し、アイテムとして置換文字列として設定されているG_TRANSLATE_COLLECTIONを指定します。


同様の手順でプロセスStore As Resultを作成します。パラメータp_collection_nameも同じくアイテムとしてG_TRANSLATE_COLLECTIONを指定します。

プロセスStore As ResultはプロセスTranslate - Save Interactive Grid Dataより下に配置します。表CWR_XLIFF_FILESの列XLIFF_RESULTに翻訳済みのデータを保存する前に、対話グリッドの更新データがAPEXコレクションに保存されている必要があります。


プロセスLoad From Resultも同様に作成します。


最後にプロセスBatch Translateを作成します。


以上でプロセスの作成は完了です。


翻訳テスト



一括翻訳をテストするために以下のINSERT文を実行し、表CWR_MESSAGESにデータを用意します。

SQLワークショップSQLスクリプトなどを使用して実行します。

insert into cwr_messages(name, message_language, message_text) values('WR_BEEF_BOWL','en','Beef Bowl');
insert into cwr_messages(name, message_language, message_text) values('WR_BEEF_BOWL','ja','牛丼');
insert into cwr_messages(name, message_language, message_text) values('WR_BEEF_BOWL','ko','소고기 덮밥');
insert into cwr_messages(name, message_language, message_text) values('WR_BEEF_BOWL','zh-cn','牛肉碗');
insert into cwr_messages(name, message_language, message_text) values('WR_FRIED_SOURCE_NOODLES','en','Fried Source Noodles');
insert into cwr_messages(name, message_language, message_text) values('WR_FRIED_SOURCE_NOODLES','ja','炸醤麺');
insert into cwr_messages(name, message_language, message_text) values('WR_FRIED_SOURCE_NOODLES','ko','짜장면');
insert into cwr_messages(name, message_language, message_text) values('WR_FRIED_SOURCE_NOODLES','zh-cn','炸醬麵');
insert into cwr_messages(name, message_language, message_text) values('WR_GINSENG_CHICKEN_SOUP','en','Ginseng Chicken Soup');
insert into cwr_messages(name, message_language, message_text) values('WR_GINSENG_CHICKEN_SOUP','ja','参鶏湯');
insert into cwr_messages(name, message_language, message_text) values('WR_GINSENG_CHICKEN_SOUP','ko','삼계탕');
insert into cwr_messages(name, message_language, message_text) values('WR_GINSENG_CHICKEN_SOUP','zh-cn','蔘鷄湯');
insert into cwr_messages(name, message_language, message_text) values('WR_HAMBURGER','en','Hamburger');
insert into cwr_messages(name, message_language, message_text) values('WR_HAMBURGER','ja','ハンバーガー');
insert into cwr_messages(name, message_language, message_text) values('WR_HAMBURGER','ko','햄버거');
insert into cwr_messages(name, message_language, message_text) values('WR_HAMBURGER','zh-cn','汉堡包');
insert into cwr_messages(name, message_language, message_text) values('WR_TITLE','en','World Diner');
insert into cwr_messages(name, message_language, message_text) values('WR_TITLE','ja','世界食堂');
insert into cwr_messages(name, message_language, message_text) values('WR_TITLE','ko','식당세계');
insert into cwr_messages(name, message_language, message_text) values('WR_TITLE','zh-cn','世界餐厅');
commit;

実行が正常に完了していると、Messagesのページより挿入された翻訳を確認することができます。


以上でテストの準備ができました。

最初の記事に添付されているworld-diner.xlfををダウンロードし、XLIFF Filesの画面でアップロードします。


XLIFFファイルをアップロードしたら、Nameをクリックして翻訳ページ(Translate)を開きます。


ボタンLoad From Sourceをクリックして、表CWR_XLIFF_FILESの列XLIFF_SOURCEとして保存されているXLIFFファイルの内容をAPEXコレクションにロードします。


Target Textをひとつひとつ手入力で更新することもでできますが、ここではボタンBatch Translateをクリックして、表CWR_MESSAGESの内容を使って一括翻訳します。


翻訳結果がAPEXコレクションに反映されます。ボタンStore As Resultをクリックし、表CWR_XLIFF_FILESの列XLIFF_RESULTにXLIFFファイルとして保存します。


XLIFF Filesの画面に戻るとXLIFF Resultとして、ファイルのダウンロードができるようになっています。これをクリックして翻訳済みのXLIFFファイルをダウンロードして内容を確認します。


ダウンロードされたファイルの名前はworld-diner_ja_en.xlfになります。ファイルを開いて内容が翻訳されていることを確認します。

以上で翻訳作業を行うページは完成です。

次の記事で、DeepLを使った翻訳を追加します。

続く