Translate your Dyalog GUIs without touching MAKE_-code
Vector 17:3, page 131
Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 131
I have done it sometimes before in some special cases: provided a menu-driven possibility to change language on the fly from English to Finnish and vice versa. This far, applications have been small autonomous tools with the kind of interface that you normally start building from scratch.
As a Finn, if you even dream of your applications ending up as products and being of interest beyond domestic circles, you must either write them in English from the start, or prepare to support the language of the interface as a user configurable parameter.
How easy it’s going to be to support different languages depends very much on the structure of the application. People like Adrian who went off and developed their own GUI builder tool (Causeway) having text labels, button captions and so on in arrays separated from the code, may find it quite flexible just to provide some kind of GUI to maintain relevant text strings. Whichever the maintaining method is, it has to be something that can be given to someone else (most likely not a programmer) familiar with the language to support.
I started using Dyalog APL in Winter ’92, taking part in the beta test program and since then I have lived on quite happily still carrying along many programming habits that date back those early days. Even today, aside from the general dialogue forms, all the other forms in my applications are created using MAKE_{MyApp}-like calls, keeping them in a separate layer in the structure of the application. This convention is well supported by WDesign, which is a fine tool, although I have used it quite occasionally. Even when I do use it, I always make final refinements by hand.
What to do with MAKE_ when you face the need to support different languages?
In this approach all the text strings which appear on the form (except for the items driven directly from data) are hard-encoded in the APL functions. Am I prepared to change programming habits, adopt a different way of defining application structure to get some better language support?
What is the source of your productivity? I know a lot of APLers, Finns and foreigners, many older, a few younger - most having some kind of inclination to ultra-individualism, very anxious to create their own styles, find their own ways to face logistic challenges. In a world as rich as APL spiced up with Dyalog fancy additions to the language like namespaces, there are endless possibilities. Do these discoveries produce a big increase in your productivity? After all, I think it is not so relevant how you do your things - what counts is how consistent you are in your way of doing them.
Well, what to do then with MAKE_ functions that are part of your adopted structure?
Version 9 of the Dyalog APL interpreter made it possible to manipulate GUI properties directly by simple assignment. This felt good and natural, and despite the fact that it was mainly said to be based on the need to make the syntax closer to the one used in other programming languages, such as Visual Basic, known for their perfect support of third party OLE components, and to prepare for the future .NET development, you were almost tempted to indulge in inadmissible thoughts like "shouldn’t they have implemented it this way from the very beginning? This is APL, after all."
The new supported syntax makes the old
be reborn as
certainly more straightforward and less convoluted thanks to the reduction in the number of quotes needed. Would this be of help in our translatability problem? I started my experiments by creating a little toolset that can be run from the root, or the session namespace or any other namespace. I prefer to run it from namespace Lng.
The simple idea is to read all the language-dependent text to appear in a form directly from an active instance and store it in an ASCII file. To test my tool, I took a form written years ago and certainly not with the purpose of trying out such a tool.
To allow the new syntactic constructs, I must make sure that ⎕wx←1 before I load the form.
MAKE_Tfrm¶

