base.lisp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. ;; This software is Copyright (c) Leslie P. Polzer, 2011.
  2. ;; Leslie P. Polzer grants you the rights to distribute
  3. ;; and use this software as governed by the terms
  4. ;; of the Lisp Lesser GNU Public License
  5. ;; (http://opensource.franz.com/preamble.html),
  6. ;; known as the LLGPL
  7. (in-package :cl-i18n)
  8. (defparameter *translation-file-root* "."
  9. "The directory where translation files are stored.
  10. Defaults to current directory.")
  11. (defparameter *locale* nil)
  12. (defparameter *categories* "LC_MESSAGES")
  13. (defparameter *plural-form-function* #'n/=1-plural-form
  14. "This is the function used by the library to figure out the right plural form")
  15. (defparameter *translation-table* (make-hash-table :test 'equal)
  16. "The actual translation table used, it is an hashtable with the original (untranslated) string as key and an instance of the class translation as value")
  17. (defun translation-table ()
  18. *translation-table*)
  19. (defun random-string (strings)
  20. (nth (random (list-length strings)) strings))
  21. (defparameter *translation-collect* nil)
  22. (defclass locale-definition ()
  23. ((language
  24. :initform nil
  25. :initarg :language
  26. :accessor language)
  27. (territory
  28. :initform nil
  29. :initarg :territory
  30. :accessor territory)
  31. (codeset
  32. :initform nil
  33. :initarg :codeset
  34. :accessor codeset)
  35. (modifier
  36. :initform nil
  37. :initarg :modifier
  38. :accessor modifier)))
  39. (defmethod print-object ((object locale-definition) stream)
  40. (with-accessors ((language language) (territory territory)
  41. (codeset codeset) (modifier modifier)) object
  42. (print-unreadable-object (object stream :type t :identity t)
  43. (format stream
  44. "lang: ~s terr: ~s codeset ~s modifier ~s"
  45. language territory codeset modifier))))
  46. (defgeneric all-possible-locale-dir (object))
  47. (defgeneric fittest-actual-locale-dir (object))
  48. (defgeneric fittest-actual-locale-file (object filename))
  49. (defmethod all-possible-locale-dir ((object locale-definition))
  50. (with-accessors ((language language) (territory territory)
  51. (codeset codeset) (modifier modifier)) object
  52. (remove-duplicates
  53. (vector
  54. (format nil "~a_~a.~a~@[@~a~]" language territory codeset modifier)
  55. (format nil "~a_~a~@[.~a~]" language territory codeset)
  56. (format nil "~a~@[_~a~]" language territory)
  57. (format nil "~a" language))
  58. :test #'string=)))
  59. (defmethod fittest-actual-locale-dir ((object locale-definition))
  60. (loop for p across (all-possible-locale-dir object) do
  61. (let ((path (format nil "~a~a~a~a~a"
  62. *translation-file-root*
  63. *directory-sep*
  64. p
  65. *directory-sep*
  66. *categories*)))
  67. (when (uiop:directory-exists-p path)
  68. (return-from fittest-actual-locale-dir path))))
  69. nil)
  70. (defmethod fittest-actual-locale-file ((object locale-definition) filename)
  71. (loop for p across (all-possible-locale-dir object) do
  72. (let ((path (format nil "~a~a~a~a~a~a~a.mo"
  73. *translation-file-root*
  74. *directory-sep*
  75. p
  76. *directory-sep*
  77. *categories*
  78. *directory-sep*
  79. filename)))
  80. (when (uiop:file-exists-p path)
  81. (return-from fittest-actual-locale-file path))))
  82. nil)
  83. (defun find-locale ()
  84. (let ((raw (or (uiop:getenvp "LC_ALL")
  85. (uiop:getenvp *categories*)
  86. (uiop:getenvp "LANG"))))
  87. (if raw
  88. (multiple-value-bind (match registers)
  89. (cl-ppcre:scan-to-strings "([^_]+)_?([^\\.]+)?\\.?([^@]+)?@?(.+)?" raw)
  90. (if match
  91. (make-instance 'locale-definition
  92. :language (elt registers 0)
  93. :territory (elt registers 1)
  94. :codeset (elt registers 2)
  95. :modifier (elt registers 3))
  96. nil))
  97. nil)))
  98. (defun save-language (lang &optional (destination nil)
  99. (translation-table nil) (plural-function nil))
  100. "Save a translation table to a file, default path is *translation-file-root* \"/\" lang"
  101. (let ((output-file (or destination
  102. (concatenate 'string
  103. *translation-file-root*
  104. *directory-sep* lang
  105. ".lisp"))))
  106. (create-brand-new-file output-file)
  107. (with-open-file (file output-file
  108. :if-does-not-exist :create
  109. :if-exists :supersede
  110. :direction :output)
  111. (format file "~(~s~)~%~a"
  112. (if plural-function
  113. (symbol-name (get-function-name plural-function))
  114. *plural-form-function*)
  115. (translation-hash-table->list
  116. (if translation-table
  117. translation-table
  118. *translation-table*))))))
  119. (defmacro if-not-utf8-read-whole ((filename) &body body)
  120. `(if (utf8-encoded-p ,filename)
  121. (with-po-file (:filename ,filename)
  122. ,@body)
  123. (with-po-file (:filename nil :buffer (slurp-file ,filename
  124. :convert-to-string t))
  125. ,@body)))
  126. (defun init-translation-table (filename &key
  127. (store-hashtable t)
  128. (store-plural-function t)
  129. (update-translation-table t))
  130. "Load translations from a file (*translation-file-root* is used as a
  131. prefix for the actual path), storing them in a hash table. if
  132. store-hashtable is t *translation-table* is setf'd to the loaded
  133. table, if store-plural-function is t *plural-form-function* is
  134. setf'd too. The *plural-form-function* is setf'd too"
  135. (let ((t-table (make-hash-table :test 'equal))
  136. (local-plural-function nil))
  137. (restart-case
  138. (let ((actual-filename (cond
  139. ((typep *locale* 'locale-definition)
  140. (or
  141. (fittest-actual-locale-file *locale* filename)
  142. ""))
  143. ((typep *locale* 'string)
  144. (format nil "~a~a~a~a~a~a~a.mo"
  145. *translation-file-root*
  146. *directory-sep*
  147. *locale*
  148. *directory-sep*
  149. *categories*
  150. *directory-sep*
  151. filename))
  152. (t
  153. (format nil "~a~a~a"
  154. *translation-file-root*
  155. *directory-sep*
  156. filename)))))
  157. (flet ((to-sexp-table ()
  158. (handler-case
  159. (with-open-file (file actual-filename :if-does-not-exist :error)
  160. (setf local-plural-function (symbol-function
  161. (alexandria:format-symbol 'cl-i18n
  162. "~@:(~a~)"
  163. (read file))))
  164. (setf t-table (translation-list->hash-table (read file)
  165. (make-hash-table
  166. :test 'equal))))
  167. (end-of-file () (setf local-plural-function #'english-plural-form
  168. t-table (make-hash-table :test 'equal)))
  169. (file-error () (progn
  170. (create-brand-new-file actual-filename)
  171. (setf local-plural-function #'english-plural-form
  172. t-table (make-hash-table :test 'equal)))))))
  173. (cond
  174. ((scan +pofile-ext+ actual-filename)
  175. (if-not-utf8-read-whole (actual-filename)
  176. (multiple-value-bind (hashtable plural-function errorsp errors)
  177. (parse-po-file)
  178. (if errorsp
  179. (error 'i18n-conditions:parsing-pofile-error
  180. :text (format nil "~{~a~}" errors))
  181. (progn
  182. (setf local-plural-function plural-function)
  183. (setf t-table hashtable))))))
  184. ((scan +utx-ext+ actual-filename)
  185. (utx-file:with-utx-file (:filename actual-filename)
  186. (multiple-value-bind (hashtable plural-function errorsp errors)
  187. (utx-file:parse-utx-file)
  188. (if errorsp
  189. (error 'i18n-conditions:parsing-utxfile-error
  190. :text (format nil "~{~a~}" errors))
  191. (progn
  192. (setf local-plural-function plural-function)
  193. (setf t-table hashtable))))))
  194. ((scan +lisp-table-ext+ actual-filename)
  195. (to-sexp-table))
  196. ((is-mo-file-p actual-filename :ext ".*" :test-magic-number t);;maybe a MO file?
  197. (with-mo-file (stream mofile actual-filename)
  198. (parse-mofile mofile stream)
  199. (if (not (null (parsing-errors mofile)))
  200. (error 'i18n-conditions:parsing-mofile-error
  201. :text (format nil "~{~a~}" (parsing-errors mofile)))
  202. (multiple-value-bind (hashtable plural-function)
  203. (mofile->translation mofile)
  204. (setf t-table hashtable)
  205. (setf local-plural-function plural-function)))))
  206. (t
  207. (to-sexp-table)))
  208. (when update-translation-table
  209. (maphash #'(lambda (k v) (setf (gethash k *translation-table*) v))
  210. *translation-table*))
  211. (when store-hashtable
  212. (setf *translation-table* t-table))
  213. (when store-plural-function
  214. (setf *plural-form-function* local-plural-function))
  215. (values t-table local-plural-function)))
  216. (return-empty-translation-table ()
  217. (setf t-table (make-hash-table :test 'equal))))))
  218. (defun load-language (catalog &key (locale *locale*) (categories *categories*)
  219. (store-plural-function t) (store-hashtable t)
  220. (update-translation-table t))
  221. "Load a language that will be used for all subsequent translations.
  222. Pass the evaluation results of (find-locale) to let the library guess the current locale.
  223. Use a locale string to explicitly set a locale instead."
  224. (let ((*locale* locale)
  225. (*categories* categories))
  226. (init-translation-table catalog
  227. :store-hashtable store-hashtable
  228. :store-plural-function store-plural-function
  229. :update-translation-table update-translation-table)))
  230. (defun translate (str)
  231. "Translate a string. This will raise an error if the translation table has not been
  232. initialized beforehand. If the string doesn't have a translation a warning
  233. is emitted as well and the original string returned."
  234. (restart-case
  235. (when (and (= (hash-table-count *translation-table*) 0)
  236. (not *translation-collect*))
  237. (error 'i18n-conditions:no-translation-table-error
  238. :text "cl-i18n: translation table not initialized! Call \"load-language\" first."))
  239. (load-language (value &optional (store-plural t))
  240. (load-language value :store-plural-function store-plural))
  241. (use-value (value)
  242. (setf *translation-table* value))
  243. (return-untranslated ()
  244. str))
  245. (multiple-value-bind (translation found) (gethash str *translation-table*)
  246. (if (or (not found) (string= (translated translation) ""))
  247. (if *translation-collect*
  248. (setf (gethash str *translation-table*) (make-translation str))
  249. (progn
  250. (warn 'i18n-conditions:no-translation
  251. :text (format nil "cl-i18n: no translation for ~S defined!" str))
  252. str))
  253. (typecase translation
  254. (translation (translated translation))
  255. (string translation)
  256. (cons (apply (first translation) (rest translation)))
  257. (t (format nil "~A" translation))))))
  258. (defun ntranslate (str1 str2 n)
  259. "Translate a string guessing a plural form.
  260. str1 is the string to be translated
  261. str2 is the fallback plural form
  262. n is the number of the objects
  263. First str1 is checked to get the translated object, if found
  264. the nth element (as computed by the function *plural-form-function*)
  265. of its plural-translated slot is used as plural form.
  266. If this index is less than 0 or more than the length of plural-translated
  267. ntranslate return str2.
  268. If the translation object does not exists str2 is returned"
  269. (let ((translation (gethash str1 *translation-table*)))
  270. (if (not (null translation))
  271. (let ((index (funcall *plural-form-function* n)))
  272. (if (> index 0)
  273. (let ((plural-translation (nth (1- index)
  274. (plural-translated translation))))
  275. (if (not (null plural-translation))
  276. plural-translation
  277. str2))
  278. (translated translation)))
  279. (if (= n 1)
  280. str1
  281. str2))))
  282. (defun read-lisp-string (input)
  283. "Parse a Lisp string. Expects \"input\" to point to the
  284. first character after the leading double quote.
  285. Slick version by Xach."
  286. (with-output-to-string (output)
  287. (loop
  288. (let ((char (read-char input)))
  289. (case char
  290. (#\\
  291. (setf char (read-char input)))
  292. (#\"
  293. (return)))
  294. (write-char char output)))))
  295. (defmacro with-translation ((translations plural-function) &body body)
  296. "Macro to switch between language at runtime"
  297. `(let ((*translation-table* ,translations)
  298. (*plural-form-function* ,plural-function))
  299. ,@body))
  300. (set-dispatch-macro-character #\# #\!
  301. #'(lambda (stream char1 char2)
  302. (declare (ignore char1 char2))
  303. (if (char= (read-char stream) #\")
  304. `(translate ,(read-lisp-string stream))
  305. (error "cl-i18n: the read macro '#!' must precede a double-quoted string!"))))
  306. (set-dispatch-macro-character #\# #-lispworks #\§
  307. #+lispworks #\SECTION-SIGN
  308. #'(lambda (stream char1 num)
  309. (declare (ignore char1))
  310. `(cl-i18n:ntranslate ,(read stream) ,(read stream) ,num)))