i18n.scm 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. ;;;; i18n.scm --- internationalization support -*- coding: utf-8 -*-
  2. ;;;; Copyright (C) 2006, 2007, 2009, 2010, 2012,
  3. ;;;; 2017, 2019 Free Software Foundation, Inc.
  4. ;;;;
  5. ;;;; This library is free software; you can redistribute it and/or
  6. ;;;; modify it under the terms of the GNU Lesser General Public
  7. ;;;; License as published by the Free Software Foundation; either
  8. ;;;; version 3 of the License, or (at your option) any later version.
  9. ;;;;
  10. ;;;; This library is distributed in the hope that it will be useful,
  11. ;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. ;;;; Lesser General Public License for more details.
  14. ;;;;
  15. ;;;; You should have received a copy of the GNU Lesser General Public
  16. ;;;; License along with this library; if not, write to the Free Software
  17. ;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  18. ;;; Author: Ludovic Courtès <ludo@gnu.org>
  19. ;;; Commentary:
  20. ;;;
  21. ;;; This module provides a number of routines that support
  22. ;;; internationalization (e.g., locale-dependent text collation, character
  23. ;;; mapping, etc.). It also defines `locale' objects, representing locale
  24. ;;; settings, that may be passed around to most of these procedures.
  25. ;;;
  26. ;;; Code:
  27. (define-module (ice-9 i18n)
  28. :use-module (ice-9 optargs)
  29. :export (;; `locale' type
  30. make-locale locale?
  31. %global-locale
  32. ;; text collation
  33. string-locale<? string-locale>?
  34. string-locale-ci<? string-locale-ci>? string-locale-ci=?
  35. char-locale<? char-locale>?
  36. char-locale-ci<? char-locale-ci>? char-locale-ci=?
  37. ;; character mapping
  38. char-locale-downcase char-locale-upcase char-locale-titlecase
  39. string-locale-downcase string-locale-upcase string-locale-titlecase
  40. ;; reading numbers
  41. locale-string->integer locale-string->inexact
  42. ;; charset/encoding
  43. locale-encoding
  44. ;; days and months
  45. locale-day-short locale-day locale-month-short locale-month
  46. ;; date and time
  47. locale-am-string locale-pm-string
  48. locale-date+time-format locale-date-format locale-time-format
  49. locale-time+am/pm-format
  50. locale-era locale-era-year
  51. locale-era-date-format locale-era-date+time-format
  52. locale-era-time-format
  53. ;; monetary
  54. locale-currency-symbol
  55. locale-monetary-decimal-point locale-monetary-thousands-separator
  56. locale-monetary-grouping locale-monetary-fractional-digits
  57. locale-currency-symbol-precedes-positive?
  58. locale-currency-symbol-precedes-negative?
  59. locale-positive-separated-by-space?
  60. locale-negative-separated-by-space?
  61. locale-monetary-positive-sign locale-monetary-negative-sign
  62. locale-positive-sign-position locale-negative-sign-position
  63. monetary-amount->locale-string
  64. ;; number formatting
  65. locale-digit-grouping locale-decimal-point
  66. locale-thousands-separator
  67. number->locale-string
  68. ;; miscellaneous
  69. locale-yes-regexp locale-no-regexp
  70. ;; debugging
  71. %locale-dump))
  72. (eval-when (expand load eval)
  73. (load-extension (string-append "libguile-" (effective-version))
  74. "scm_init_i18n"))
  75. ;;;
  76. ;;; Charset/encoding.
  77. ;;;
  78. (define (locale-encoding . locale)
  79. (apply nl-langinfo CODESET locale))
  80. ;;;
  81. ;;; Months and days.
  82. ;;;
  83. ;; Helper macro: Define a procedure named NAME that maps its argument to
  84. ;; NL-ITEMS. Gnulib guarantees that these items are available.
  85. (define-macro (define-vector-langinfo-mapping name nl-items)
  86. (let* ((item-count (length nl-items))
  87. (defines `(define %nl-items (vector #f ,@nl-items)))
  88. (make-body (lambda (result)
  89. `(if (and (integer? item) (exact? item))
  90. (if (and (>= item 1) (<= item ,item-count))
  91. ,result
  92. (throw 'out-of-range "out of range" item))
  93. (throw 'wrong-type-arg "wrong argument type" item)))))
  94. `(define (,name item . locale)
  95. ,defines
  96. ,(make-body '(apply nl-langinfo (vector-ref %nl-items item) locale)))))
  97. (define-vector-langinfo-mapping locale-day-short
  98. (ABDAY_1 ABDAY_2 ABDAY_3 ABDAY_4 ABDAY_5 ABDAY_6 ABDAY_7))
  99. (define-vector-langinfo-mapping locale-day
  100. (DAY_1 DAY_2 DAY_3 DAY_4 DAY_5 DAY_6 DAY_7))
  101. (define-vector-langinfo-mapping locale-month-short
  102. (ABMON_1 ABMON_2 ABMON_3 ABMON_4 ABMON_5 ABMON_6
  103. ABMON_7 ABMON_8 ABMON_9 ABMON_10 ABMON_11 ABMON_12))
  104. (define-vector-langinfo-mapping locale-month
  105. (MON_1 MON_2 MON_3 MON_4 MON_5 MON_6 MON_7 MON_8 MON_9 MON_10 MON_11 MON_12))
  106. ;;;
  107. ;;; Date and time.
  108. ;;;
  109. ;; Define a procedure NAME that gets langinfo item ITEM. Gnulib's
  110. ;; `nl_langinfo' does not guarantee that all these items are supported
  111. ;; (for instance, `GROUPING' is lacking on Darwin and Gnulib provides no
  112. ;; replacement), so use DEFAULT as the default value when ITEM is not
  113. ;; available.
  114. (define-macro (define-simple-langinfo-mapping name item default)
  115. (let ((body (if (defined? item)
  116. `(apply nl-langinfo ,item locale)
  117. default)))
  118. `(define (,name . locale)
  119. ,body)))
  120. (define-simple-langinfo-mapping locale-am-string
  121. AM_STR "AM")
  122. (define-simple-langinfo-mapping locale-pm-string
  123. PM_STR "PM")
  124. (define-simple-langinfo-mapping locale-date+time-format
  125. D_T_FMT "%a %b %e %H:%M:%S %Y")
  126. (define-simple-langinfo-mapping locale-date-format
  127. D_FMT "%m/%d/%y")
  128. (define-simple-langinfo-mapping locale-time-format
  129. T_FMT "%H:%M:%S")
  130. (define-simple-langinfo-mapping locale-time+am/pm-format
  131. T_FMT_AMPM "%I:%M:%S %p")
  132. (define-simple-langinfo-mapping locale-era
  133. ERA "")
  134. (define-simple-langinfo-mapping locale-era-year
  135. ERA_YEAR "")
  136. (define-simple-langinfo-mapping locale-era-date+time-format
  137. ERA_D_T_FMT "")
  138. (define-simple-langinfo-mapping locale-era-date-format
  139. ERA_D_FMT "")
  140. (define-simple-langinfo-mapping locale-era-time-format
  141. ERA_T_FMT "")
  142. ;;;
  143. ;;; Monetary information.
  144. ;;;
  145. ;; Define a procedure NAME that gets item LOCAL-ITEM or INTL-ITEM,
  146. ;; depending on whether the caller asked for the international version
  147. ;; or not. Since Gnulib's `nl_langinfo' module doesn't guarantee that
  148. ;; all these items are available, use DEFAULT/LOCAL and DEFAULT/INTL as
  149. ;; default values when the system does not support them.
  150. (define-macro (define-monetary-langinfo-mapping name local-item intl-item
  151. default/local default/intl)
  152. (let ((body
  153. (let ((intl (if (defined? intl-item)
  154. `(apply nl-langinfo ,intl-item locale)
  155. default/intl))
  156. (local (if (defined? local-item)
  157. `(apply nl-langinfo ,local-item locale)
  158. default/local)))
  159. `(if intl? ,intl ,local))))
  160. `(define (,name intl? . locale)
  161. ,body)))
  162. ;; FIXME: How can we use ALT_DIGITS?
  163. (define-monetary-langinfo-mapping locale-currency-symbol
  164. CRNCYSTR INT_CURR_SYMBOL
  165. "-" "")
  166. (define-monetary-langinfo-mapping locale-monetary-fractional-digits
  167. FRAC_DIGITS INT_FRAC_DIGITS
  168. 2 2)
  169. (define-simple-langinfo-mapping locale-monetary-positive-sign
  170. POSITIVE_SIGN "+")
  171. (define-simple-langinfo-mapping locale-monetary-negative-sign
  172. NEGATIVE_SIGN "-")
  173. (define-simple-langinfo-mapping locale-monetary-decimal-point
  174. MON_DECIMAL_POINT ".")
  175. (define-simple-langinfo-mapping locale-monetary-thousands-separator
  176. MON_THOUSANDS_SEP "")
  177. (define-simple-langinfo-mapping locale-monetary-grouping
  178. MON_GROUPING '())
  179. (define-monetary-langinfo-mapping locale-currency-symbol-precedes-positive?
  180. P_CS_PRECEDES INT_P_CS_PRECEDES
  181. #t #t)
  182. (define-monetary-langinfo-mapping locale-currency-symbol-precedes-negative?
  183. N_CS_PRECEDES INT_N_CS_PRECEDES
  184. #t #t)
  185. (define-monetary-langinfo-mapping locale-positive-separated-by-space?
  186. ;; Whether a space should be inserted between a positive amount and the
  187. ;; currency symbol.
  188. P_SEP_BY_SPACE INT_P_SEP_BY_SPACE
  189. #t #t)
  190. (define-monetary-langinfo-mapping locale-negative-separated-by-space?
  191. ;; Whether a space should be inserted between a negative amount and the
  192. ;; currency symbol.
  193. N_SEP_BY_SPACE INT_N_SEP_BY_SPACE
  194. #t #t)
  195. (define-monetary-langinfo-mapping locale-positive-sign-position
  196. ;; Position of the positive sign wrt. currency symbol and quantity in a
  197. ;; monetary amount.
  198. P_SIGN_POSN INT_P_SIGN_POSN
  199. 'unspecified 'unspecified)
  200. (define-monetary-langinfo-mapping locale-negative-sign-position
  201. ;; Position of the negative sign wrt. currency symbol and quantity in a
  202. ;; monetary amount.
  203. N_SIGN_POSN INT_N_SIGN_POSN
  204. 'unspecified 'unspecified)
  205. (define (integer->string number)
  206. "Return a string representing NUMBER, an integer, written in base 10."
  207. (define (digit->char digit)
  208. (integer->char (+ digit (char->integer #\0))))
  209. (if (zero? number)
  210. "0"
  211. (let loop ((number number)
  212. (digits '()))
  213. (if (zero? number)
  214. (list->string digits)
  215. (loop (quotient number 10)
  216. (cons (digit->char (modulo number 10))
  217. digits))))))
  218. (define (number-decimal-string number digit-count)
  219. "Return a string representing the decimal part of NUMBER. When
  220. DIGIT-COUNT is an integer, return exactly DIGIT-COUNT digits; when
  221. DIGIT-COUNT is #t, return as many decimals as necessary, up to an
  222. arbitrary limit."
  223. (define max-decimals
  224. 5)
  225. ;; XXX: This is brute-force and could be improved by following one of
  226. ;; the "Printing Floating-Point Numbers Quickly and Accurately"
  227. ;; papers.
  228. (if (integer? digit-count)
  229. (let ((number (* (expt 10 digit-count)
  230. (- number (floor number)))))
  231. (string-pad (integer->string (round (inexact->exact number)))
  232. digit-count
  233. #\0))
  234. (let loop ((decimals 0))
  235. (let ((number' (* number (expt 10 decimals))))
  236. (if (or (= number' (floor number'))
  237. (>= decimals max-decimals))
  238. (let* ((fraction (- number'
  239. (* (floor number)
  240. (expt 10 decimals))))
  241. (str (integer->string
  242. (round (inexact->exact fraction)))))
  243. (if (zero? fraction)
  244. ""
  245. str))
  246. (loop (+ decimals 1)))))))
  247. (define (%number-integer-part int grouping separator)
  248. ;; Process INT (a string denoting a number's integer part) and return a new
  249. ;; string with digit grouping and separators according to GROUPING (a list,
  250. ;; potentially circular) and SEPARATOR (a string).
  251. ;; Process INT from right to left.
  252. (let loop ((int int)
  253. (grouping grouping)
  254. (result '()))
  255. (cond ((string=? int "") (apply string-append result))
  256. ((null? grouping) (apply string-append int result))
  257. (else
  258. (let* ((len (string-length int))
  259. (cut (min (car grouping) len)))
  260. (loop (substring int 0 (- len cut))
  261. (cdr grouping)
  262. (let ((sub (substring int (- len cut) len)))
  263. (if (> len cut)
  264. (cons* separator sub result)
  265. (cons sub result)))))))))
  266. (define (add-monetary-sign+currency amount figure intl? locale)
  267. ;; Add a sign and currency symbol around FIGURE. FIGURE should be a
  268. ;; formatted unsigned amount (a string) representing AMOUNT.
  269. (let* ((positive? (> amount 0))
  270. (sign
  271. (cond ((> amount 0) (locale-monetary-positive-sign locale))
  272. ((< amount 0) (locale-monetary-negative-sign locale))
  273. (else "")))
  274. (currency (locale-currency-symbol intl? locale))
  275. (currency-precedes?
  276. (if positive?
  277. locale-currency-symbol-precedes-positive?
  278. locale-currency-symbol-precedes-negative?))
  279. (separated?
  280. (if positive?
  281. locale-positive-separated-by-space?
  282. locale-negative-separated-by-space?))
  283. (sign-position
  284. (if positive?
  285. locale-positive-sign-position
  286. locale-negative-sign-position))
  287. (currency-space
  288. (if (separated? intl? locale) " " ""))
  289. (append-currency
  290. (lambda (amt)
  291. (if (currency-precedes? intl? locale)
  292. (string-append currency currency-space amt)
  293. (string-append amt currency-space currency)))))
  294. (case (sign-position intl? locale)
  295. ((parenthesize)
  296. (string-append "(" (append-currency figure) ")"))
  297. ((sign-before)
  298. (string-append sign (append-currency figure)))
  299. ((sign-after unspecified)
  300. ;; following glibc's recommendation for `unspecified'.
  301. (if (currency-precedes? intl? locale)
  302. (string-append currency currency-space sign figure)
  303. (string-append figure currency-space currency sign)))
  304. ((sign-before-currency-symbol)
  305. (if (currency-precedes? intl? locale)
  306. (string-append sign currency currency-space figure)
  307. (string-append figure currency-space sign currency))) ;; unlikely
  308. ((sign-after-currency-symbol)
  309. (if (currency-precedes? intl? locale)
  310. (string-append currency sign currency-space figure)
  311. (string-append figure currency-space currency sign)))
  312. (else
  313. (error "unsupported sign position" (sign-position intl? locale))))))
  314. (define* (monetary-amount->locale-string amount intl?
  315. #:optional (locale %global-locale))
  316. "Convert @var{amount} (an inexact) into a string according to the cultural
  317. conventions of either @var{locale} (a locale object) or the current locale.
  318. If @var{intl?} is true, then the international monetary format for the given
  319. locale is used."
  320. (let* ((fraction-digits
  321. (or (locale-monetary-fractional-digits intl? locale) 2))
  322. (decimal-part
  323. (lambda (dec)
  324. (if (or (string=? dec "") (eq? 0 fraction-digits))
  325. ""
  326. (string-append (locale-monetary-decimal-point locale)
  327. (if (< fraction-digits (string-length dec))
  328. (substring dec 0 fraction-digits)
  329. dec)))))
  330. (int (integer->string (inexact->exact
  331. (floor (abs amount)))))
  332. (dec (decimal-part
  333. (number-decimal-string (abs amount)
  334. fraction-digits)))
  335. (grouping (locale-monetary-grouping locale))
  336. (separator (locale-monetary-thousands-separator locale)))
  337. (add-monetary-sign+currency amount
  338. (string-append
  339. (%number-integer-part int grouping
  340. separator)
  341. dec)
  342. intl? locale)))
  343. ;;;
  344. ;;; Number formatting.
  345. ;;;
  346. (define-simple-langinfo-mapping locale-digit-grouping
  347. GROUPING '())
  348. (define-simple-langinfo-mapping locale-decimal-point
  349. RADIXCHAR ".")
  350. (define-simple-langinfo-mapping locale-thousands-separator
  351. THOUSEP "")
  352. (define* (number->locale-string number
  353. #:optional (fraction-digits #t)
  354. (locale %global-locale))
  355. "Convert @var{number} (an inexact) into a string according to the cultural
  356. conventions of either @var{locale} (a locale object) or the current locale.
  357. By default, print as many fractional digits as necessary, up to an upper bound.
  358. Optionally, @var{fraction-digits} may be bound to an integer specifying the
  359. number of fractional digits to be displayed."
  360. (let* ((sign
  361. (cond ((> number 0) "")
  362. ((< number 0) "-")
  363. (else "")))
  364. (decimal-part
  365. (lambda (dec)
  366. (if (or (string=? dec "") (eq? 0 fraction-digits))
  367. ""
  368. (string-append (locale-decimal-point locale)
  369. (if (and (integer? fraction-digits)
  370. (< fraction-digits
  371. (string-length dec)))
  372. (substring dec 0 fraction-digits)
  373. dec))))))
  374. (let* ((int (integer->string (inexact->exact
  375. (floor (abs number)))))
  376. (dec (decimal-part
  377. (number-decimal-string (abs number)
  378. fraction-digits)))
  379. (grouping (locale-digit-grouping locale))
  380. (separator (locale-thousands-separator locale)))
  381. (string-append sign
  382. (%number-integer-part int grouping separator)
  383. dec))))
  384. ;;;
  385. ;;; Miscellaneous.
  386. ;;;
  387. (define-simple-langinfo-mapping locale-yes-regexp
  388. YESEXPR "^[yY]")
  389. (define-simple-langinfo-mapping locale-no-regexp
  390. NOEXPR "^[nN]")
  391. ;; `YESSTR' and `NOSTR' are considered deprecated so we don't provide them.
  392. ;;;
  393. ;;; Debugging
  394. ;;;
  395. (define (%locale-dump loc)
  396. "Given a locale, display an association list containing all the locale
  397. information.
  398. This procedure is intended for debugging locale problems, and should
  399. not be used in production code."
  400. (when (locale? loc)
  401. (list
  402. (cons 'encoding (locale-encoding loc))
  403. (cons 'day-short
  404. (map (lambda (n) (locale-day-short (1+ n) loc)) (iota 7)))
  405. (cons 'day
  406. (map (lambda (n) (locale-day (1+ n) loc)) (iota 7)))
  407. (cons 'month-short
  408. (map (lambda (n) (locale-month-short (1+ n) loc)) (iota 12)))
  409. (cons 'month
  410. (map (lambda (n) (locale-month (1+ n) loc)) (iota 12)))
  411. (cons 'am-string (locale-am-string loc))
  412. (cons 'pm-string (locale-pm-string loc))
  413. (cons 'date+time-format (locale-date+time-format loc))
  414. (cons 'date-format (locale-date-format loc))
  415. (cons 'time-format (locale-time-format loc))
  416. (cons 'time+am/pm-format (locale-time+am/pm-format loc))
  417. (cons 'era (locale-era loc))
  418. (cons 'era-year (locale-era-year loc))
  419. (cons 'era-date-format (locale-era-date-format loc))
  420. (cons 'era-date+time-format (locale-era-date+time-format loc))
  421. (cons 'era-time-format (locale-era-time-format loc))
  422. (cons 'currency-symbol
  423. (list (locale-currency-symbol #t loc)
  424. (locale-currency-symbol #f loc)))
  425. (cons 'monetary-decimal-point (locale-monetary-decimal-point loc))
  426. (cons 'monetary-thousands-separator (locale-monetary-thousands-separator loc))
  427. (cons 'monetary-grouping (locale-monetary-grouping loc))
  428. (cons 'monetary-fractional-digits
  429. (list (locale-monetary-fractional-digits #t loc)
  430. (locale-monetary-fractional-digits #f loc)))
  431. (cons 'currency-symbol-precedes-positive?
  432. (list (locale-currency-symbol-precedes-positive? #t loc)
  433. (locale-currency-symbol-precedes-positive? #f loc)))
  434. (cons 'currency-symbol-precedes-negative?
  435. (list (locale-currency-symbol-precedes-negative? #t loc)
  436. (locale-currency-symbol-precedes-negative? #f loc)))
  437. (cons 'positive-separated-by-space?
  438. (list (locale-positive-separated-by-space? #t loc)
  439. (locale-positive-separated-by-space? #f loc)))
  440. (cons 'negative-separated-by-space?
  441. (list (locale-negative-separated-by-space? #t loc)
  442. (locale-negative-separated-by-space? #f loc)))
  443. (cons 'monetary-positive-sign (locale-monetary-positive-sign loc))
  444. (cons 'monetary-negative-sign (locale-monetary-negative-sign loc))
  445. (cons 'positive-sign-position
  446. (list (locale-positive-sign-position #t loc)
  447. (locale-negative-sign-position #f loc)))
  448. (cons 'negative-sign-position
  449. (list (locale-negative-sign-position #t loc)
  450. (locale-negative-sign-position #f loc)))
  451. (cons 'digit-grouping (locale-digit-grouping loc))
  452. (cons 'decimal-point (locale-decimal-point loc))
  453. (cons 'thousands-separator (locale-thousands-separator loc))
  454. (cons 'locale-yes-regexp (locale-yes-regexp loc))
  455. (cons 'no-regexp (locale-no-regexp loc)))))
  456. ;;; i18n.scm ends here