Version 10 of msgcat-Ready Script Utility

Updated 2007-02-19 15:28:14 by EKB

EKB This is a small GUI utility for taking an existing script and making it "msgcat-ready".

EKB 19 Feb. 2007 - Please accept my apologies to anyone who tried the GUI version. It had several bugs. I hadn't cleaned up the testing area and thought it had generated the output correctly. Sigh. Live and learn. The version below is fixed and should work as advertised. If it doesn't, please let me know.

(The original command-line version is farther down the page.) It does this by:

  • Looking for each unique string (delimited by "") and asking whether it should be put in the message catalog
  • Creating a new copy of the script with any msgcat-strings wrapped in [mc...]
  • Creating a .msg file (which should be put in a "msgs" subfolder)

The new files also have some commands for loading the msgcat package, importing some msgcat commands and loading the message files.

A limitation is that it will only find strings in quotation marks. Identifying single-word, unquoted strings was too much of a challenge for me. However, if there are only a few unquoted single words (or strings in braces), then this utility should get you most of the way.

Here's what it looks like:

http://www.kb-creative.net/screenshots/makemsgcatGUI.gif

To use it,

  • Type in the language of the original script
  • Click on the "..." button and browse for the script
  • Click on "Get Strings" to fill in the list of strings
  • Check off the strings that need translating
  • Click on the "Run" button to generate the files
  • Look in the same folder/directory as the original script to find the generated files

EKB Fixed a bug 11/11/06 - "newfp" was used (and undefined) in getstrings, where it shouldn't have appeared at all.

Here's the script (NOL - no obligation license):

    ##############################################################
    #
    # (c) 2005 Eric Kemp-Benedict
    #
    # No-obligation license: Use it as you like!
    #
    # Transform a script into a msgcat-ready alternative
    # Wrap strings in [mc ] and export a properly-formatted
    # list.
    #
    ##############################################################

    #------------------------------------------------------
    #
    # A slightly spiced-up checkbutton for convenience
    #
    #------------------------------------------------------

    namespace eval qstring {
        variable strings
        variable vals
        variable len 30
    }

    proc qstring::checkstring {w s} {
        variable len

        frame $w
        checkbutton $w.cb -variable qstring::vals($s) -text $s -bg white \
            -selectcolor white -width $len -anchor w -command "qstring::togglecolor $w.cb"
        set qstring::vals($s) false

        pack $w.cb
    }

    proc qstring::togglecolor {w} {
        if {[$w cget -bg] == "white"} {
            $w config -bg gray80
            $w config -selectcolor gray80
        } else {
            $w config -bg white
            $w config -selectcolor white
        }
    }

    #------------------------------------------------------
    #
    # Create the basic interface
    #
    #------------------------------------------------------

    set deflang "en_US"

    # Specify language
    frame .lang
    pack .lang -side top -anchor w
    label .lang.l -text "Starting Language: "
    pack .lang.l -side left
    entry .lang.e -textvariable deflang -width 10
    pack .lang.e -side left

    # Browse for file
    frame .file
    pack .file -side top -anchor w
    entry .file.e -textvariable infile -width 50
    pack .file.e -side left
    button .file.browse -text "..." -command findfile
    pack .file.browse -side left
    button .file.getstr -text "Get Strings" -command getstrings
    pack .file.getstr -side left

    proc findfile {} {
        set ::infile [tk_getOpenFile]
    }

    # Run!
    frame .run
    pack .run -side top
    button .run.b -text "Run" -command run
    pack .run.b

    # A place to put strings
    frame .strings -height 300
    pack .strings -side bottom -fill both -expand yes
    text .strings.l -yscrollcommand {.strings.sb set} -cursor arrow -width 65
    pack .strings.l -side left -fill both -expand yes
    scrollbar .strings.sb -command {.strings.l yview}
    pack .strings.sb -side right -fill y

    proc getstrings {} {
        variable strings

        set infp [open $::infile r]

        while {[gets $infp currline] != -1} {
            # Is this a comment? Just keep going
            if {[regexp -- "^\s*#" $currline] == 1} {
                continue
            }
            set splitline [split $currline "\""]
            set isstring false
            foreach fragment $splitline {
                if {$isstring} {
                    if [info exists ignore($fragment)] {
                        set isstring false
                        continue
                    }
                    set strings($fragment) 1
                    set isstring false
                } else {
                    set isstring true
                }
            }
        }
        close $infp

        set qstring::len 60
        set i 0
        foreach s [array names strings] {
            qstring::checkstring .strings.l.s$i $s
            .strings.l window create end -window .strings.l.s$i
            incr i
        }
    }

    proc run {} {
        variable strings

        foreach s [array names qstring::vals] {
            if {!$qstring::vals($s)} {
                set ignore($s) 1
            }
        }

        set newfile "[file rootname $::infile]_$::deflang.tcl"
        set mcatfile "$::deflang.msg"

        set infp [open $::infile r]
        set newfp [open $newfile w]

        puts $newfp "package require msgcat"
        puts $newfp "namespace import msgcat::mc"
        puts $newfp "msgcat::mclocale $::deflang"
        # NOTE: Put the .msg files into a "msgs" folder under the script
        puts $newfp {msgcat::mcload [file join [file dirname [info script]] msgs]}
        puts $newfp ""

        while {[gets $infp currline] != -1} {
            # Is this a comment? Print it and just keep going
            if {[regexp -- "^\s*#" $currline] == 1} {
                puts $newfp $currline
                continue
            }
            set splitline [split $currline "\""]
            set outline ""
            # Assume (!!) first part of the line is not a quote.
            set isstring false
            foreach fragment $splitline {
                if {$isstring} {
                    # If not ignoring, then it's an acceptable string
                    if [info exists ignore($fragment)] {
                        set outline "$outline\"$fragment\""
                    } else {
                        set outline "$outline\[mc \"$fragment\"\]"
                    }
                    # Toggle - next fragment not a string
                    set isstring false
                } else {
                    set outline "$outline$fragment"
                    set isstring true
                }
            }
            puts $newfp $outline
        }

        close $infp
        close $newfp

        set mcfp [open $mcatfile w]

        puts $mcfp "namespace import -force msgcat::mcset"
        puts $mcfp ""
        foreach mcstring [lsort [array names strings]] {
            if {![info exists ignore($mcstring)]} {
                puts $mcfp "mcset $::deflang \"$mcstring\" \"$mcstring\""
            }
        }

        close $mcfp
    }

