Here is a solution to translate user-generated content in M3, and M3 content, in 52 languages.
For that, I will use the Google Translate API and a Personalized Script for Lawson Smart Office.
Business advantage
This solution is interesting to translate content that is generated by users, such as:
- Bill of Materials
- Work Orders
- Service Orders
- Customer Order Notes
- etc.
Such content is entered in the user’s language and by design is not translated by Lawson Smart Office.
Also, this solution is interesting to translate M3 itself beyond the number of languages that Lawson makes available.
Lawson Smart Office
Lawson Smart Office supports 18 languages: Czech, Danish, German, Greek, English, Spanish, Finnish, French, Hungarian, Italian, Japanese, Dutch, Norwegian, Polish, Portuguese, Russian, Swedish, and Chinese:

It’s a high number of languages given that text is manually translated by professional translators which are probably paid by the word.
The quality is near perfect.
But by design, the user-generated content is not translated.
Google Translate
Google Translate supports 52 languages: Afrikaans, Albanian, Arabic, Belarusian, Bulgarian, Catalan, Chinese Simplified, Chinese Traditional, Croatian, Czech, Danish, Dutch, English, Estonian, Filipino, Finnish, French, Galician, German, Greek, Hebrew, Hindi, Hungarian, Icelandic, Indonesian, Irish, Italian, Japanese, Korean, Latvian, Lithuanian, Macedonian, Malay, Maltese, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Slovenian, Spanish, Swahili, Swedish, Thai, Turkish, Ukrainian, Vietnamese, Welsh, and Yiddish.

