3 # This file defines various procedures which implement a
4 # message catalog facility for Tcl programs. It should be
5 # loaded with the command "package require msgcat".
7 # Copyright (c) 1998-2000 by Ajuba Solutions.
8 # Copyright (c) 1998 by Mark Harrison.
10 # See the file "license.terms" for information on usage and redistribution
11 # of this file, and for a DISCLAIMER OF ALL WARRANTIES.
13 package require Tcl 8.5
14 # When the version number changes, be sure to update the pkgIndex.tcl file,
15 # and the installation directory in the Makefiles.
16 package provide msgcat 1.4.4
18 namespace eval msgcat {
19 namespace export mc mcload mclocale mcmax mcmset mcpreferences mcset \
22 # Records the current locale as passed to mclocale
25 # Records the list of locales to search
28 # Records the mapping between source strings and translated strings. The
29 # dict key is of the form "<locale> <namespace> <src>", where locale and
30 # namespace should be themselves dict values and the value is
31 # the translated string.
32 variable Msgs [dict create]
34 # Map of language codes used in Windows registry to those of ISO-639
35 if { $::tcl_platform(platform) eq "windows" } {
36 variable WinRegToISO639 [dict create {*}{
37 01 ar 0401 ar_SA 0801 ar_IQ 0c01 ar_EG 1001 ar_LY 1401 ar_DZ
38 1801 ar_MA 1c01 ar_TN 2001 ar_OM 2401 ar_YE 2801 ar_SY
39 2c01 ar_JO 3001 ar_LB 3401 ar_KW 3801 ar_AE 3c01 ar_BH
43 04 zh 0404 zh_TW 0804 zh_CN 0c04 zh_HK 1004 zh_SG 1404 zh_MO
46 07 de 0407 de_DE 0807 de_CH 0c07 de_AT 1007 de_LU 1407 de_LI
48 09 en 0409 en_US 0809 en_GB 0c09 en_AU 1009 en_CA 1409 en_NZ
49 1809 en_IE 1c09 en_ZA 2009 en_JM 2409 en_GD 2809 en_BZ
50 2c09 en_TT 3009 en_ZW 3409 en_PH
51 0a es 040a es_ES 080a es_MX 0c0a es_ES@modern 100a es_GT 140a es_CR
52 180a es_PA 1c0a es_DO 200a es_VE 240a es_CO 280a es_PE
53 2c0a es_AR 300a es_EC 340a es_CL 380a es_UY 3c0a es_PY
54 400a es_BO 440a es_SV 480a es_HN 4c0a es_NI 500a es_PR
56 0c fr 040c fr_FR 080c fr_BE 0c0c fr_CA 100c fr_CH 140c fr_LU
61 10 it 0410 it_IT 0810 it_CH
64 13 nl 0413 nl_NL 0813 nl_BE
65 14 no 0414 no_NO 0814 nn_NO
67 16 pt 0416 pt_BR 0816 pt_PT
71 1a hr 041a hr_HR 081a sr_YU 0c1a sr_YU@cyrillic
74 1d sv 041d sv_SE 081d sv_FI
77 20 ur 0420 ur_PK 0820 ur_IN
89 2c az 042c az_AZ@latin 082c az_AZ@cyrillic
104 043c gd_UK 083c ga_IE
106 3e ms 043e ms_MY 083e ms_BN
111 43 uz 0443 uz_UZ@latin 0843 uz_UZ@cyrillic
140 60 ks 0460 ks_PK 0860 ks_IN
141 61 ne 0461 ne_NP 0861 ne_IN
167 # Find the translation for the given string based on the current
168 # locale setting. Check the local namespace first, then look in each
169 # parent namespace until the source is found. If additional args are
170 # specified, use the format command to work them into the traslated
174 # src The string to translate.
175 # args Args to pass to the format command
178 # Returns the translated string. Propagates errors thrown by the
181 proc msgcat::mc {src args} {
182 # Check for the src in each namespace starting from the local and
183 # ending in the global.
189 set ns [uplevel 1 [list ::namespace current]]
192 foreach loc $Loclist {
193 if {[dict exists $Msgs $loc $ns $src]} {
194 if {[llength $args] == 0} {
195 return [dict get $Msgs $loc $ns $src]
197 return [format [dict get $Msgs $loc $ns $src] {*}$args]
201 set ns [namespace parent $ns]
203 # we have not found the translation
204 return [uplevel 1 [list [namespace origin mcunknown] \
205 $Locale $src {*}$args]]
208 # msgcat::mclocale --
210 # Query or set the current locale.
213 # newLocale (Optional) The new locale string. Locale strings
214 # should be composed of one or more sublocale parts
215 # separated by underscores (e.g. en_US).
218 # Returns the current locale.
220 proc msgcat::mclocale {args} {
223 set len [llength $args]
226 return -code error "wrong # args: should be\
227 \"[lindex [info level 0] 0] ?newLocale?\""
231 set newLocale [lindex $args 0]
232 if {$newLocale ne [file tail $newLocale]} {
233 return -code error "invalid newLocale value \"$newLocale\":\
234 could be path to unsafe code."
236 set Locale [string tolower $newLocale]
239 foreach part [split $Locale _] {
240 set word [string trim "${word}_${part}" _]
241 if {$word ne [lindex $Loclist 0]} {
242 set Loclist [linsert $Loclist 0 $word]
246 set Locale [lindex $Loclist 0]
251 # msgcat::mcpreferences --
253 # Fetch the list of locales used to look up strings, ordered from
254 # most preferred to least preferred.
260 # Returns an ordered list of the locales preferred by the user.
262 proc msgcat::mcpreferences {} {
269 # Attempt to load message catalogs for each locale in the
270 # preference list from the specified directory.
273 # langdir The directory to search.
276 # Returns the number of message catalogs that were loaded.
278 proc msgcat::mcload {langdir} {
280 foreach p [mcpreferences] {
284 set langfile [file join $langdir $p.msg]
285 if {[file exists $langfile]} {
287 uplevel 1 [list ::source -encoding utf-8 $langfile]
295 # Set the translation for a given string in a specified locale.
298 # locale The locale to use.
299 # src The source string.
300 # dest (Optional) The translated string. If omitted,
301 # the source string is used.
304 # Returns the new locale.
306 proc msgcat::mcset {locale src {dest ""}} {
308 if {[llength [info level 0]] == 3} { ;# dest not specified
312 set ns [uplevel 1 [list ::namespace current]]
314 set locale [string tolower $locale]
316 dict set Msgs $locale $ns $src $dest
322 # Set the translation for multiple strings in a specified locale.
325 # locale The locale to use.
326 # pairs One or more src/dest pairs (must be even length)
329 # Returns the number of pairs processed
331 proc msgcat::mcmset {locale pairs } {
334 set length [llength $pairs]
336 return -code error "bad translation list:\
337 should be \"[lindex [info level 0] 0] locale {src dest ...}\""
340 set locale [string tolower $locale]
341 set ns [uplevel 1 [list ::namespace current]]
343 foreach {src dest} $pairs {
344 dict set Msgs $locale $ns $src $dest
350 # msgcat::mcunknown --
352 # This routine is called by msgcat::mc if a translation cannot
353 # be found for a string. This routine is intended to be replaced
354 # by an application specific routine for error reporting
355 # purposes. The default behavior is to return the source string.
356 # If additional args are specified, the format command will be used
357 # to work them into the traslated string.
360 # locale The current locale.
361 # src The string to be translated.
362 # args Args to pass to the format command
365 # Returns the translated value.
367 proc msgcat::mcunknown {locale src args} {
368 if {[llength $args]} {
369 return [format $src {*}$args]
377 # Calculates the maximum length of the translated strings of the given
381 # args strings to translate.
384 # Returns the length of the longest translated string.
386 proc msgcat::mcmax {args} {
388 foreach string $args {
389 set translated [uplevel 1 [list [namespace origin mc] $string]]
390 set len [string length $translated]
398 # Convert the locale values stored in environment variables to a form
399 # suitable for passing to [mclocale]
400 proc msgcat::ConvertLocale {value} {
401 # Assume $value is of form: $language[_$territory][.$codeset][@modifier]
402 # Convert to form: $language[_$territory][_$modifier]
404 # Comment out expanded RE version -- bugs alleged
406 # ^ # Match all the way to the beginning
407 # ([^_.@]*) # Match "lanugage"; ends with _, ., or @
408 # (_([^.@]*))? # Match (optional) "territory"; starts with _
409 # ([.]([^@]*))? # Match (optional) "codeset"; starts with .
410 # (@(.*))? # Match (optional) "modifier"; starts with @
411 # $ # Match all the way to the end
412 # } $value -> language _ territory _ codeset _ modifier
413 if {![regexp {^([^_.@]+)(_([^.@]*))?([.]([^@]*))?(@(.*))?$} $value \
414 -> language _ territory _ codeset _ modifier]} {
415 return -code error "invalid locale '$value': empty language part"
418 if {[string length $territory]} {
419 append ret _$territory
421 if {[string length $modifier]} {
422 append ret _$modifier
427 # Initialize the default locale
428 proc msgcat::Init {} {
429 global env tcl_platform
432 # set default locale, try to get from environment
434 foreach varName {LC_ALL LC_MESSAGES LANG} {
435 if {[info exists env($varName)] && ("" ne $env($varName))} {
437 mclocale [ConvertLocale $env($varName)]
444 # On Darwin, fallback to current CFLocale identifier if available.
446 if {[info exists ::tcl::mac::locale] && $::tcl::mac::locale ne ""} {
448 mclocale [ConvertLocale $::tcl::mac::locale]
454 # The rest of this routine is special processing for Windows;
455 # all other platforms, get out now.
457 if {$tcl_platform(platform) ne "windows"} {
462 # On Windows, try to set locale depending on registry settings,
463 # or fall back on locale of "C".
466 package require registry
467 set key {HKEY_CURRENT_USER\Control Panel\International}
468 set locale [registry get $key "locale"]
474 # Keep trying to match against smaller and smaller suffixes
475 # of the registry value, since the latter hexadigits appear
476 # to determine general language and earlier hexadigits determine
477 # more precise information, such as territory. For example,
478 # 0409 - English - United States
479 # 0809 - English - United Kingdom
480 # Add more translations to the WinRegToISO639 array above.
482 variable WinRegToISO639
483 set locale [string tolower $locale]
484 while {[string length $locale]} {
486 mclocale [ConvertLocale [dict get $WinRegToISO639 $locale]]
490 set locale [string range $locale 1 end]
493 # No translation known. Fall back on "C" locale