Here's an example. I took RS's A little sluice simulation and passed it through. Choosing only the strings that needed translating, this gave a default (English-language) .msg file (which I saved under a "msgs" folder):

 namespace import -force msgcat::mcset

 mcset en_US "Can't open gate - water not level" "Can't open gate - water not level"
 mcset en_US "Can't open valve when gate still open" "Can't open valve when gate still open"
 mcset en_US "Welcome to the sluice simulation! (Hint: open the right valve)" "Welcome to the sluice simulation! (Hint: open the right valve)"
 mcset en_US "sluice simulator" "sluice simulator"

and a new script:

 package require msgcat
 namespace import msgcat::mc
 msgcat::mclocale en_US
 msgcat::mcload [file join [file dirname [info script]] msgs]

 wm title . [mc "sluice simulator"]
 pack [label .info -textvar info -anchor w] -side bottom -fill x
 set info [mc "Welcome to the sluice simulation! (Hint: open the right valve)"]
 pack [canvas .c -width 600 -height 280 -bg lightblue]
 .c create polygon 0 300  0 90  450 90  600 120  600 300 -fill green3
 .c create polygon 140 300 140 80 460 80 460 300 -fill grey
 .c create polygon 150 250 150 80 450 80 450 250 -fill grey60
 .c create rect 150 80 153 150 -fill brown -tag gate1
 set isOpen(gate1) 0
 .c bind gate1 <1> {toggleGate .c gate1}
 .c create rect 450 80 453 250 -fill brown -tag gate2
 set isOpen(gate2) 0
 .c bind gate2 <1> {toggleGate .c gate2}
 .c create polygon 0 152 0 100 150 100 150 152 -fill blue1\
    -tag {water upriver} -stipple gray50
 .c create polygon 452 250 452 200 600 200 600 250 -fill blue1 \
    -stipple gray50 -tag {water downriver}
 .c create polygon 150 100 150 250 452 250 452 100 -fill blue1 \
    -tag {water sluicewater sluiced} -stipple gray50
 .c create line 90 150 90 160 100 170 150 170 -width 5 -fill blue1 \
    -smooth 1 -tag water
 .c create polygon 140 290 140 250 460 250 460 290 -fill grey -tag water
 .c create oval 110 160 130 180 -fill white -tag {valve1 water}
 .c create rect 118 160 122 180 -fill grey -tag {valve1 valve1r water}
 set isOpen(valve1r) 0
 .c bind valve1 <1> {toggleValve .c valve1r}
 .c create line 420 250 420 260 430 270 480 270 490 265 490 250 \
    -width 5 -fill blue1 -smooth 1 -tag fg
 .c create oval 450 260 470 280 -fill white -tag valve2
 .c create rect 458 260 462 280 -fill grey -tag {valve2 valve2r}
 set isOpen(valve2r) 0
 .c bind valve2 <1> {toggleValve .c valve2r}

 proc boat {w} {
    $w create poly 10 90 10 77 50 77 50 90 -fill red -tag boat
    $w create rect 8 78 52 75  -fill grey -tag boat
    $w create rect 13 86 23 79 -fill white -tag boat
    $w create rect 28 86 38 79 -fill white -tag boat
    $w create poly 0 90  0 95  203 95  205 90 -fill white -tag boat
    $w create poly 0 95  0 125  5 130  200 130  203 95 \
        -fill black -tag boat
    $w create poly 50 90 90 80 130 90 160 80 200 90\
        -fill bisque -outline black -tag boat
    $w move boat 160 0
    $w lower boat water
    set ::moveBoat 0
    set ::boatDirection 1
 }
 boat .c
 proc toggleGate {w tag} {
    global info isOpen moveBoat boatDirection
    if { $tag=="gate1" && [maxy $w sluicewater]>[maxy $w upriver] \
       ||$tag=="gate2" && [maxy $w sluicewater]<[maxy $w downriver]} {
           set info [mc "Can't open gate - water not level"]
           return
    }
    foreach {x0 y0 x1 y1} [$w coords $tag] break
    set x0 [expr {$x0 + ($isOpen($tag)? 50 : -50)}]
    $w coords $tag $x0 $y0 $x1 $y1
    set isOpen($tag) [expr {1-$isOpen($tag)}]
    set info "$tag [expr {$isOpen($tag)? {opened} : {closed}}]"
    foreach {bx0 by0 bx1 by1} [$w bbox boat] break
    if {$bx1<100*$boatDirection || $bx0<460*$boatDirection} {
        set moveBoat 0
    }
    if {$isOpen($tag)} {set moveBoat [expr $boatDirection*2]}
 }
 proc toggleValve {w tag} {
    global isOpen
    if {!$isOpen($tag) && ($isOpen(gate1) || $isOpen(gate2))} {
        set ::info [mc "Can't open valve when gate still open"]
        return
    }
    foreach {x0 y0 x1 y1} [$w coords $tag] break
    set dx2 [expr {($x1-$x0)/2.}]
    set mx  [expr {($x0+$x1)/2}]
    set dy2 [expr {($y1-$y0)/2.}]
    set my  [expr {($y0+$y1)/2}]
    set isOpen($tag) [expr {$dx2<$dy2}]
    $w itemconfig $tag -fill [expr {$isOpen($tag)? "blue1": "grey"}]
    $w coords $tag [expr {$mx-$dy2}] [expr {$my-$dx2}] \
                 [expr {$mx+$dy2}] [expr {$my+$dx2}]
    set ::info "$tag [expr {$::isOpen($tag)? {opened} : {closed}}]"
 }
 proc every {ms body} {eval $body; after $ms [info level 0]}
 proc maxy {w tag} {lindex [$w bbox $tag] 1}
 proc animate {w} {
    global moveBoat isOpen
    foreach {bx0 by0 bx1 by1} [$w bbox boat]        break
    foreach {sx0 top sx1 sy1} [$w bbox sluicewater] break
    if {$bx0 > $sx0 && $bx1 < $sx1} {
        $w addtag sluiced withtag boat
        if {$bx1>390 && $bx0<460 && $moveBoat>0 && !$isOpen(gate2)} {
            set moveBoat 0
        }
        if {$bx0<160 && $bx1>90 && $moveBoat<0 && !$isOpen(gate1)} {
            set moveBoat 0
        }
    } else {
        $w dtag boat sluiced
        if {$bx0<470 && $bx0>150 && $moveBoat<0 && !$isOpen(gate2) \
          || $bx1>100 && $bx1<450 && $moveBoat>0 && !$isOpen(gate1)} {
            set moveBoat 0
        }
    }
    if {$top<[maxy $w downriver] && $isOpen(valve2r)} {
        $w move sluiced 0 1
        set moveBoat 0
    }
    if {$top>[maxy $w upriver] && $isOpen(valve1r)} {
        $w move sluiced 0 -1
        set moveBoat 0
    }
    $w move boat $moveBoat 0
    if {$bx0>700} {
        if {rand()>0.5} {
            $w scale boat [expr {($bx0+$bx1)/2}] $by0 -1 1
            set moveBoat -2; set ::boatDirection -1
        } else {$w move boat -1000 -100}
    }
    if {$bx0<-300} {
        if {rand()>0.5} {
            $w scale boat [expr {($bx0+$bx1)/2}] $by0 -1 1
            set moveBoat 2; set ::boatDirection 1
        } else {$w move boat 1000 100}
    }
 }
 every 100 {animate .c}
 wm resizable . 0 0