It’s a very high number of languages because it uses machine learning and statistical analysis for automatic machine translation of millions of web pages and of official translations done by governments and by international organizations.
It is one of the best machine translations available, considered state of the art, and the quality is improving constantly. [1] [2] [3].
Google is even working on recognizing handwritten text, and text in images.
But even though the quality is good it’s not yet accurate.
It may not be accurate enough in a professional context to translate user-generated content in M3 with the Google Translate API.
But it still gives the user a general idea of the meaning of the text.
And as a pedagogical tool, it serves the purpose of illustrating how to write scripts for Smart Office, and how to integrate M3 to external systems.
Hello World!
To use the Google Translate API you need to register and obtain a key. It is a paid service that will translate one million characters of text for $20.
Once you obtain your key, you need to construct a URL with your API key, the text to translate, and the source and target languages.
Here is a sample URL that translates the text Hello World! from English (en) to French (fr):
https://www.googleapis.com/language/translate/v2?key=YOUR_API_KEY&q=Hello%20World!&source=en&target=fr
The result is a JSON object like this:
{
"data": {
"translations": [
{
"translatedText": "Bonjour tout le monde!"
}
]
}
}
First script
Then write a Personalized Script for Lawson Smart Office using the Script Tool.
The script will submit the HTTP GET Request to the Google Translate API over HTTPS and will parse the JSON response.
function translate(text: String, source, target) {
var url = 'https://www.googleapis.com/language/translate/v2?key=YOUR_API_KEY&source=' + source + '&target=' + target + '&q=' + HttpUtility.UrlEncode(text);
var request = HttpWebRequest(WebRequest.Create(url));
var response = HttpWebResponse(request.GetResponse());
var jsonText = (new StreamReader(response.GetResponseStream())).ReadToEnd();
var o = eval('(' + jsonText + ')', 'unsafe');
return o.data.translations[0].translatedText;
}
We can now use this function to translate any piece of user-generated content, for example the Customer Name in CRS610/E (WRCUNM):
var WRCUNM = ScriptUtil.FindChild(controller.RenderEngine.Content, 'WRCUNM');
WRCUNM.Text = translate(WRCUNM.Text, 'en', 'fr');
Also, we can translate several pieces of text at once by appending as many q parameters to the URL as pieces of text.
Beyond
With this technique, we can translate all the Controls of our Panel, including the user-generated content: Label, TextBox, Button, ListView, GridViewColumnHeader, ListRow, etc. That will cover Panels A, B, E, F, etc.
Also, we will need to submit the HTTP Request in a background thread to avoid blocking the user interface.
Complete Script
Here is the complete source code of my script that translates all the content of any M3 program, any panel.
import System;
import Mango.UI;
import MForms;
import System.Collections;
import System.ComponentModel;
import System.IO;
import System.Net;
import System.Web;
import System.Windows.Controls;
import System.Windows;
/*
Thibaud Lopez Schneider
Lawson Software
March 26, 2010
This Personalized Script for Lawson Smart Office translates an M3 panel using the Google Translate API.
The script adds a translation button for each target language, for example: de, es, fr, hi, iw, sv, zh-CN, etc.
When the user clicks on a button, the script translates every piece of text of the M3 panel:
the labels, the text boxes, the buttons, the list’s columns’ headers, and the list’s rows.
The script is useful where user generated content must be translated, for example in programs such as:
- Indented Bill of Material - PDS100
- Work Order - MOS100
- Service - MOS300
SCREENSHOTS:
http://lawsonapp.com/TranslatePanel/doc/
INSTALLATION:
- Get an API key for the Google Translate API at https://code.google.com/apis/console/?api=translate
- Set your API key in the variable apiKey in the source code here below.
- Drop the script in the MNE.war\jscript folder in WebSphere. For more information, refer to the Lawson Smart Office documentation.
- Attach the script to the M3 program (Smart Office > Tools > Personalize > Personalize Scripts). For more information, refer to the Lawson Smart Office documentation.
THIRD-PARTY LICENSE:
Google Translate API:
The Google Translate API supports 52 languages.
http://code.google.com/apis/language/translate/overview.html
PENDING:
- Make a simple version of this script for teaching purposes.
- Make a loop of HTTP Requests to counter the Google Translate API limit of 128 pieces of text maximum.
- Call the DestroyWorker to cleanup
- Translate the T panels
*/
package MForms.JScript {
class TranslatePanel {
/*
Change these settings to suit your needs.
*/
var apiKey = 'YOUR_API_KEY'; // the API key for the Google Translate API, https://code.google.com/apis/console/?api=translate
var sourceLanguage = 'en'; // the source language
var targetLanguages = ['de', 'es', 'fr', 'hi', 'iw', 'sv', 'zh-CN']; // the target languages
/*
Language reference [DO NOT CHANGE]
http://code.google.com/apis/language/translate/v2/using_rest.html#language-params
*/
var languages = {
'af': 'Afrikaans',
'sq': 'Albanian',
'ar': 'Arabic',
'be': 'Belarusian',
'bg': 'Bulgarian',
'ca': 'Catalan',
'zh-CN': 'Chinese Simplified',
'zh-TW': 'Chinese Traditional',
'hr': 'Croatian',
'cs': 'Czech',
'da': 'Danish',
'nl': 'Dutch',
'en': 'English',
'et': 'Estonian',
'tl': 'Filipino',
'fi': 'Finnish',
'fr': 'French',
'gl': 'Galician',
'de': 'German',
'el': 'Greek',
'ht': 'Haitian Creole',
'iw': 'Hebrew',
'hi': 'Hindi',
'hu': 'Hungarian',
'is': 'Icelandic',
'id': 'Indonesian',
'ga': 'Irish',
'it': 'Italian',
'ja': 'Japanese',
'lv': 'Latvian',
'lt': 'Lithuanian',
'mk': 'Macedonian',
'ms': 'Malay',
'mt': 'Maltese',
'no': 'Norwegian',
'fa': 'Persian',
'pl': 'Polish',
'pt': 'Portuguese',
'ro': 'Romanian',
'ru': 'Russian',
'sr': 'Serbian',
'sk': 'Slovak',
'sl': 'Slovenian',
'es': 'Spanish',
'sw': 'Swahili',
'sv': 'Swedish',
'th': 'Thai',
'tr': 'Turkish',
'uk': 'Ukrainian',
'vi': 'Vietnamese',
'cy': 'Welsh',
'yi': 'Yiddish'
};
var GOOGLE_MAX_TEXT_SEGMENTS = 128; // Google Translate API's limit is 128 q parameters, otherwise it returns the error "Too many text segments"
var controller: Object;
var content: Object;
var debug: Object;
var worker: BackgroundWorker;
/*
Main entry point.
*/
public function Init(element: Object, args: Object, controller : Object, debug : Object) {
try {
this.controller = controller;
this.content = controller.RenderEngine.Content;
this.debug = debug;
InitializeComponent();
InitializeBackgroundWorker();
} catch(ex: Exception) {
ConfirmDialog.ShowErrorDialogWithoutCancel('Init: ' + ex.GetType(), ex.Message + '\n' + ex.StackTrace);
}
}
/*
Adds the buttons at the top right of the panel.
*/
function InitializeComponent() {
try {
var panel: WrapPanel = new WrapPanel();
for (var i in this.targetLanguages) {
var targetLanguage = this.targetLanguages[i];
var button = new Button();
button.Content = targetLanguage;
button.Tag = targetLanguage;
button.ToolTip = 'Translate from ' + languages[sourceLanguage] + ' to ' + languages[targetLanguage] + ' using Google Translate.';
button.Width = 20;
panel.Children.Add(button);
button.add_Click(OnClick);
}
Grid.SetColumn(panel, 0);
Grid.SetRow(panel, 0);
Grid.SetColumnSpan(panel, 98);
Grid.SetRowSpan(panel, 23);
panel.HorizontalAlignment = HorizontalAlignment.Right;
this.content.Children.Add(panel);
} catch(ex: Exception) {
ConfirmDialog.ShowErrorDialogWithoutCancel('InitializeComponent: ' + ex.GetType(), ex.Message + '\n' + ex.StackTrace);
}
}
/*
Prepare a BackgroundWorker to not block the UI while making HTTP Requests.
*/
function InitializeBackgroundWorker() {
this.worker = new BackgroundWorker();
this.worker.add_DoWork(DoWork);
this.worker.add_RunWorkerCompleted(WorkerCompleted);
}
/*
Translate all the elements of the panel.
*/
function OnClick(sender: Object, e: RoutedEventArgs) {
try {
var values = new ArrayList();
for (var i = 0; i < this.content.Children.Count; i++) {
if (this.content.Children[i].GetType() == 'System.Windows.Controls.Label') {
// Label
values.Add(this.content.Children[i].Content);
} else if (this.content.Children[i].GetType() == 'System.Windows.Controls.TextBox') {
// TextBox
values.Add(this.content.Children[i].Text);
} else if (this.content.Children[i].GetType() == 'System.Windows.Controls.Button') {
// Button
values.Add(this.content.Children[i].Content);
} else if (this.content.Children[i].GetType() == 'System.Windows.Controls.ListView') {
// ListView
var listView = this.content.Children[i];
// ListView's headers
var headers = this.controller.RenderEngine.ListControl.Headers;
for (var header in headers) {
values.Add(header);
}
// ListView's cells
var rows = listView.Items;
for (var row = 0; row < listView.Items.Count; row++) { var columns = rows[row].Items; for (var column in columns) { values.Add(listView.Items[row].Item[column]); } } } } if (values.Count >= GOOGLE_MAX_TEXT_SEGMENTS) {
this.controller.RenderEngine.ShowMessage('Translated only ' + GOOGLE_MAX_TEXT_SEGMENTS + ' text segments because of the Google Translate API limit.');
}
// do the translation using the BackgroundWorker
this.worker.RunWorkerAsync({
'values': values,
'sourceLanguage': this.sourceLanguage,
'targetLanguage': sender.Tag
});
} catch(ex: Exception) {
ConfirmDialog.ShowErrorDialogWithoutCancel('OnClick: ' + ex.GetType(), ex.Message + '\n' + ex.StackTrace);
}
}
/*
Send the HTTP Request to the Google Translate API with the specified values to translate,
from the specified source language, to the specified target language.
*/
function DoWork(sender: Object, e: DoWorkEventArgs) {
try {
// prepare the HTTP Request
var url = 'https://www.googleapis.com/language/translate/v2?';
url += '&key=' + HttpUtility.UrlEncode(this.apiKey);
url += '&source=' + HttpUtility.UrlEncode(e.Argument.sourceLanguage);
url += '&target=' + HttpUtility.UrlEncode(e.Argument.targetLanguage);
for (var i = 0; i < e.Argument.values.Count && i < GOOGLE_MAX_TEXT_SEGMENTS; i++) {
url += '&q=' + HttpUtility.UrlEncode(e.Argument.values[i]);
}
// send the HTTP Request
var request: HttpWebRequest = HttpWebRequest(WebRequest.Create(url));
var response: HttpWebResponse = HttpWebResponse(request.GetResponse());
var stream: Stream = response.GetResponseStream();
var reader = new StreamReader(stream);
// return the resulting JSON
var jsonText = reader.ReadToEnd();
e.Result = eval('(' + jsonText + ')', 'unsafe');
} catch(ex: Exception) {
MessageBox.Show('DoWork: ' + ex.Message + '\n' + ex.StackTrace, ex.GetType());
}
}
/*
Process the translated texts.
*/
function WorkerCompleted(sender: Object, e: RunWorkerCompletedEventArgs) {
try {
if (e.Error == null) {
var o = e.Result;
if (o.error == null) {
var translations = o.data.translations;
var count = 0;
for (var i = 0; i < this.content.Children.Count && count < GOOGLE_MAX_TEXT_SEGMENTS; i++) {
if (this.content.Children[i].GetType() == 'System.Windows.Controls.Label') {
// Label
this.content.Children[i].Content = translations[count].translatedText;
count++;
} else if (this.content.Children[i].GetType() == 'System.Windows.Controls.TextBox') {
// TextBox
this.content.Children[i].Text = translations[count].translatedText;
count++;
} else if (this.content.Children[i].GetType() == 'System.Windows.Controls.Button') {
// Button
this.content.Children[i].Content = translations[count].translatedText;
count++;
} else if (this.content.Children[i].GetType() == 'System.Windows.Controls.ListView') {
// ListView
var listView = this.content.Children[i];
// ListView's headers
var gridView: GridView = controller.RenderEngine.ListControl.GridView;
var columnCollection: GridViewColumnCollection = gridView.Columns;
var gridViewColumn: GridViewColumn;
for (gridViewColumn in columnCollection) {
gridViewColumn.Header = translations[count].translatedText;
count++;
}
// ListView's cells
var rows = listView.Items;
for (var row = 0; row < listView.Items.Count && count < GOOGLE_MAX_TEXT_SEGMENTS; row++) { // replace the row var listRow = new Mango.UI.Services.Lists.ListRow('', rows[row].Items.length, false, false, false); var columns = rows[row].Items; var column: int; for (column in columns) { listRow.Add(translations[count].translatedText); count++; if (count >= GOOGLE_MAX_TEXT_SEGMENTS) { break; }
}
listView.Items[row] = listRow;
}
}
}
} else {
ConfirmDialog.ShowErrorDialogWithoutCancel('WorkerCompleted: ' + o.error.code, o.error.message);
}
} else {
MessageBox.Show(e.Error.Message, 'WorkerCompleted');
}
} catch (ex: Exception) {
ConfirmDialog.ShowErrorDialogWithoutCancel('WorkerCompleted: ' + ex.GetType(), ex.Message + '\n' + ex.StackTrace);
}
}
function DestroyWorker() {
this.worker.remove_DoWork(DoWork);
this.worker.remove_RunWorkerCompleted(WorkerCompleted);
this.worker = null;
}
}
}
Installation
Replace the constant YOUR_API_KEY of the source code with your own Google Translate API key.
The script has a limit GOOGLE_MAX_TEXT_SEGMENTS which was applicable when I wrote the script back in March 2010, but Google has since removed the limit so you can remove it from the script as well.
Then deploy the script on each program and each panel that you’d like to translate. The deployment can probably be automated with some custom XML and XSLT.
Result
Here is an animation of the M3 program Work Order – MOS100/B1 with buttons for seven languages. Click on the image to see the animation. Note how the user-generated content in the rightmost column of the list is also being translated.

Future Work
A future implementation should also translate menus, drop down lists, and text panels (T). I still haven’t been able to execute scripts in a T panel.
That’s it!