README.org 13.1 KB
Newer Older
Daniel Kochmański's avatar
Daniel Kochmański committed
1 2 3 4

* Introduction

  =TRANSLATE= is a thin abstraction layer over the ordinary strings
5 6 7
  which allows the creation of a seamless translation for your
  project. The code is written in a plain Common Lisp without any
  dependencies. A system definition is provided in =ASD= format,
Daniel Kochmański's avatar
Daniel Kochmański committed
8 9
  although =ASDF= isn't required to run this software.

10 11
  This library is licensed under =LLGPLv2=, which means that it may be
  used in any project for any purpose although any significant
Daniel Kochmański's avatar
Daniel Kochmański committed
12 13 14 15 16 17 18
  modifications to the library should be published. It doesn't impose
  it's license on software depending on it.

  To illustrate library usage some imaginary functions are used:
  =MAKE-BUTTON= and =MAKE-LABEL=. The following sample code convention
  is used:

19
  - lines preceded with ==> indicate *reader* input
Daniel Kochmański's avatar
Daniel Kochmański committed
20 21 22 23 24 25
  - all other lines represent *printer* output

* Description

** Basic usage

26 27 28 29
   The library pollutes =*READTABLE*= with dispatch macro character
   =#t=, where "t" is abbreviation of "translate". Instead of writing
   ordinary constant strings in his/her application, the user instead
   precedes them with =#t=:
Daniel Kochmański's avatar
Daniel Kochmański committed
30

31
   #+BEGIN_SRC lisp
32 33
     ==> #t"dum dum dum, piję rum"
     "la la la, I drink in the spa"
Daniel Kochmański's avatar
Daniel Kochmański committed
34 35 36
   #+END_SRC

   If =TRANSLATE:*LANGUAGE*= is bound to =NIL= and
37 38
   =TRANSLATE:*RESOLUTION-TIME*= to =:LOAD-TIME=, then =#t="hello"
   will resolve to the string "hello" and the translation will have no
Daniel Kochmański's avatar
Daniel Kochmański committed
39
   effect. It is convenient, because programmer can put =#t="strings"
40
   for the future translation with no functional consequences at
Daniel Kochmański's avatar
Daniel Kochmański committed
41 42 43
   run-time while keeping application innards visually separated from
   the messages meant to be presented to user.

44
   #+BEGIN_SRC lisp
Daniel Kochmański's avatar
Daniel Kochmański committed
45
     ==> (setf translate:*language* nil)
46 47
     NIL