Next I made a translation in my not-so-good French (I really really don't know if I got the right translation for "sluice" in this case) and saved it as "fr.msg" in a "msgs" folder:

 namespace import -force msgcat::mcset

 mcset fr "Can't open gate - water not level" "Impossible d'ouvrir la porte - le niveau de l'eau n'est pas le même"
 mcset fr "Can't open valve when gate still open" "Impossible d'ouvre la valve lorsque la porte est encore ouverte"
 mcset fr "Welcome to the sluice simulation! (Hint: open the right valve)" "Bienvenu à la simulation de l'écluse! (Suggestion: ouvrez la valve à droite)"
 mcset fr "sluice simulator" "simulation de l'écluse"

Then in the main script I set "msgcat::locale" to "fr" and ran the program. Here's the result:

http://www.kb-creative.net/screenshots/sluicesim_fr.gif


Command-Line Version Here's the script for the command-line version:

    # Transform a script into a msgcat-ready alternative
    # Wrap strings in [mc ] and export a properly-formatted
    # list.

    set deflang "en_US"

    switch $argc {
        1 {
            set infile [lindex $argv 0]
        }
        3 {
            # Allow either "-l lang file" or "file -l lang"
            for {set i 0} {$i < 3} {incr i} {set cmd$i [lindex $argv $i]}
            if {$cmd0 == "-l"} {
                set $deflang $cmd1
                set infile $cmd2
            } else {
                # In this case, cmd1 ought to be -l
                set deflang $cmd2
                set infile $cmd0
            }
        }
        default {
            set me [file tail $argv0]
            puts "Usage:"
            puts "\n\ttclsh $me \[-l language\] file"
            puts "\n$me helps build a message catalog from an existing script."
            exit
        }
    }

    set newfile "[file rootname $infile]_$deflang.tcl"
    set mcatfile "$deflang.msg"

    set infp [open $infile r]
    set newfp [open $newfile w]

    puts $newfp "package require msgcat"
    puts $newfp "namespace import msgcat::mc"
    puts $newfp "msgcat::mclocale $deflang"
    # NOTE: Put the .msg files into a "msgs" folder under the script
    puts $newfp {msgcat::mcload [file join [file dirname [info script]] msgs]}
    puts $newfp ""

    while {[gets $infp currline] != -1} {
        # Is this a comment? Print it and just keep going
        if {[regexp -- "^\s*#" $currline] == 1} {
            puts $newfp $currline
            continue
        }
        set splitline [split $currline "\""]
        set outline ""
        # Assume (!!) first part of the line is not a quote.
        set isstring false
        foreach fragment $splitline {
            if {$isstring} {
                if [info exists ignore($fragment)] {
                    set isstring false
                    set outline "$outline\"$fragment\""
                    continue
                }
                puts -nonewline stdout "\"$fragment\"? "
                flush stdout
                if {[regexp -nocase -- "^\s*y" [gets stdin]] == 1} {
                    set outline "$outline\[mc \"$fragment\"\]"
                    # Store strings in an array -- won't duplicate
                    set strings($fragment) 1
                } else {
                    set ignore($fragment) 1
                    set outline "$outline\"$fragment\""
                }
                set isstring false
            } else {
                set outline "$outline$fragment"
                set isstring true
            }
        }
        puts $newfp $outline
    }

    close $infp
    close $newfp

    set mcfp [open $mcatfile w]

    puts $mcfp "namespace import -force msgcat::mcset"
    puts $mcfp ""
    foreach mcstring [lsort [array names strings]] {
        puts $mcfp "mcset $deflang \"$mcstring\" \"$mcstring\""
    }

    close $mcfp

MG Without having actually tried this, I think it's a really nice idea. Personally, I never code for msgcat-translations when I write a script (apart from in menus, for some reason) and have to go through and find it all afterwards to translate, if it's needed. One (very minor) criticism/suggestion, having looked at it briefly, though - you should be able to pass the language on the command line, as well as the file name to fix, IMHO (with it still defaulting to US English), to save having to constantly edit this script.

EKB Thanks! I agree that it needs to accept the language. I've modified the script above so that it accepts input like (assuming the script is saved as mkmsgcat.tcl):

 % tclsh mkmsgcat.tcl -l de myfile.tcl

or

 % tclsh mkmsgcat.tcl myfile.tcl -l de

Category Local