1211 lines
33 KiB
Tcl
1211 lines
33 KiB
Tcl
# 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 Harald Oehlmann.
|
|
# Copyright (c) 1998-2000 Ajuba Solutions.
|
|
# Copyright (c) 1998 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
|