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.5.0
18 namespace eval msgcat {
19 namespace export mc mcload mclocale mcmax mcmset mcpreferences mcset \
20 mcunknown mcflset mcflmset
22 # Records the current locale as passed to mclocale
25 # Records the list of locales to search
28 # Records the locale of the currently sourced message catalogue file
31 # Records the mapping between source strings and translated strings. The
32 # dict key is of the form "<locale> <namespace> <src>", where locale and
33 # namespace should be themselves dict values and the value is
34 # the translated string.
35 variable Msgs [dict create]
37 # Map of language codes used in Windows registry to those of ISO-639
38 if {[info sharedlibextension] eq ".dll"} {
39 variable WinRegToISO639 [dict create {*}{
40 01 ar 0401 ar_SA 0801 ar_IQ 0c01 ar_EG 1001 ar_LY 1401 ar_DZ
41 1801 ar_MA 1c01 ar_TN 2001 ar_OM 2401 ar_YE 2801 ar_SY
42 2c01 ar_JO 3001 ar_LB 3401 ar_KW 3801 ar_AE 3c01 ar_BH
46 04 zh 0404 zh_TW 0804 zh_CN 0c04 zh_HK 1004 zh_SG 1404 zh_MO
49 07 de 0407 de_DE 0807 de_CH 0c07 de_AT 1007 de_LU 1407 de_LI
51 09 en 0409 en_US 0809 en_GB 0c09 en_AU 1009 en_CA 1409 en_NZ
52 1809 en_IE 1c09 en_ZA 2009 en_JM 2409 en_GD 2809 en_BZ
53 2c09 en_TT 3009 en_ZW 3409 en_PH
54 0a es 040a es_ES 080a es_MX 0c0a es_ES@modern 100a es_GT 140a es_CR
55 180a es_PA 1c0a es_DO 200a es_VE 240a es_CO 280a es_PE
56 2c0a es_AR 300a es_EC 340a es_CL 380a es_UY 3c0a es_PY
57 400a es_BO 440a es_SV 480a es_HN 4c0a es_NI 500a es_PR
59 0c fr 040c fr_FR 080c fr_BE 0c0c fr_CA 100c fr_CH 140c fr_LU
64 10 it 0410 it_IT 0810 it_CH
67 13 nl 0413 nl_NL 0813 nl_BE
68 14 no 0414 no_NO 0814 nn_NO
70 16 pt 0416 pt_BR 0816 pt_PT
72 18 ro 0418 ro_RO 0818 ro_MO
74 1a hr 041a hr_HR 081a sr_YU 0c1a sr_YU@cyrillic
77 1d sv 041d sv_SE 081d sv_FI
80 20 ur 0420 ur_PK 0820 ur_IN
92 2c az 042c az_AZ@latin 082c az_AZ@cyrillic
108 043c gd_UK 083c ga_IE
110 3e ms 043e ms_MY 083e ms_BN
115 43 uz 0443 uz_UZ@latin 0843 uz_UZ@cyrillic
144 60 ks 0460 ks_PK 0860 ks_IN
145 61 ne 0461 ne_NP 0861 ne_IN
171 # Find the translation for the given string based on the current
172 # locale setting. Check the local namespace first, then look in each
173 # parent namespace until the source is found. If additional args are
174 # specified, use the format command to work them into the traslated
178 # src The string to translate.
179 # args Args to pass to the format command
182 # Returns the translated string. Propagates errors thrown by the
185 proc msgcat::mc {src args} {
186 # Check for the src in each namespace starting from the local and
187 # ending in the global.
193 set ns [uplevel 1 [list ::namespace current]]
196 foreach loc $Loclist {
197 if {[dict exists $Msgs $loc $ns $src]} {
198 if {[llength $args] == 0} {
199 return [dict get $Msgs $loc $ns $src]
201 return [format [dict get $Msgs $loc $ns $src] {*}$args]
205 set ns [namespace parent $ns]
207 # we have not found the translation
208 return [uplevel 1 [list [namespace origin mcunknown] \
209 $Locale $src {*}$args]]
212 # msgcat::mclocale --
214 # Query or set the current locale.
217 # newLocale (Optional) The new locale string. Locale strings
218 # should be composed of one or more sublocale parts
219 # separated by underscores (e.g. en_US).
222 # Returns the current locale.
224 proc msgcat::mclocale {args} {
227 set len [llength $args]
230 return -code error "wrong # args: should be\
231 \"[lindex [info level 0] 0] ?newLocale?\""
235 set newLocale [lindex $args 0]
236 if {$newLocale ne [file tail $newLocale]} {
237 return -code error "invalid newLocale value \"$newLocale\":\
238 could be path to unsafe code."
240 set Locale [string tolower $newLocale]
243 foreach part [split $Locale _] {
244 set word [string trim "${word}_${part}" _]
245 if {$word ne [lindex $Loclist 0]} {
246 set Loclist [linsert $Loclist 0 $word]
250 set Locale [lindex $Loclist 0]
255 # msgcat::mcpreferences --
257 # Fetch the list of locales used to look up strings, ordered from
258 # most preferred to least preferred.
264 # Returns an ordered list of the locales preferred by the user.
266 proc msgcat::mcpreferences {} {
273 # Attempt to load message catalogs for each locale in the
274 # preference list from the specified directory.
277 # langdir The directory to search.
280 # Returns the number of message catalogs that were loaded.
282 proc msgcat::mcload {langdir} {
284 # Save the file locale if we are recursively called
285 if {[info exists FileLocale]} {
286 set nestedFileLocale $FileLocale
289 foreach p [mcpreferences] {
293 set langfile [file join $langdir $p.msg]
294 if {[file exists $langfile]} {
296 set FileLocale [string tolower [file tail [file rootname $langfile]]]
297 if {"root" eq $FileLocale} {
300 uplevel 1 [list ::source -encoding utf-8 $langfile]
304 if {[info exists nestedFileLocale]} {
305 set FileLocale $nestedFileLocale
312 # Set the translation for a given string in a specified locale.
315 # locale The locale to use.
316 # src The source string.
317 # dest (Optional) The translated string. If omitted,
318 # the source string is used.
321 # Returns the new locale.
323 proc msgcat::mcset {locale src {dest ""}} {
325 if {[llength [info level 0]] == 3} { ;# dest not specified
329 set ns [uplevel 1 [list ::namespace current]]
331 set locale [string tolower $locale]
333 dict set Msgs $locale $ns $src $dest
339 # Set the translation for a given string in the current file locale.
342 # src The source string.
343 # dest (Optional) The translated string. If omitted,
344 # the source string is used.
347 # Returns the new locale.
349 proc msgcat::mcflset {src {dest ""}} {
353 if {![info exists FileLocale]} {
355 "must only be used inside a message catalog loaded with ::msgcat::mcload"
357 if {[llength [info level 0]] == 2} { ;# dest not specified
361 set ns [uplevel 1 [list ::namespace current]]
362 dict set Msgs $FileLocale $ns $src $dest
368 # Set the translation for multiple strings in a specified locale.
371 # locale The locale to use.
372 # pairs One or more src/dest pairs (must be even length)
375 # Returns the number of pairs processed
377 proc msgcat::mcmset {locale pairs } {
380 set length [llength $pairs]
382 return -code error "bad translation list:\
383 should be \"[lindex [info level 0] 0] locale {src dest ...}\""
386 set locale [string tolower $locale]
387 set ns [uplevel 1 [list ::namespace current]]
389 foreach {src dest} $pairs {
390 dict set Msgs $locale $ns $src $dest
393 return [expr {$length / 2}]
396 # msgcat::mcflmset --
398 # Set the translation for multiple strings in the mc file locale.
401 # pairs One or more src/dest pairs (must be even length)
404 # Returns the number of pairs processed
406 proc msgcat::mcflmset {pairs} {
410 if {![info exists FileLocale]} {
412 "must only be used inside a message catalog loaded with ::msgcat::mcload"
414 set length [llength $pairs]
416 return -code error "bad translation list:\
417 should be \"[lindex [info level 0] 0] locale {src dest ...}\""
420 set ns [uplevel 1 [list ::namespace current]]
421 foreach {src dest} $pairs {
422 dict set Msgs $FileLocale $ns $src $dest
424 return [expr {$length / 2}]
427 # msgcat::mcunknown --
429 # This routine is called by msgcat::mc if a translation cannot
430 # be found for a string. This routine is intended to be replaced
431 # by an application specific routine for error reporting
432 # purposes. The default behavior is to return the source string.
433 # If additional args are specified, the format command will be used
434 # to work them into the traslated string.
437 # locale The current locale.
438 # src The string to be translated.
439 # args Args to pass to the format command
442 # Returns the translated value.
444 proc msgcat::mcunknown {locale src args} {
445 if {[llength $args]} {
446 return [format $src {*}$args]
454 # Calculates the maximum length of the translated strings of the given
458 # args strings to translate.
461 # Returns the length of the longest translated string.
463 proc msgcat::mcmax {args} {
465 foreach string $args {
466 set translated [uplevel 1 [list [namespace origin mc] $string]]
467 set len [string length $translated]
475 # Convert the locale values stored in environment variables to a form
476 # suitable for passing to [mclocale]
477 proc msgcat::ConvertLocale {value} {
478 # Assume $value is of form: $language[_$territory][.$codeset][@modifier]
479 # Convert to form: $language[_$territory][_$modifier]
481 # Comment out expanded RE version -- bugs alleged
483 # ^ # Match all the way to the beginning
484 # ([^_.@]*) # Match "lanugage"; ends with _, ., or @
485 # (_([^.@]*))? # Match (optional) "territory"; starts with _
486 # ([.]([^@]*))? # Match (optional) "codeset"; starts with .
487 # (@(.*))? # Match (optional) "modifier"; starts with @
488 # $ # Match all the way to the end
489 # } $value -> language _ territory _ codeset _ modifier
490 if {![regexp {^([^_.@]+)(_([^.@]*))?([.]([^@]*))?(@(.*))?$} $value \
491 -> language _ territory _ codeset _ modifier]} {
492 return -code error "invalid locale '$value': empty language part"
495 if {[string length $territory]} {
496 append ret _$territory
498 if {[string length $modifier]} {
499 append ret _$modifier
504 # Initialize the default locale
505 proc msgcat::Init {} {
509 # set default locale, try to get from environment
511 foreach varName {LC_ALL LC_MESSAGES LANG} {
512 if {[info exists env($varName)] && ("" ne $env($varName))} {
514 mclocale [ConvertLocale $env($varName)]
521 # On Darwin, fallback to current CFLocale identifier if available.
523 if {[info exists ::tcl::mac::locale] && $::tcl::mac::locale ne ""} {
525 mclocale [ConvertLocale $::tcl::mac::locale]
531 # The rest of this routine is special processing for Windows or
532 # Cygwin. All other platforms, get out now.
534 if {([info sharedlibextension] ne ".dll")
535 || [catch {package require registry}]} {
540 # On Windows or Cygwin, try to set locale depending on registry
541 # settings, or fall back on locale of "C".
544 # First check registry value LocalName present from Windows Vista
545 # which contains the local string as RFC5646, composed of:
546 # [a-z]{2,3} : language
547 # -[a-z]{4} : script (optional, translated by table Latn->latin)
548 # -[a-z]{2}|[0-9]{3} : territory (optional, numerical region codes not used)
549 # (-.*)* : variant, extension, private use (optional, not used)
550 # Those are translated to local strings.
551 # Examples: de-CH -> de_ch, sr-Latn-CS -> sr_cs@latin, es-419 -> es
553 set key {HKEY_CURRENT_USER\Control Panel\International}
554 if {([registry values $key "LocaleName"] ne "")
555 && [regexp {^([a-z]{2,3})(?:-([a-z]{4}))?(?:-([a-z]{2}))?(?:-.+)?$}\
556 [string tolower [registry get $key "LocaleName"]] match locale\
558 if {"" ne $territory} {
559 append locale _ $territory
561 set modifierDict [dict create latn latin cyrl cyrillic]
562 if {[dict exists $modifierDict $script]} {
563 append locale @ [dict get $modifierDict $script]
566 mclocale [ConvertLocale $locale]
572 # then check key locale which contains a numerical language ID
574 set locale [registry get $key "locale"]
580 # Keep trying to match against smaller and smaller suffixes
581 # of the registry value, since the latter hexadigits appear
582 # to determine general language and earlier hexadigits determine
583 # more precise information, such as territory. For example,
584 # 0409 - English - United States
585 # 0809 - English - United Kingdom
586 # Add more translations to the WinRegToISO639 array above.
588 variable WinRegToISO639
589 set locale [string tolower $locale]
590 while {[string length $locale]} {
592 mclocale [ConvertLocale [dict get $WinRegToISO639 $locale]]
596 set locale [string range $locale 1 end]
599 # No translation known. Fall back on "C" locale