The principle on which the tool is based is that the text file with all the strings can be given to your expert in the target language. The form instance is still created with the MAKE_ function and all the GUI strings are substituted at runtime with the ones read from the ASCII file after the form is created. With the speed of execution of today’s workstations, nobody notices any speed delays.
The default language to be used is a system parameter and so are all the supported languages. They can be read from an Appl.ini file or from the Windows registry at application start-up.
The language to be used is stored somewhere safe as a global variable or DATA property: my choice is to use
The naming conventions then are quite consistent and clear. Let’s say the application root directory is C:\Tabla. Then the place for the text source files would generally always be .\Lng. If you want it to run independently of the working directory, you must use the full path. I also associate the file extensions to the language identifier. The function that associates the file name with a particular combination of form and language is Form_texts (see functions).
So, to update all the captions, labels and other texts after creating the form, just execute:
Yes, I am aware that there is a world of things to consider beside MAKE_ when you seriously aim for a language-independent solution. Still, for me this looks like a promising start and saves me from the need to re-invent everything at the same time. At least this is of great help in solving the problem associated with the strings hard-encoded in the source code, which I consider the trickiest aspect of the general problem. Anyway, I think the best benefit comes from the fact that you don’t have to touch existing MAKE_-code at all. To get the effects, just add a line after each MAKE_ call, and magically the form appears fully translated.
Captions and contents for general modal dialog forms like OK-, WARN- and similar messages which indeed are a relevant part of any application, are most likely to be stored into some tables organised by language version with identifying hooks and read from external files, supposed to be easily editable.. Help files are in general already separate from application code and can be translated using the tools they were created with in the first place.
I include the code I use. Make sure to check the comments in the code for the rules concerning running the functions under different namespace conditions. I expect it to be flexible enough to handle most obvious situations. If you are interested in trying this yourself and want to avoid to have to retype all the code, just contact me and ask for a working workspace.
Sample Functions¶
∇ frm LanguageFile_create file;exp
[1] ⍝ frm = Dyalog gui form
[2] ⍝ file = name for ascii file to store text properties
[3] ⍝ 'Caption' 'Text' 'Hint' 'Tip'
[4] ⍝ 'RowTitles' 'ColTitles' 'Items'
[5] ⍝ to be loaded by LanguageFile_load file
[6] exp←GetLanguageExpressions frm
[7] exp write_file file
[8] ⍝ Copyright KJK-tieto Oy/Kimmo Kekäläinen
∇
∇ z←GetLanguageExpressions obj;i;obs;ob;props;search;j;tob;prop;val;add
[1] obs←ObjectChildren obj
[2] obs←obs[;1]
[3] search←'Caption' 'Text' 'Hint' 'Tip' ⍝ these props
[4] search,←'RowTitles' 'ColTitles' 'Items' ⍝ of interest
[5] ⍝ vector vectors and text matrices (Tip)
[6] ⍝ -> ;-delimited simple text vectors
[7] z←0⍴⊂'' ⍝ empty result
[8] :For i :In ⍳⍴obs
[9] ob←i⊃obs ⍝ one object at time
[10] props←ob ⎕WG'Proplist' ⍝ get props
[11] props←(props∊search)/props ⍝ those of interest
[12] :If 0=⍴props ⍝ if none
[13] :Continue ⍝ go next
[14] :EndIf
[15] :For j :In ⍳⍴props
[16] prop←j⊃props ⍝ one property at time
[17] val←ob ⎕WG prop ⍝ get prop value
[18] tob←ob,'.',prop ⍝ join object.property
[19] :If 2=⍴⍴val ⍝ if value matrix
[20] val←¯1↓DLTB,val,';' ⍝ -> simple ;-segm vec
[21] :EndIf
[22] :If 2=|≡val ⍝ if nested
[23] val←¯1↓∆ENLIST val,¨';' ⍝ -> simple;-segm vec
[24] :EndIf
[25] add←tob,'=',val ⍝ obj.prop=text value
[26] z←z,⊂add ⍝ add it to result
[27] :EndFor
[28] :EndFor
[29] ⍝ Copyright KJK-tieto Oy/Kimmo Kekäläinen
∇
∇ z←ObjectChildren ob;⎕ML;depth
[1] ⍝ returns z as two-column nested
[2] ⍝ matrix[;1]=names of children
[3] ⍝ matrix[;2]=depth from parent ob=first row
[4] ⍝ ob is parent Dyalog GUI Object or ⎕se or # or .
[5] z←,⊂'' ⋄ ob←,ob ⍝ pessimist default
[6] →(~(⎕NC ob)∊¯1 9)⍴0 ⍝ must exist to hope for kids
[7] z←¯1↓SS2VV' ',Obj_Child ob ⍝ this actually does
[8] →(~(,'#')≡''⎕NS'')⍴⍙depth ⍝ if not run in root OK
[9] →(~'#.'≡2↑ob)⍴⍙depth
[10] ⍝ border line case seems to be if
[11] ⍝ this fn is run in root space #
[12] ⍝ and ob name is given with full #.-path
[13] ⍝ #. will not be set to children
[14] ⍝ so, for sake of consistency
[15] →(∧/(⊂ob)≡¨((⍴ob)↑¨z))⍴⍙depth ⍝ if start alike OK
[16] (1⊃z)←2↓1⊃z ⍝ then first has, next don't
[17] z←(⊂'#.'),¨z ⍝ -> change alike
[18] ⍙depth:
[19] depth←+/'.'=↑z
[20] :If ob≡,'.'
[21] :OrIf ob≡,'#'
[22] (1↓depth)+←1
[23] depth[1]←0
[24] :EndIf
[25] z←z,[1.1]depth
[26] ⍝ Copyright KJK-tieto Oy/Kimmo Kekäläinen
∇
∇ z←Obj_Child ob;obs;⎕ML;t;root
[1] z←ob,' '
[2] obs←⎕WN ob
[3] :If 0<⍴obs
[4] z←z,Obj_Child¨obs
[5] :EndIf
[6] z←∆ENLIST z
∇
∇ {parent}LanguageFile_load file;mat;exp;i;t;ob;txt;prop;tob
[1] ⍝ file = ascii-file created by fn LanguageFile_create
[2] ⍝ running level is guided by {parent} option. If not given
[3] ⍝ object name is taken from file and assumes names are
[4] ⍝ with full ns path = starting with #. or ⎕se.
[5] ⍝
[6] ⍝ If parent is given, it replaces that in file
[7] ⍝ so #. or ⎕se. -> parent.
[8] ⍝ if parent is given as empty ''
[9] ⍝ it defaults ##. which means that object
[10] ⍝ it supposed to be found one level above
[11] ⍝ The limitation with this is that if you run
[12] ⍝ Lng-utilities under ⎕se this does't work.
[13] ⍝ Safest way and most common case to set parent is
[14] ⍝ parent ←'' ⎕ns '' which means calling level as parent
[15] ⍝ so '' Lng.LanguaFile_load 'c:\tmp\CalcBarFrm.eng'
[16] ⍝ ('' ⎕ns '') Lng.LanguaFile_load 'c:\tmp\CalcBarFrm.eng'
[17] ⍝ should work similarly
[18] mat←read_file file ⋄ →(0=1↑⍴mat)⍴0
[19] mat←mat TBF'←'
[20] mat←(~mat∧.=' ')⌿mat ⋄ →(0=1↑⍴mat)⍴0
[21] :For i :In ⍳1↑⍴mat
[22] exp←mat[i;] ⋄ t←exp⍳'='
[23] ob←(t-1)↑exp ⋄ txt←DTB t↓exp
[24] :If 0≠⎕NC'parent'
[25] parent←,parent ⍝ make sure vector
[26] :If '#.'≡2↑ob
[27] ob←2↓ob ⍝ remove root-path
[28] :EndIf
[29] :If '⎕'=1⊃ob
[30] ob←3↓ob ⍝ remove ⎕se-path
[31] :EndIf
[32] :If 0=⍴parent
[33] parent←'##' ⍝ empty parent means
[34] :EndIf ⍝ look one level above
[35] ob←parent,'.',ob ⍝ set parent
[36] :EndIf
[37] prop←(t←¯1↑WHERE ob='.')↓ob ⍝ get property
[38] tob←(-1+⍴prop)↓ob ⍝ obj without prop
[39]
[40] :If 9≠⎕NC tob ⍝ make sure object
[41] :Continue ⍝ really exists
[42] :EndIf ⍝ before settings
[43]
[44] :If ';'∊txt ⍝ if delimited (';'
[45] txt←SS2VV';',txt ⍝ - > vec vec
[46] :If prop≡'Tip' ⍝ unless Tip
[47] txt←↑txt ⍝ - > char mtx
[48] :EndIf
[49] :EndIf
[50] ⍎ob,'←txt' ⍝ set object property
[51] ⍝ then with Dyalog ver < 9 you could run it
[52] ⍝ with expression below, and when stripping of
[53] ⍝ ns-spagetti, even down to 6.x
[54] ⍝ ((-1+⍴prop)↓ob)⎕WS prop txt
[55] :EndFor
[56] ⍝ Copyright KJK-tieto Oy/Kimmo Kekäläinen 2000 -
∇
∇ z←Form_texts frm
[1] :If '#.'≡2↑frm ⋄ frm←2↓frm ⋄ :EndIf
[2] z←'.\Lng\',frm,'.',lang_test
∇
∇ z←lang_test
[1] ⍝ fin / eng
[2] ⍝ supported by Gui-utils
[3] :If 0=⎕NC'⎕se.∆_lang'
[4] ⎕SE.∆_lang←'fin'
[5] :EndIf
[6] z←⎕SE.∆_lang
∇
Utility code¶
∇ vv write_file file;tn;⎕ML;⎕TRAP
[1] ⍝ writes vector vector to ascii file
[2] ⍝ killing old if exists
[3] ⎕ML←0
[4] ⎕TRAP←0 'E' '→⍙kill'
[5] ⍙create:
[6] ⍝ if create fails go and kill
[7] file ⎕NCREATE tn←¯1+⌊/0,⎕NNUMS
[8] ⎕ML←3 ⋄ vv←∊vv,¨⊂⎕TC[2 3]
[9] vv ⎕NAPPEND tn ⋄ ⎕NUNTIE tn ⋄ →0
[10]
[11] ⍙kill:file ⎕NTIE tn←¯1+⌊/0,⎕NNUMS
[12] file ⎕NERASE tn ⋄ →⍙create
∇
∇ z←read_file file;tn;⎕ML;⎕TRAP
[1] ⍝ returns ascii-file as char mtx
[2] ⎕TRAP←0 'E' '→⍙err'
[3] ⎕ML←0 ⋄ file ⎕NTIE tn←¯1+⌊/0,⎕NNUMS
[4] z←⎕NREAD tn,82,⎕NSIZE tn ⋄ ⎕NUNTIE tn
[5] z←⎕TC[2],z~⎕TC[3]
[6] z←↑¯1↓1↓¨(⎕TC[2]=z)⊂z ⋄ →0
[7] ⍙err:z←0 0⍴''
∇
∇ Z←DTB S
[1] ⍝ deletes trailing blancs from
[2] ⍝ text vector or matrix S
[3] →(1=⍴⍴S)⍴⍙V
[4] Z←(~⌽∧\⌽∧⌿S=' ')/S ⋄ →0
[5] ⍙V:Z←(⌽~∧\⌽S=' ')/S
∇
∇ Z←M TBF C;R;B
[1] ⍝∇Z←M ToBlancsFrom C -- BY KJK
[2] ⍝ blanc rows after char/s C
[3] R←⍴M ⋄ B←,∨\M∊C ⋄ M←,M ⋄ M[B/⍳⍴M]←' ' ⋄ Z←R⍴M
∇
Kimmo Kekäläinen
http://www.kjk-tieto.com