Daniel Kochmański's avatar
Daniel Kochmański committed
48 49 50 51
     ==> (make-button :label #t"hello")
     #<button "hello">
   #+END_SRC

Daniel Kochmański's avatar
Daniel Kochmański committed
52
   Binding =TRANSLATE:*LANGUAGE*= to a non-NIL value enables string
Daniel Kochmański's avatar
Daniel Kochmański committed
53 54 55
   translation. If no translation exists it will be temporarily
   translated to the same value enclosed in a curly brackets:

56
   #+BEGIN_SRC lisp
Daniel Kochmański's avatar
Daniel Kochmański committed
57 58
     ==> (translate:define-language :pl)
     (#<hash-table 00000000054323c0> NIL #<compiled-function 0000000005fe08c0>)
59

Daniel Kochmański's avatar
Daniel Kochmański committed
60 61
     ==> (setf translate:*language* :pl)
     :PL
62

Daniel Kochmański's avatar
Daniel Kochmański committed
63 64 65
     ==> (make-label :text #t"Greetings, programs")
     ;;; Warning: phrase "Greetings, programs" isn't defined for language :PL.
     #<label "{Greetings, programs}">
66

Daniel Kochmański's avatar
Daniel Kochmański committed
67 68 69 70 71
     ==> (make-label :text #t"Leave the grid")
     ;;; Warning: phrase "Leave the grid" isn't defined for language :PL.
     #<label "{Leave the grid}">
   #+END_SRC

72 73 74 75 76 77
   Using a string that has not been translated will cause a warning at
   resolution time and string will be added to the special list of
   phrases not yet translated (which may be evaluated for future
   processing, like adding the missing translations). If a language
   put in the =TRANSLATE:*LANGUAGE*= isn't defined yet, =CERROR= will
   be signaled with a restart allowing language creation.
Daniel Kochmański's avatar
Daniel Kochmański committed
78

Daniel Kochmański's avatar
Daniel Kochmański committed
79 80
   To distinguish dictionaries, the predicate =EQL= is used and
   phrases are distinguished using the predicate =EQUAL=, so the most
81 82
   convenient is using symbols as the language designators. It also
   implies, that phrases meant for translation are case sensitive.
Daniel Kochmański's avatar
Daniel Kochmański committed
83

84
** Adding translation
Daniel Kochmański's avatar
Daniel Kochmański committed
85

86 87 88 89
   To add translations, two interfaces are provided. The
   =ADD-SINGLE-TRANSLATION= function takes three arguments, where the
   first is the language for which we provide translation, the second
   is a translated phrase and the third is an actual
Daniel Kochmański's avatar
Daniel Kochmański committed
90
   translation. Phrase type must be a string, but the translation
91 92
   might be any kind of object (although it is advised to use strings,
   the user is free to shoot himself in the foot by abusing the
Daniel Kochmański's avatar
Daniel Kochmański committed
93 94 95 96 97 98 99
   translation mechanism).

   #+BEGIN_SRC lisp
     ==> (translate:add-single-translation 'pl "hello" "Witaj!")
     ;;; Warning: Implicitly creating language PL.
     [PL] hello -> Witaj!
     "Witaj!"
100

Daniel Kochmański's avatar
Daniel Kochmański committed
101 102 103 104
     ==> (translate:add-single-translation 'en "hello" "Welcome!")
     ;;; Warning: Implicitly creating language EN.
     [EN] hello -> Welcome!
     "Welcome!"
105

Daniel Kochmański's avatar
Daniel Kochmański committed
106 107 108 109 110 111 112 113
     ==> (translate:add-single-translation
          'pl "bang"
          (let ((i 1))
            (lambda ()
              (format nil "bah ~A" (incf i)))))
     bang -> #<bytecompiled-closure #<bytecompiled-function 00000000067d9f50>> (PL)
     #<bytecompiled-closure #<bytecompiled-function 00000000067d9f50>>
   #+END_SRC
114

Daniel Kochmański's avatar
Daniel Kochmański committed
115
   =ADD-TRANSLATIONS= is a simple wrapper around
116 117 118 119
   =ADD-SINGLE-TRANSLATION= that allows the translation of multiple
   phrases in one form. The first argument is once again the
   translation language, while all further arguments are alternately
   phrases and translations.
Daniel Kochmański's avatar
Daniel Kochmański committed
120 121 122 123 124 125 126 127 128 129 130 131 132 133

   #+BEGIN_SRC lisp
     ==> (translate:add-translations 'pl
            "header-about"   "O firmie"
            "header-offer"   "Oferta"
            "header-blog"    "Blog"
            "header-pricing" "Cennik"
            "header-contact" "Kontakt")
     [PL] header-about -> O firmie
     [PL] header-offer -> Oferta
     [PL] header-blog -> Blog
     [PL] header-pricing -> Cennik
     [PL] header-contact -> Kontakt
     T
134

Daniel Kochmański's avatar
Daniel Kochmański committed
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
     ==> (translate:add-translations 'en
            "header-about"   "About"
            "header-offer"   "Offer"
            "header-blog"    "Blog"
            "header-pricing" "Prices"
            "header-contact" "Contact")
     [EN] header-about -> About
     [EN] header-offer -> Offer
     [EN] header-blog -> Blog
     [EN] header-pricing -> Prices
     [EN] header-contact -> Contact
     T
   #+END_SRC

   Generally it is advised to use symbolic and meaningful names for
   phrases to be translated, not the final phrases written in English.
   Providing "translation-tags" of concise form is easier to
152
   comprehend for people who will translate the application.
Daniel Kochmański's avatar
Daniel Kochmański committed
153 154 155 156 157

** Interactive fixing of the missing phrases

   Loading the code is enough to catch all not yet translated phrases
   for the active language (bound to =TRANSLATE:*LANGUAGE*=) if
158
   resolution is performed at load time. Otherwise, an untranslated
Daniel Kochmański's avatar
Daniel Kochmański committed
159
   phrase is saved after it's first evaluation. To list saved phrases
160 161
   without translations, the function =MISSING-TRANSLATIONS= is
   available. It returns a list of the form ={((LANG (PHRASES*))*)}=.
Daniel Kochmański's avatar
Daniel Kochmański committed
162

163
   #+BEGIN_SRC lisp
164
     ==> (translate:missing-translations)
Daniel Kochmański's avatar
Daniel Kochmański committed
165 166 167 168 169 170
     ((PL ("phrase-1" "phrase-2" "phrase-3"))
      (BG ("phrase-1" "phrase-3")))
   #+END_SRC

   Such output means, that language =PL= doesn't have translations for
   "phrase-1", "phrase-2" and "phrase-3", while =BG= doesn't have
171
   translations for "phrase-1" and "phrase-3". Languages which have
Daniel Kochmański's avatar
Daniel Kochmański committed
172 173 174 175
   all translations are filtered and they don't appear in the result.

** Different times of the resolution

176 177 178
   The library may work in two different modes which dictate the time
   when the actual translation is performed. Strings may be translated
   at load-time, or at run-time.
Daniel Kochmański's avatar
Daniel Kochmański committed
179

180 181 182 183 184
   The first approach is faster, because it doesn't require any
   processing at run-time, while the second is much more flexible
   allowing the change of dictionaries and translations when the
   program is running or depending on lexically scoped value of the
   parameter =TRANSLATE:*LANGUAGE*=.
Daniel Kochmański's avatar
Daniel Kochmański committed
185

186
   It is important to remember that, when translations are done at
187
   run-time, strings preceded by =#t= are transformed to the function
188 189
   calls and they may work not as expected in the context where
   enclosing macro prevents their evaluation.
Daniel Kochmański's avatar
Daniel Kochmański committed
190

191
   #+BEGIN_SRC lisp
192
     ==> (setf translate:*resolution-time* :run-time)
Daniel Kochmański's avatar
Daniel Kochmański committed
193
     :RUN-TIME
194

Daniel Kochmański's avatar
Daniel Kochmański committed
195 196
     ==> (setf translate:*language* :en)
     :EN
197

Daniel Kochmański's avatar
Daniel Kochmański committed
198 199
     ==> (translate:add-single-translation :en "hello" "Hello")
     [EN] hello -> Hello
200

Daniel Kochmański's avatar
Daniel Kochmański committed
201 202
     ==> (translate:add-single-translation :pl "hello" "Cześć")
     [PL] hello -> Cześć
203

Daniel Kochmański's avatar
Daniel Kochmański committed
204 205 206
     ==> (let ((translate:*language* :en))
           #t"hello")
     "Hello"
207

Daniel Kochmański's avatar
Daniel Kochmański committed
208 209 210
     ==> (let ((translate:*language* :pl))
           #t"hello")
     "Cześć"
211

Daniel Kochmański's avatar
Daniel Kochmański committed
212 213 214 215
     ==> (quote #t"hello")
     (TRANSLATE:TRANSLATE "hello")
   #+END_SRC

216 217 218 219 220
   When translation is performed at load-time, the translation has to
   be present before the actual phrase is used (e.g. in a lambda
   expression), because phrases are resolved to their translations
   immediately. That also means that changing =TRANSLATE:*LANGUAGE*=
   in the future won't affect translations resolved earlier.
Daniel Kochmański's avatar
Daniel Kochmański committed
221

222
   #+BEGIN_SRC lisp
223
     ==> (setf translate:*resolution-time* :load-time)
224
     :LOAD-TIME
225

Daniel Kochmański's avatar
Daniel Kochmański committed
226 227
     ==> (setf translate:*language* :en)
     :EN
228

Daniel Kochmański's avatar
Daniel Kochmański committed
229 230 231
     ==> (defparameter *my-function-1*
           (lambda () #t"hello"))
     ;;; Warning: phrase "hello" isn't defined for language EN.
232 233
     *MY-FUNCTION-1*

234
     ==> (translate:add-single-translation :en "hello" "Hello")
Daniel Kochmański's avatar
Daniel Kochmański committed
235
     hello -> Hello (EN)
236

237
     ==> (translate:add-single-translation :pl "hello" "Cześć")
Daniel Kochmański's avatar
Daniel Kochmański committed
238
     hello -> Cześć (PL)
239

240
     ==> (let ((translate:*language* :en))
Daniel Kochmański's avatar
Daniel Kochmański committed
241 242
           #t"hello")
     "Hello"
243

244
     ==> (let ((translate:*language* :pl))              ; lexical scope is ignored
Daniel Kochmański's avatar
Daniel Kochmański committed
245 246
           #t"hello")
     "Hello"
247

Daniel Kochmański's avatar
Daniel Kochmański committed
248 249
     ==> (defparameter *my-function-2*
           (lambda () #t"hello"))
250
     *MY-FUNCTION-2*
251

Daniel Kochmański's avatar
Daniel Kochmański committed
252 253
     ==> (funcall *my-function-1*) ; phrase wasn't translated when function was created
     "{hello}"
254

Daniel Kochmański's avatar
Daniel Kochmański committed
255 256
     ==> (funcall *my-function-2*)
     "Hello"
257

Daniel Kochmański's avatar
Daniel Kochmański committed
258 259 260 261
     ==> (quote #t"hello")
     "Hello"
   #+END_SRC

262 263 264 265 266
   Translation at run-time is better when the programmer wants to add
   translations ad-hoc or wants to switch languages when the
   application is running. Load-time translation is more suitable for
   static translations for deployed applications or where macros
   prevent necessary evaluation of the expressions. Also when the
Daniel Kochmański's avatar
Daniel Kochmański committed
267
   programmer wants to add translations in future (if language is
268 269
   bound to nil and resolution is performed at load-time the
   expression =#t="hello world" means the same as the "hello world").
Daniel Kochmański's avatar
Daniel Kochmański committed
270 271 272

* Reference
** Parameters
273 274
*** =*LANGUAGE*=
    #+BEGIN_SRC text
275
      This variable holds the current language designator (the predicate
276 277
      used for comparison is EQL). If bound to NIL, translation works the
      same way as the IDENTITY function.
278
    #+END_SRC
279

280 281
*** =*RESOLUTION-TIME*=
    #+BEGIN_SRC text
282
      Applicable values are :LOAD-TIME and :RUN-TIME (the latter is the
283 284
      default). The variable controls time of actual resolution.

285 286 287 288
      If it's the :LOAD-TIME, then resolution is performed when the reader
      encounters the #t dispatch macro character, while setting the variable
      to :RUN-TIME translates #t"string" to the form (TRANSLATE "string")
      and resolution takes place at the time of the form evaluation.
289
    #+END_SRC
Daniel Kochmański's avatar
Daniel Kochmański committed
290 291 292 293 294 295 296 297 298

** Functions
*** DEFINE-LANGUAGE
    #+BEGIN_SRC text
      DEFINE-LANGUAGE - external symbol in TRANSLATE package
      -----------------------------------------------------------------------------
      DEFINE-LANGUAGE (NAME &REST TRANSLATIONS)                          [Function]
      Define language NAME with provided TRANSLATIONS

299 300 301
      If LANGUAGE exists, a continuable error is signalled, which allows either
      dropping the operation or superseding the language which is already defined.
      TRANSLATIONS are alternating phrases and their corresponding objects.
Daniel Kochmański's avatar
Daniel Kochmański committed
302 303 304 305 306 307 308 309 310 311
      -----------------------------------------------------------------------------
    #+END_SRC

*** ADD-SINGLE-TRANSLATION
    #+BEGIN_SRC text
      ADD-SINGLE-TRANSLATION - external symbol in TRANSLATE package
      -----------------------------------------------------------------------------
      ADD-SINGLE-TRANSLATION (LANGUAGE PHRASE TRANSLATION)               [Function]
      Add TRANSLATION of PHRASE for given LANGUAGE

312 313
      If LANGUAGE doesn't exist, it is implicitly created and a warning is
      emmited.
Daniel Kochmański's avatar
Daniel Kochmański committed
314 315 316 317 318 319 320 321
      -----------------------------------------------------------------------------
    #+END_SRC

*** ADD-TRANSLATIONS
    #+BEGIN_SRC text
      ADD-TRANSLATIONS - external symbol in TRANSLATE package
      -----------------------------------------------------------------------------
      ADD-TRANSLATIONS (LANGUAGE &REST TRANSLATIONS)                     [Function]
322
      Add any number of TRANSLATIONS for the given LANGUAGE
Daniel Kochmański's avatar
Daniel Kochmański committed
323 324 325 326 327 328 329 330
      -----------------------------------------------------------------------------
    #+END_SRC

*** TRANSLATE
    #+BEGIN_SRC text
      TRANSLATE - external symbol in TRANSLATE package
      -----------------------------------------------------------------------------
      TRANSLATE (PHRASE &OPTIONAL (LANGUAGE *LANGUAGE*))                 [Function]
331
      Find the translation of PHRASE in the store associated with LANGUAGE
Daniel Kochmański's avatar
Daniel Kochmański committed
332 333

      If LANGUAGE is NIL, then this is the same as the IDENTITY function. If
334 335 336 337
      the provided LANGUAGE isn't defined, the store is explicitly
      created. If no PHRASE is defined for a given language, it is stored
      for later translation and replaced by PHRASE surrunded by curly
      brackets.
Daniel Kochmański's avatar
Daniel Kochmański committed
338 339 340 341 342 343 344 345
      -----------------------------------------------------------------------------
    #+END_SRC

*** MISSING-TRANSLATIONS
    #+BEGIN_SRC text
      MISSING-TRANSLATIONS - external symbol in TRANSLATE package
      -----------------------------------------------------------------------------
      MISSING-TRANSLATIONS                                               [Function]
346 347
      Creates a list of phrases which aren't translated for the defined
      languages. Returns a list of form: ({(LANG ({PHRASE}*))}*)
Daniel Kochmański's avatar
Daniel Kochmański committed
348 349
      -----------------------------------------------------------------------------
    #+END_SRC