# msgcat.tcl -- # # This file defines various procedures which implement a # message catalog facility for Tcl programs. It should be # loaded with the command "package require msgcat". # # Copyright (c) 2010-2015 by Harald Oehlmann. # Copyright (c) 1998-2000 by Ajuba Solutions. # Copyright (c) 1998 by Mark Harrison. # # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. package require Tcl 8.5- # When the version number changes, be sure to update the pkgIndex.tcl file, # and the installation directory in the Makefiles. package provide msgcat 1.6.1 namespace eval msgcat { namespace export mc mcexists mcload mclocale mcmax mcmset mcpreferences mcset\ mcunknown mcflset mcflmset mcloadedlocales mcforgetpackage\ mcpackageconfig mcpackagelocale # Records the list of locales to search variable Loclist {} # List of currently loaded locales variable LoadedLocales {} # Records the locale of the currently sourced message catalogue file variable FileLocale # Configuration values per Package (e.g. client namespace). # The dict key is of the form "<option> <namespace>" and the value is the # configuration option. A nonexisting key is an unset option. variable PackageConfig [dict create mcfolder {} loadcmd {} changecmd {}\ unknowncmd {} loadedlocales {} loclist {}] # Records the mapping between source strings and translated strings. The # dict key is of the form "<namespace> <locale> <src>", where locale and # namespace should be themselves dict values and the value is # the translated string. variable Msgs [dict create] # Map of language codes used in Windows registry to those of ISO-639 if {[info sharedlibextension] eq ".dll"} { variable WinRegToISO639 [dict create {*}{ 01 ar 0401 ar_SA 0801 ar_IQ 0c01 ar_EG 1001 ar_LY 1401 ar_DZ 1801 ar_MA 1c01 ar_TN 2001 ar_OM 2401 ar_YE 2801 ar_SY 2c01 ar_JO 3001 ar_LB 3401 ar_KW 3801 ar_AE 3c01 ar_BH 4001 ar_QA 02 bg 0402 bg_BG 03 ca 0403 ca_ES 04 zh 0404 zh_TW 0804 zh_CN 0c04 zh_HK 1004 zh_SG 1404 zh_MO 05 cs 0405 cs_CZ 06 da 0406 da_DK 07 de 0407 de_DE 0807 de_CH 0c07 de_AT 1007 de_LU 1407 de_LI 08 el 0408 el_GR 09 en 0409 en_US 0809 en_GB 0c09 en_AU 1009 en_CA 1409 en_NZ 1809 en_IE 1c09 en_ZA 2009 en_JM 2409 en_GD 2809 en_BZ 2c09 en_TT 3009 en_ZW 3409 en_PH 0a es 040a es_ES 080a es_MX 0c0a es_ES@modern 100a es_GT 140a es_CR 180a es_PA 1c0a es_DO 200a es_VE 240a es_CO 280a es_PE 2c0a es_AR 300a es_EC 340a es_CL 380a es_UY 3c0a es_PY 400a es_BO 440a es_SV 480a es_HN 4c0a es_NI 500a es_PR 0b fi 040b fi_FI 0c fr 040c fr_FR 080c fr_BE 0c0c fr_CA 100c fr_CH 140c fr_LU 180c fr_MC 0d he 040d he_IL 0e hu 040e hu_HU 0f is 040f is_IS 10 it 0410 it_IT 0810 it_CH 11 ja 0411 ja_JP 12 ko 0412 ko_KR 13 nl 0413 nl_NL 0813 nl_BE 14 no 0414 no_NO 0814 nn_NO 15 pl 0415 pl_PL 16 pt 0416 pt_BR 0816 pt_PT 17 rm 0417 rm_CH 18 ro 0418 ro_RO 0818 ro_MO 19 ru 0819 ru_MO 1a hr 041a hr_HR 081a sr_YU 0c1a sr_YU@cyrillic 1b sk 041b sk_SK 1c sq 041c sq_AL 1d sv 041d sv_SE 081d sv_FI 1e th 041e th_TH 1f tr 041f tr_TR 20 ur 0420 ur_PK 0820 ur_IN 21 id 0421 id_ID 22 uk 0422 uk_UA 23 be 0423 be_BY 24 sl 0424 sl_SI 25 et 0425 et_EE 26 lv 0426 lv_LV 27 lt 0427 lt_LT 28 tg 0428 tg_TJ 29 fa 0429 fa_IR 2a vi 042a vi_VN 2b hy 042b hy_AM 2c az 042c az_AZ@latin 082c az_AZ@cyrillic 2d eu 2e wen 042e wen_DE 2f mk 042f mk_MK 30 bnt 0430 bnt_TZ 31 ts 0431 ts_ZA 32 tn 33 ven 0433 ven_ZA 34 xh 0434 xh_ZA 35 zu 0435 zu_ZA 36 af 0436 af_ZA 37 ka 0437 ka_GE 38 fo 0438 fo_FO 39 hi 0439 hi_IN 3a mt 043a mt_MT 3b se 043b se_NO 043c gd_UK 083c ga_IE 3d yi 043d yi_IL 3e ms 043e ms_MY 083e ms_BN 3f kk 043f kk_KZ 40 ky 0440 ky_KG 41 sw 0441 sw_KE 42 tk 0442 tk_TM 43 uz 0443 uz_UZ@latin 0843 uz_UZ@cyrillic 44 tt 0444 tt_RU 45 bn 0445 bn_IN 46 pa 0446 pa_IN 47 gu 0447 gu_IN 48 or 0448 or_IN 49 ta 4a te 044a te_IN 4b kn 044b kn_IN 4c ml 044c ml_IN 4d as 044d as_IN 4e mr 044e mr_IN 4f sa 044f sa_IN 50 mn 51 bo 0451 bo_CN 52 cy 0452 cy_GB 53 km 0453 km_KH 54 lo 0454 lo_LA 55 my 0455 my_MM 56 gl 0456 gl_ES 57 kok 0457 kok_IN 58 mni 0458 mni_IN 59 sd 5a syr 045a syr_TR 5b si 045b si_LK 5c chr 045c chr_US 5d iu 045d iu_CA 5e am 045e am_ET 5f ber 045f ber_MA 60 ks 0460 ks_PK 0860 ks_IN 61 ne 0461 ne_NP 0861 ne_IN 62 fy 0462 fy_NL 63 ps 64 tl 0464 tl_PH 65 div 0465 div_MV 66 bin 0466 bin_NG 67 ful 0467 ful_NG 68 ha 0468 ha_NG 69 nic 0469 nic_NG 6a yo 046a yo_NG 70 ibo 0470 ibo_NG 71 kau 0471 kau_NG 72 om 0472 om_ET 73 ti 0473 ti_ET 74 gn 0474 gn_PY 75 cpe 0475 cpe_US 76 la 0476 la_VA 77 so 0477 so_SO 78 sit 0478 sit_CN 79 pap 0479 pap_AN }] } } # msgcat::mc -- # # Find the translation for the given string based on the current # locale setting. Check the local namespace first, then look in each # parent namespace until the source is found. If additional args are # specified, use the format command to work them into the traslated # string. # If no catalog item is found, mcunknown is called in the caller frame # and its result is returned. # # Arguments: # src The string to translate. # args Args to pass to the format command # # Results: # Returns the translated string. Propagates errors thrown by the # format command. proc msgcat::mc {src args} { # this may be replaced by: # return [mcget -namespace [uplevel 1 [list ::namespace current]] --\ # $src {*}$args] # Check for the src in each namespace starting from the local and # ending in the global. variable Msgs variable Loclist set ns [uplevel 1 [list ::namespace current]] set loclist [PackagePreferences $ns] set nscur $ns while {$nscur != ""} { foreach loc $loclist { if {[dict exists $Msgs $nscur $loc $src]} { return [DefaultUnknown "" [dict get $Msgs $nscur $loc $src]\ {*}$args] } } set nscur [namespace parent $nscur] } # call package local or default unknown command set args [linsert $args 0 [lindex $loclist 0] $src] switch -exact -- [Invoke unknowncmd $args $ns result 1] { 0 { return [uplevel 1 [linsert $args 0 [namespace origin mcunknown]]] } 1 { return [DefaultUnknown {*}$args] } default { return $result } } } # msgcat::mcexists -- # # Check if a catalog item is set or if mc would invoke mcunknown. # # Arguments: # -exactnamespace Only check the exact namespace and no # parent namespaces # -exactlocale Only check the exact locale and not all members # of the preferences list # src Message catalog key # # Results: # true if an adequate catalog key was found proc msgcat::mcexists {args} { variable Msgs variable Loclist variable PackageConfig set ns [uplevel 1 [list ::namespace current]] set loclist [PackagePreferences $ns] while {[llength $args] != 1} { set args [lassign $args option] switch -glob -- $option { -exactnamespace { set exactnamespace 1 } -exactlocale { set loclist [lrange $loclist 0 0] } -* { return -code error "unknown option \"$option\"" } default { return -code error "wrong # args: should be\ \"[lindex [info level 0] 0] ?-exactnamespace?\ ?-exactlocale? src\"" } } } set src [lindex $args 0] while {$ns ne ""} { foreach loc $loclist { if {[dict exists $Msgs $ns $loc $src]} { return 1 } } if {[info exists exactnamespace]} {return 0} set ns [namespace parent $ns] } return 0 } # msgcat::mclocale -- # # Query or set the current locale. # # Arguments: # newLocale (Optional) The new locale string. Locale strings # should be composed of one or more sublocale parts # separated by underscores (e.g. en_US). # # Results: # Returns the normalized set locale. proc msgcat::mclocale {args} { variable Loclist variable LoadedLocales set len [llength $args] if {$len > 1} { return -code error "wrong # args: should be\ \"[lindex [info level 0] 0] ?newLocale?\"" } if {$len == 1} { set newLocale [string tolower [lindex $args 0]] if {$newLocale ne [file tail $newLocale]} { return -code error "invalid newLocale value \"$newLocale\":\ could be path to unsafe code." } if {[lindex $Loclist 0] ne $newLocale} { set Loclist [GetPreferences $newLocale] # locale not loaded jet LoadAll $Loclist # Invoke callback Invoke changecmd $Loclist } } return [lindex $Loclist 0] } # msgcat::GetPreferences -- # # Get list of locales from a locale. # The first element is always the lowercase locale. # Other elements have one component separated by "_" less. # Multiple "_" are seen as one separator: de__ch_spec de__ch de {} # # Arguments: # Locale. # # Results: # Locale list proc msgcat::GetPreferences {locale} { set locale [string tolower $locale] set loclist [list $locale] while {-1 !=[set pos [string last "_" $locale]]} { set locale [string range $locale 0 $pos-1] if { "_" ne [string index $locale end] } { lappend loclist $locale } } if {"" ne [lindex $loclist end]} { lappend loclist {} } return $loclist } # msgcat::mcpreferences -- # # Fetch the list of locales used to look up strings, ordered from # most preferred to least preferred. # # Arguments: # None. # # Results: # Returns an ordered list of the locales preferred by the user. proc msgcat::mcpreferences {} { variable Loclist return $Loclist } # msgcat::mcloadedlocales -- # # Get or change the list of currently loaded default locales # # The following subcommands are available: # loaded # Get the current list of loaded locales # clear # Remove all loaded locales not present in mcpreferences. # # Arguments: # subcommand One of loaded or clear # # Results: # Empty string, if not stated differently for the subcommand proc msgcat::mcloadedlocales {subcommand} { variable Loclist variable LoadedLocales variable Msgs variable PackageConfig switch -exact -- $subcommand { clear { # Remove all locales not contained in Loclist # skip any packages with package locale set LoadedLocales $Loclist foreach ns [dict keys $Msgs] { if {![dict exists $PackageConfig loclist $ns]} { foreach locale [dict keys [dict get $Msgs $ns]] { if {$locale ni $Loclist} { dict unset Msgs $ns $locale } } } } } loaded { return $LoadedLocales } default { return -code error "unknown subcommand \"$subcommand\": must be\ clear, or loaded" } } return } # msgcat::mcpackagelocale -- # # Get or change the package locale of the calling package. # # The following subcommands are available: # set # Set a package locale. # This may load message catalog files and may clear message catalog # items, if the former locale was the default locale. # Returns the normalized set locale. # The default locale is taken, if locale is not given. # get # Get the locale valid for this package. # isset # Returns true, if a package locale is set # unset # Unset the package locale and activate the default locale. # This loads message catalog file which where missing in the package # locale. # preferences # Return locale preference list valid for the package. # loaded # Return loaded locale list valid for the current package. # clear # If the current package has a package locale, remove all package # locales not containes in package mcpreferences. # It is an error to call this without a package locale set. # # The subcommands get, preferences and loaded return the corresponding # default data, if no package locale is set. # # Arguments: # subcommand see list above # locale package locale (only set subcommand) # # Results: # Empty string, if not stated differently for the subcommand proc msgcat::mcpackagelocale {subcommand {locale ""}} { # todo: implement using an ensemble variable Loclist variable LoadedLocales variable Msgs variable PackageConfig # Check option # check if required item is exactly provided if {[llength [info level 0]] == 2} { # locale not given unset locale } else { # locale given if {$subcommand in {"get" "isset" "unset" "preferences" "loaded" "clear"} } { return -code error "wrong # args: should be\ \"[lrange [info level 0] 0 1]\"" } set locale [string tolower $locale] } set ns [uplevel 1 {::namespace current}] switch -exact -- $subcommand { get { return [lindex [PackagePreferences $ns] 0] } preferences { return [PackagePreferences $ns] } loaded { return [PackageLocales $ns] } present { return [expr {$locale in [PackageLocales $ns]} ]} isset { return [dict exists $PackageConfig loclist $ns] } set { # set a package locale or add a package locale # Copy the default locale if no package locale set so far if {![dict exists $PackageConfig loclist $ns]} { dict set PackageConfig loclist $ns $Loclist dict set PackageConfig loadedlocales $ns $LoadedLocales } # Check if changed set loclist [dict get $PackageConfig loclist $ns] if {! [info exists locale] || $locale eq [lindex $loclist 0] } { return [lindex $loclist 0] } # Change loclist set loclist [GetPreferences $locale] set locale [lindex $loclist 0] dict set PackageConfig loclist $ns $loclist # load eventual missing locales set loadedLocales [dict get $PackageConfig loadedlocales $ns] if {$locale in $loadedLocales} { return $locale } set loadLocales [ListComplement $loadedLocales $loclist] dict set PackageConfig loadedlocales $ns\ [concat $loadedLocales $loadLocales] Load $ns $loadLocales return $locale } clear { # Remove all locales not contained in Loclist if {![dict exists $PackageConfig loclist $ns]} { return -code error "clear only when package locale set" } set loclist [dict get $PackageConfig loclist $ns] dict set PackageConfig loadedlocales $ns $loclist if {[dict exists $Msgs $ns]} { foreach locale [dict keys [dict get $Msgs $ns]] { if {$locale ni $loclist} { dict unset Msgs $ns $locale } } } } unset { # unset package locale and restore default locales if { ![dict exists $PackageConfig loclist $ns] } { return } # unset package locale set loadLocales [ListComplement\ [dict get $PackageConfig loadedlocales $ns] $LoadedLocales] dict unset PackageConfig loadedlocales $ns dict unset PackageConfig loclist $ns # unset keys not in global loaded locales if {[dict exists $Msgs $ns]} { foreach locale [dict keys [dict get $Msgs $ns]] { if {$locale ni $LoadedLocales} { dict unset Msgs $ns $locale } } } # Add missing locales Load $ns $loadLocales } default { return -code error "unknown subcommand \"$subcommand\": must be\ clear, get, isset, loaded, present, set, or unset" } } return } # msgcat::mcforgetpackage -- # # Remove any data of the calling package from msgcat # proc msgcat::mcforgetpackage {} { # todo: this may be implemented using an ensemble variable PackageConfig variable Msgs set ns [uplevel 1 {::namespace current}] # Remove MC items dict unset Msgs $ns # Remove config items foreach key [dict keys $PackageConfig] { dict unset PackageConfig $key $ns } return } # msgcat::mcpackageconfig -- # # Get or modify the per caller namespace (e.g. packages) config options. # # Available subcommands are: # # get get the current value or an error if not set. # isset return true, if the option is set # set set the value (see also distinct option). # Returns the number of loaded message files. # unset Clear option. return "". # # Available options are: # # mcfolder # The message catalog folder of the package. # This is automatically set by mcload. # If the value is changed using the set subcommand, an evntual # loadcmd is invoked and all message files of the package locale are # loaded. # # loadcmd # The command gets executed before a message file would be # sourced for this module. # The command is invoked with the expanded locale list to load. # The command is not invoked if the registering package namespace # is not present. # This callback might also be used as an alternative to message # files. # If the value is changed using the set subcommand, the callback is # directly invoked with the current file locale list. No file load is # executed. # # changecmd # The command is invoked, after an executed locale change. # Appended argument is expanded mcpreferences. # # unknowncmd # Use a package locale mcunknown procedure instead the global one. # The appended arguments are identical to mcunknown. # A default unknown handler is used if set to the empty string. # This consists in returning the key if no arguments are given. # With given arguments, format is used to process the arguments. # # Arguments: # subcommand Operation on the package # option The package option to get or set. # ?value? Eventual value for the subcommand # # Results: # Depends on the subcommand and option and is described there proc msgcat::mcpackageconfig {subcommand option {value ""}} { variable PackageConfig # get namespace set ns [uplevel 1 {::namespace current}] if {$option ni {"mcfolder" "loadcmd" "changecmd" "unknowncmd"}} { return -code error "bad option \"$option\": must be mcfolder, loadcmd,\ changecmd, or unknowncmd" } # check if value argument is exactly provided if {[llength [info level 0]] == 4 } { # value provided if {$subcommand in {"get" "isset" "unset"}} { return -code error "wrong # args: should be\ \"[lrange [info level 0] 0 2] value\"" } } elseif {$subcommand eq "set"} { return -code error\ "wrong # args: should be \"[lrange [info level 0] 0 2]\"" } # Execute subcommands switch -exact -- $subcommand { get { # Operation get return current value if {![dict exists $PackageConfig $option $ns]} { return -code error "package option \"$option\" not set" } return [dict get $PackageConfig $option $ns] } isset { return [dict exists $PackageConfig $option $ns] } unset { dict unset PackageConfig $option $ns } set { # Set option if {$option eq "mcfolder"} { set value [file normalize $value] } # Check if changed if { [dict exists $PackageConfig $option $ns] && $value eq [dict get $PackageConfig $option $ns] } { return 0 } # set new value dict set PackageConfig $option $ns $value # Reload pending message catalogs switch -exact -- $option { mcfolder { return [Load $ns [PackageLocales $ns]] } loadcmd { return [Load $ns [PackageLocales $ns] 1] } } return 0 } default { return -code error "unknown subcommand \"$subcommand\":\ must be get, isset, set, or unset" } } return } # msgcat::PackagePreferences -- # # Return eventual present package preferences or the default list if not # present. # # Arguments: # ns Package namespace # # Results: # locale list proc msgcat::PackagePreferences {ns} { variable PackageConfig if {[dict exists $PackageConfig loclist $ns]} { return [dict get $PackageConfig loclist $ns] } variable Loclist return $Loclist } # msgcat::PackageLocales -- # # Return eventual present package locales or the default list if not # present. # # Arguments: # ns Package namespace # # Results: # locale list proc msgcat::PackageLocales {ns} { variable PackageConfig if {[dict exists $PackageConfig loadedlocales $ns]} { return [dict get $PackageConfig loadedlocales $ns] } variable LoadedLocales return $LoadedLocales } # msgcat::ListComplement -- # # Build the complement of two lists. # Return a list with all elements in list2 but not in list1. # Optionally return the intersection. # # Arguments: # list1 excluded list # list2 included list # inlistname If not "", write in this variable the intersection list # # Results: # list with all elements in list2 but not in list1 proc msgcat::ListComplement {list1 list2 {inlistname ""}} { if {"" ne $inlistname} { upvar 1 $inlistname inlist } set inlist {} set outlist {} foreach item $list2 { if {$item in $list1} { lappend inlist $item } else { lappend outlist $item } } return $outlist } # msgcat::mcload -- # # Attempt to load message catalogs for each locale in the # preference list from the specified directory. # # Arguments: # langdir The directory to search. # # Results: # Returns the number of message catalogs that were loaded. proc msgcat::mcload {langdir} { return [uplevel 1 [list\ [namespace origin mcpackageconfig] set mcfolder $langdir]] } # msgcat::LoadAll -- # # Load a list of locales for all packages not having a package locale # list. # # Arguments: # langdir The directory to search. # # Results: # Returns the number of message catalogs that were loaded. proc msgcat::LoadAll {locales} { variable PackageConfig variable LoadedLocales if {0 == [llength $locales]} { return {} } # filter jet unloaded locales set locales [ListComplement $LoadedLocales $locales] if {0 == [llength $locales]} { return {} } lappend LoadedLocales {*}$locales set packages [lsort -unique [concat\ [dict keys [dict get $PackageConfig loadcmd]]\ [dict keys [dict get $PackageConfig mcfolder]]]] foreach ns $packages { if {! [dict exists $PackageConfig loclist $ns] } { Load $ns $locales } } return $locales } # msgcat::Load -- # # Invoke message load callback and load message catalog files. # # Arguments: # ns Namespace (equal package) to load the message catalog. # locales List of locales to load. # callbackonly true if only callback should be invoked # # Results: # Returns the number of message catalogs that were loaded. proc msgcat::Load {ns locales {callbackonly 0}} { variable FileLocale variable PackageConfig variable LoadedLocals if {0 == [llength $locales]} { return 0 } # Invoke callback Invoke loadcmd $locales $ns if {$callbackonly || ![dict exists $PackageConfig mcfolder $ns]} { return 0 } # Invoke file load set langdir [dict get $PackageConfig mcfolder $ns] # Save the file locale if we are recursively called if {[info exists FileLocale]} { set nestedFileLocale $FileLocale } set x 0 foreach p $locales { if {$p eq {}} { set p ROOT } set langfile [file join $langdir $p.msg] if {[file exists $langfile]} { incr x set FileLocale [string tolower\ [file tail [file rootname $langfile]]] if {"root" eq $FileLocale} { set FileLocale "" } namespace inscope $ns [list ::source -encoding utf-8 $langfile] unset FileLocale } } if {[info exists nestedFileLocale]} { set FileLocale $nestedFileLocale } return $x } # msgcat::Invoke -- # # Invoke a set of registered callbacks. # The callback is only invoked, if its registered namespace exists. # # Arguments: # index Index into PackageConfig to get callback command # arglist parameters to the callback invocation # ns (Optional) package to call. # If not given or empty, check all registered packages. # resultname Variable to save the callback result of the last called # callback to. May be set to "" to discard the result. # failerror (0) Fail on error if true. Otherwise call bgerror. # # Results: # Possible values: # - 0: no valid command registered # - 1: registered command was the empty string # - 2: registered command called, resultname is set # - 3: registered command failed # If multiple commands are called, the maximum of all results is returned. proc msgcat::Invoke {index arglist {ns ""} {resultname ""} {failerror 0}} { variable PackageConfig variable Config if {"" ne $resultname} { upvar 1 $resultname result } if {"" eq $ns} { set packageList [dict keys [dict get $PackageConfig $index]] } else { set packageList [list $ns] } set ret 0 foreach ns $packageList { if {[dict exists $PackageConfig $index $ns] && [namespace exists $ns]} { set cmd [dict get $PackageConfig $index $ns] if {"" eq $cmd} { if {$ret == 0} {set ret 1} } else { if {$failerror} { set result [namespace inscope $ns $cmd {*}$arglist] set ret 2 } elseif {1 == [catch { set result [namespace inscope $ns $cmd {*}$arglist] if {$ret < 2} {set ret 2} } err derr]} { after idle [concat [::interp bgerror ""]\ [list $err $derr]] set ret 3 } } } } return $ret } # msgcat::mcset -- # # Set the translation for a given string in a specified locale. # # Arguments: # locale The locale to use. # src The source string. # dest (Optional) The translated string. If omitted, # the source string is used. # # Results: # Returns the new locale. proc msgcat::mcset {locale src {dest ""}} { variable Msgs if {[llength [info level 0]] == 3} { ;# dest not specified set dest $src } set ns [uplevel 1 [list ::namespace current]] set locale [string tolower $locale] dict set Msgs $ns $locale $src $dest return $dest } # msgcat::mcflset -- # # Set the translation for a given string in the current file locale. # # Arguments: # src The source string. # dest (Optional) The translated string. If omitted, # the source string is used. # # Results: # Returns the new locale. proc msgcat::mcflset {src {dest ""}} { variable FileLocale variable Msgs if {![info exists FileLocale]} { return -code error "must only be used inside a message catalog loaded\ with ::msgcat::mcload" } return [uplevel 1 [list [namespace origin mcset] $FileLocale $src $dest]] } # msgcat::mcmset -- # # Set the translation for multiple strings in a specified locale. # # Arguments: # locale The locale to use. # pairs One or more src/dest pairs (must be even length) # # Results: # Returns the number of pairs processed proc msgcat::mcmset {locale pairs} { variable Msgs set length [llength $pairs] if {$length % 2} { return -code error "bad translation list:\ should be \"[lindex [info level 0] 0] locale {src dest ...}\"" } set locale [string tolower $locale] set ns [uplevel 1 [list ::namespace current]] foreach {src dest} $pairs { dict set Msgs $ns $locale $src $dest } return [expr {$length / 2}] } # msgcat::mcflmset -- # # Set the translation for multiple strings in the mc file locale. # # Arguments: # pairs One or more src/dest pairs (must be even length) # # Results: # Returns the number of pairs processed proc msgcat::mcflmset {pairs} { variable FileLocale variable Msgs if {![info exists FileLocale]} { return -code error "must only be used inside a message catalog loaded\ with ::msgcat::mcload" } return [uplevel 1 [list [namespace origin mcmset] $FileLocale $pairs]] } # msgcat::mcunknown -- # # This routine is called by msgcat::mc if a translation cannot # be found for a string and no unknowncmd is set for the current # package. This routine is intended to be replaced # by an application specific routine for error reporting # purposes. The default behavior is to return the source string. # If additional args are specified, the format command will be used # to work them into the traslated string. # # Arguments: # locale The current locale. # src The string to be translated. # args Args to pass to the format command # # Results: # Returns the translated value. proc msgcat::mcunknown {args} { return [uplevel 1 [list [namespace origin DefaultUnknown] {*}$args]] } # msgcat::DefaultUnknown -- # # This routine is called by msgcat::mc if a translation cannot # be found for a string in the following circumstances: # - Default global handler, if mcunknown is not redefined. # - Per package handler, if the package sets unknowncmd to the empty # string. # It returna the source string if the argument list is empty. # If additional args are specified, the format command will be used # to work them into the traslated string. # # Arguments: # locale (unused) The current locale. # src The string to be translated. # args Args to pass to the format command # # Results: # Returns the translated value. proc msgcat::DefaultUnknown {locale src args} { if {[llength $args]} { return [format $src {*}$args] } else { return $src } } # msgcat::mcmax -- # # Calculates the maximum length of the translated strings of the given # list. # # Arguments: # args strings to translate. # # Results: # Returns the length of the longest translated string. proc msgcat::mcmax {args} { set max 0 foreach string $args { set translated [uplevel 1 [list [namespace origin mc] $string]] set len [string length $translated] if {$len>$max} { set max $len } } return $max } # Convert the locale values stored in environment variables to a form # suitable for passing to [mclocale] proc msgcat::ConvertLocale {value} { # Assume $value is of form: $language[_$territory][.$codeset][@modifier] # Convert to form: $language[_$territory][_$modifier] # # Comment out expanded RE version -- bugs alleged # regexp -expanded { # ^ # Match all the way to the beginning # ([^_.@]*) # Match "lanugage"; ends with _, ., or @ # (_([^.@]*))? # Match (optional) "territory"; starts with _ # ([.]([^@]*))? # Match (optional) "codeset"; starts with . # (@(.*))? # Match (optional) "modifier"; starts with @ # $ # Match all the way to the end # } $value -> language _ territory _ codeset _ modifier if {![regexp {^([^_.@]+)(_([^.@]*))?([.]([^@]*))?(@(.*))?$} $value \ -> language _ territory _ codeset _ modifier]} { return -code error "invalid locale '$value': empty language part" } set ret $language if {[string length $territory]} { append ret _$territory } if {[string length $modifier]} { append ret _$modifier } return $ret } # Initialize the default locale proc msgcat::Init {} { global env # # set default locale, try to get from environment # foreach varName {LC_ALL LC_MESSAGES LANG} { if {[info exists env($varName)] && ("" ne $env($varName))} { if {![catch { mclocale [ConvertLocale $env($varName)] }]} { return } } } # # On Darwin, fallback to current CFLocale identifier if available. # if {[info exists ::tcl::mac::locale] && $::tcl::mac::locale ne ""} { if {![catch { mclocale [ConvertLocale $::tcl::mac::locale] }]} { return } } # # The rest of this routine is special processing for Windows or # Cygwin. All other platforms, get out now. # if {([info sharedlibextension] ne ".dll") || [catch {package require registry}]} { mclocale C return } # # On Windows or Cygwin, try to set locale depending on registry # settings, or fall back on locale of "C". # # On Vista and later: # HCU/Control Panel/Desktop : PreferredUILanguages is for language packs, # HCU/Control Pannel/International : localName is the default locale. # # They contain the local string as RFC5646, composed of: # [a-z]{2,3} : language # -[a-z]{4} : script (optional, translated by table Latn->latin) # -[a-z]{2}|[0-9]{3} : territory (optional, numerical region codes not used) # (-.*)* : variant, extension, private use (optional, not used) # Those are translated to local strings. # Examples: de-CH -> de_ch, sr-Latn-CS -> sr_cs@latin, es-419 -> es # foreach key {{HKEY_CURRENT_USER\Control Panel\Desktop} {HKEY_CURRENT_USER\Control Panel\International}}\ value {PreferredUILanguages localeName} { if {![catch {registry get $key $value} localeName] && [regexp {^([a-z]{2,3})(?:-([a-z]{4}))?(?:-([a-z]{2}))?(?:-.+)?$}\ [string tolower $localeName] match locale script territory]} { if {"" ne $territory} { append locale _ $territory } set modifierDict [dict create latn latin cyrl cyrillic] if {[dict exists $modifierDict $script]} { append locale @ [dict get $modifierDict $script] } if {![catch {mclocale [ConvertLocale $locale]}]} { return } } } # then check value locale which contains a numerical language ID if {[catch { set locale [registry get $key "locale"] }]} { mclocale C return } # # Keep trying to match against smaller and smaller suffixes # of the registry value, since the latter hexadigits appear # to determine general language and earlier hexadigits determine # more precise information, such as territory. For example, # 0409 - English - United States # 0809 - English - United Kingdom # Add more translations to the WinRegToISO639 array above. # variable WinRegToISO639 set locale [string tolower $locale] while {[string length $locale]} { if {![catch { mclocale [ConvertLocale [dict get $WinRegToISO639 $locale]] }]} { return } set locale [string range $locale 1 end] } # # No translation known. Fall back on "C" locale # mclocale C } msgcat::Init