[CalendarServer-changes] [3325] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Nov 5 17:45:14 PST 2008


Revision: 3325
          http://trac.macosforge.org/projects/calendarserver/changeset/3325
Author:   sagen at apple.com
Date:     2008-11-05 17:45:12 -0800 (Wed, 05 Nov 2008)
Log Message:
-----------
Landing localized emails branch

Modified Paths:
--------------
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/locales/en/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/locales/en/LC_MESSAGES/calendarserver.po
    CalendarServer/trunk/twistedcaldav/config.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/mail.py
    CalendarServer/trunk/twistedcaldav/test/test_mail.py

Added Paths:
-----------
    CalendarServer/trunk/locales/
    CalendarServer/trunk/locales/en/
    CalendarServer/trunk/locales/en/LC_MESSAGES/
    CalendarServer/trunk/support/msgfmt.py
    CalendarServer/trunk/support/pygettext.py
    CalendarServer/trunk/twistedcaldav/test/data/locales/
    CalendarServer/trunk/twistedcaldav/test/data/locales/en/
    CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po
    CalendarServer/trunk/twistedcaldav/test/data/locales/pig/
    CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po
    CalendarServer/trunk/twistedcaldav/test/test_localization.py

Removed Paths:
-------------
    CalendarServer/trunk/locales/en/
    CalendarServer/trunk/locales/en/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/test/data/locales/en/
    CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po
    CalendarServer/trunk/twistedcaldav/test/data/locales/pig/
    CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/
    CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
    CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po

Property Changed:
----------------
    CalendarServer/trunk/locales/en/LC_MESSAGES/calendarserver.mo

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2008-11-06 01:28:37 UTC (rev 3324)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2008-11-06 01:45:12 UTC (rev 3325)
@@ -638,6 +638,13 @@
       <string>logs/caldavd-pydir.sock</string>
     </dict>
 
+    <key>Localization</key>
+    <dict>
+      <key>LocalesDirectory</key>
+      <string>locales</string>
+      <key>Language</key>
+      <string>en</string>
+    </dict>
 
   </dict>
 </plist>

Modified: CalendarServer/trunk/locales/en/LC_MESSAGES/calendarserver.mo
===================================================================
(Binary files differ)

Copied: CalendarServer/trunk/support/msgfmt.py (from rev 3323, CalendarServer/branches/users/sagen/localization-3308/support/msgfmt.py)
===================================================================
--- CalendarServer/trunk/support/msgfmt.py	                        (rev 0)
+++ CalendarServer/trunk/support/msgfmt.py	2008-11-06 01:45:12 UTC (rev 3325)
@@ -0,0 +1,203 @@
+#! /usr/bin/env python
+# -*- coding: iso-8859-1 -*-
+# Written by Martin v. L\xF6wis <loewis at informatik.hu-berlin.de>
+
+"""Generate binary message catalog from textual translation description.
+
+This program converts a textual Uniforum-style message catalog (.po file) into
+a binary GNU catalog (.mo file).  This is essentially the same function as the
+GNU msgfmt program, however, it is a simpler implementation.
+
+Usage: msgfmt.py [OPTIONS] filename.po
+
+Options:
+    -o file
+    --output-file=file
+        Specify the output file to write to.  If omitted, output will go to a
+        file named filename.mo (based off the input file name).
+
+    -h
+    --help
+        Print this message and exit.
+
+    -V
+    --version
+        Display version information and exit.
+"""
+
+import sys
+import os
+import getopt
+import struct
+import array
+
+__version__ = "1.1"
+
+MESSAGES = {}
+
+
+
+def usage(code, msg=''):
+    print >> sys.stderr, __doc__
+    if msg:
+        print >> sys.stderr, msg
+    sys.exit(code)
+
+
+
+def add(id, str, fuzzy):
+    "Add a non-fuzzy translation to the dictionary."
+    global MESSAGES
+    if not fuzzy and str:
+        MESSAGES[id] = str
+
+
+
+def generate():
+    "Return the generated output."
+    global MESSAGES
+    keys = MESSAGES.keys()
+    # the keys are sorted in the .mo file
+    keys.sort()
+    offsets = []
+    ids = strs = ''
+    for id in keys:
+        # For each string, we need size and file offset.  Each string is NUL
+        # terminated; the NUL does not count into the size.
+        offsets.append((len(ids), len(id), len(strs), len(MESSAGES[id])))
+        ids += id + '\0'
+        strs += MESSAGES[id] + '\0'
+    output = ''
+    # The header is 7 32-bit unsigned integers.  We don't use hash tables, so
+    # the keys start right after the index tables.
+    # translated string.
+    keystart = 7*4+16*len(keys)
+    # and the values start after the keys
+    valuestart = keystart + len(ids)
+    koffsets = []
+    voffsets = []
+    # The string table first has the list of keys, then the list of values.
+    # Each entry has first the size of the string, then the file offset.
+    for o1, l1, o2, l2 in offsets:
+        koffsets += [l1, o1+keystart]
+        voffsets += [l2, o2+valuestart]
+    offsets = koffsets + voffsets
+    output = struct.pack("Iiiiiii",
+                         0x950412deL,       # Magic
+                         0,                 # Version
+                         len(keys),         # # of entries
+                         7*4,               # start of key index
+                         7*4+len(keys)*8,   # start of value index
+                         0, 0)              # size and offset of hash table
+    output += array.array("i", offsets).tostring()
+    output += ids
+    output += strs
+    return output
+
+
+
+def make(filename, outfile):
+    ID = 1
+    STR = 2
+
+    # Compute .mo name from .po name and arguments
+    if filename.endswith('.po'):
+        infile = filename
+    else:
+        infile = filename + '.po'
+    if outfile is None:
+        outfile = os.path.splitext(infile)[0] + '.mo'
+
+    try:
+        lines = open(infile).readlines()
+    except IOError, msg:
+        print >> sys.stderr, msg
+        sys.exit(1)
+
+    section = None
+    fuzzy = 0
+
+    # Parse the catalog
+    lno = 0
+    for l in lines:
+        lno += 1
+        # If we get a comment line after a msgstr, this is a new entry
+        if l[0] == '#' and section == STR:
+            add(msgid, msgstr, fuzzy)
+            section = None
+            fuzzy = 0
+        # Record a fuzzy mark
+        if l[:2] == '#,' and 'fuzzy' in l:
+            fuzzy = 1
+        # Skip comments
+        if l[0] == '#':
+            continue
+        # Now we are in a msgid section, output previous section
+        if l.startswith('msgid'):
+            if section == STR:
+                add(msgid, msgstr, fuzzy)
+            section = ID
+            l = l[5:]
+            msgid = msgstr = ''
+        # Now we are in a msgstr section
+        elif l.startswith('msgstr'):
+            section = STR
+            l = l[6:]
+        # Skip empty lines
+        l = l.strip()
+        if not l:
+            continue
+        # XXX: Does this always follow Python escape semantics?
+        l = eval(l)
+        if section == ID:
+            msgid += l
+        elif section == STR:
+            msgstr += l
+        else:
+            print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
+                  'before:'
+            print >> sys.stderr, l
+            sys.exit(1)
+    # Add last entry
+    if section == STR:
+        add(msgid, msgstr, fuzzy)
+
+    # Compute output
+    output = generate()
+
+    try:
+        open(outfile,"wb").write(output)
+    except IOError,msg:
+        print >> sys.stderr, msg
+
+
+
+def main():
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], 'hVo:',
+                                   ['help', 'version', 'output-file='])
+    except getopt.error, msg:
+        usage(1, msg)
+
+    outfile = None
+    # parse options
+    for opt, arg in opts:
+        if opt in ('-h', '--help'):
+            usage(0)
+        elif opt in ('-V', '--version'):
+            print >> sys.stderr, "msgfmt.py", __version__
+            sys.exit(0)
+        elif opt in ('-o', '--output-file'):
+            outfile = arg
+    # do it
+    if not args:
+        print >> sys.stderr, 'No input file given'
+        print >> sys.stderr, "Try `msgfmt --help' for more information."
+        return
+
+    for filename in args:
+        make(filename, outfile)
+
+
+if __name__ == '__main__':
+    main()

Copied: CalendarServer/trunk/support/pygettext.py (from rev 3323, CalendarServer/branches/users/sagen/localization-3308/support/pygettext.py)
===================================================================
--- CalendarServer/trunk/support/pygettext.py	                        (rev 0)
+++ CalendarServer/trunk/support/pygettext.py	2008-11-06 01:45:12 UTC (rev 3325)
@@ -0,0 +1,669 @@
+#! /usr/bin/env python
+# -*- coding: iso-8859-1 -*-
+# Originally written by Barry Warsaw <barry at zope.com>
+#
+# Minimally patched to make it even more xgettext compatible
+# by Peter Funk <pf at artcom-gmbh.de>
+#
+# 2002-11-22 J\xFCrgen Hermann <jh at web.de>
+# Added checks that _() only contains string literals, and
+# command line args are resolved to module lists, i.e. you
+# can now pass a filename, a module or package name, or a
+# directory (including globbing chars, important for Win32).
+# Made docstring fit in 80 chars wide displays using pydoc.
+#
+
+# for selftesting
+try:
+    import fintl
+    _ = fintl.gettext
+except ImportError:
+    _ = lambda s: s
+
+__doc__ = _("""pygettext -- Python equivalent of xgettext(1)
+
+Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the
+internationalization of C programs. Most of these tools are independent of
+the programming language and can be used from within Python programs.
+Martin von Loewis' work[1] helps considerably in this regard.
+
+There's one problem though; xgettext is the program that scans source code
+looking for message strings, but it groks only C (or C++). Python
+introduces a few wrinkles, such as dual quoting characters, triple quoted
+strings, and raw strings. xgettext understands none of this.
+
+Enter pygettext, which uses Python's standard tokenize module to scan
+Python source code, generating .pot files identical to what GNU xgettext[2]
+generates for C and C++ code. From there, the standard GNU tools can be
+used.
+
+A word about marking Python strings as candidates for translation. GNU
+xgettext recognizes the following keywords: gettext, dgettext, dcgettext,
+and gettext_noop. But those can be a lot of text to include all over your
+code. C and C++ have a trick: they use the C preprocessor. Most
+internationalized C source includes a #define for gettext() to _() so that
+what has to be written in the source is much less. Thus these are both
+translatable strings:
+
+    gettext("Translatable String")
+    _("Translatable String")
+
+Python of course has no preprocessor so this doesn't work so well.  Thus,
+pygettext searches only for _() by default, but see the -k/--keyword flag
+below for how to augment this.
+
+ [1] http://www.python.org/workshops/1997-10/proceedings/loewis.html
+ [2] http://www.gnu.org/software/gettext/gettext.html
+
+NOTE: pygettext attempts to be option and feature compatible with GNU
+xgettext where ever possible. However some options are still missing or are
+not fully implemented. Also, xgettext's use of command line switches with
+option arguments is broken, and in these cases, pygettext just defines
+additional switches.
+
+Usage: pygettext [options] inputfile ...
+
+Options:
+
+    -a
+    --extract-all
+        Extract all strings.
+
+    -d name
+    --default-domain=name
+        Rename the default output file from messages.pot to name.pot.
+
+    -E
+    --escape
+        Replace non-ASCII characters with octal escape sequences.
+
+    -D
+    --docstrings
+        Extract module, class, method, and function docstrings.  These do
+        not need to be wrapped in _() markers, and in fact cannot be for
+        Python to consider them docstrings. (See also the -X option).
+
+    -h
+    --help
+        Print this help message and exit.
+
+    -k word
+    --keyword=word
+        Keywords to look for in addition to the default set, which are:
+        %(DEFAULTKEYWORDS)s
+
+        You can have multiple -k flags on the command line.
+
+    -K
+    --no-default-keywords
+        Disable the default set of keywords (see above).  Any keywords
+        explicitly added with the -k/--keyword option are still recognized.
+
+    --no-location
+        Do not write filename/lineno location comments.
+
+    -n
+    --add-location
+        Write filename/lineno location comments indicating where each
+        extracted string is found in the source.  These lines appear before
+        each msgid.  The style of comments is controlled by the -S/--style
+        option.  This is the default.
+
+    -o filename
+    --output=filename
+        Rename the default output file from messages.pot to filename.  If
+        filename is `-' then the output is sent to standard out.
+
+    -p dir
+    --output-dir=dir
+        Output files will be placed in directory dir.
+
+    -S stylename
+    --style stylename
+        Specify which style to use for location comments.  Two styles are
+        supported:
+
+        Solaris  # File: filename, line: line-number
+        GNU      #: filename:line
+
+        The style name is case insensitive.  GNU style is the default.
+
+    -v
+    --verbose
+        Print the names of the files being processed.
+
+    -V
+    --version
+        Print the version of pygettext and exit.
+
+    -w columns
+    --width=columns
+        Set width of output to columns.
+
+    -x filename
+    --exclude-file=filename
+        Specify a file that contains a list of strings that are not be
+        extracted from the input files.  Each string to be excluded must
+        appear on a line by itself in the file.
+
+    -X filename
+    --no-docstrings=filename
+        Specify a file that contains a list of files (one per line) that
+        should not have their docstrings extracted.  This is only useful in
+        conjunction with the -D option above.
+
+If `inputfile' is -, standard input is read.
+""")
+
+import os
+import imp
+import sys
+import glob
+import time
+import getopt
+import token
+import tokenize
+import operator
+
+__version__ = '1.5'
+
+default_keywords = ['_']
+DEFAULTKEYWORDS = ', '.join(default_keywords)
+
+EMPTYSTRING = ''
+
+
+
+# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
+# there.
+pot_header = _('''\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\\n"
+"POT-Creation-Date: %(time)s\\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
+"Last-Translator: FULL NAME <EMAIL at ADDRESS>\\n"
+"Language-Team: LANGUAGE <LL at li.org>\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=CHARSET\\n"
+"Content-Transfer-Encoding: ENCODING\\n"
+"Generated-By: pygettext.py %(version)s\\n"
+
+''')
+
+
+def usage(code, msg=''):
+    print >> sys.stderr, __doc__ % globals()
+    if msg:
+        print >> sys.stderr, msg
+    sys.exit(code)
+
+
+
+escapes = []
+
+def make_escapes(pass_iso8859):
+    global escapes
+    if pass_iso8859:
+        # Allow iso-8859 characters to pass through so that e.g. 'msgid
+        # "H\xF6he"' would result not result in 'msgid "H\366he"'.  Otherwise we
+        # escape any character outside the 32..126 range.
+        mod = 128
+    else:
+        mod = 256
+    for i in range(256):
+        if 32 <= (i % mod) <= 126:
+            escapes.append(chr(i))
+        else:
+            escapes.append("\\%03o" % i)
+    escapes[ord('\\')] = '\\\\'
+    escapes[ord('\t')] = '\\t'
+    escapes[ord('\r')] = '\\r'
+    escapes[ord('\n')] = '\\n'
+    escapes[ord('\"')] = '\\"'
+
+
+def escape(s):
+    global escapes
+    s = list(s)
+    for i in range(len(s)):
+        s[i] = escapes[ord(s[i])]
+    return EMPTYSTRING.join(s)
+
+
+def safe_eval(s):
+    # unwrap quotes, safely
+    return eval(s, {'__builtins__':{}}, {})
+
+
+def normalize(s):
+    # This converts the various Python string types into a format that is
+    # appropriate for .po files, namely much closer to C style.
+    lines = s.split('\n')
+    if len(lines) == 1:
+        s = '"' + escape(s) + '"'
+    else:
+        if not lines[-1]:
+            del lines[-1]
+            lines[-1] = lines[-1] + '\n'
+        for i in range(len(lines)):
+            lines[i] = escape(lines[i])
+        lineterm = '\\n"\n"'
+        s = '""\n"' + lineterm.join(lines) + '"'
+    return s
+
+
+def containsAny(str, set):
+    """Check whether 'str' contains ANY of the chars in 'set'"""
+    return 1 in [c in str for c in set]
+
+
+def _visit_pyfiles(list, dirname, names):
+    """Helper for getFilesForName()."""
+    # get extension for python source files
+    if not globals().has_key('_py_ext'):
+        global _py_ext
+        _py_ext = [triple[0] for triple in imp.get_suffixes()
+                   if triple[2] == imp.PY_SOURCE][0]
+
+    # don't recurse into CVS directories
+    if 'CVS' in names:
+        names.remove('CVS')
+
+    # add all *.py files to list
+    list.extend(
+        [os.path.join(dirname, file) for file in names
+         if os.path.splitext(file)[1] == _py_ext]
+        )
+
+
+def _get_modpkg_path(dotted_name, pathlist=None):
+    """Get the filesystem path for a module or a package.
+
+    Return the file system path to a file for a module, and to a directory for
+    a package. Return None if the name is not found, or is a builtin or
+    extension module.
+    """
+    # split off top-most name
+    parts = dotted_name.split('.', 1)
+
+    if len(parts) > 1:
+        # we have a dotted path, import top-level package
+        try:
+            file, pathname, description = imp.find_module(parts[0], pathlist)
+            if file: file.close()
+        except ImportError:
+            return None
+
+        # check if it's indeed a package
+        if description[2] == imp.PKG_DIRECTORY:
+            # recursively handle the remaining name parts
+            pathname = _get_modpkg_path(parts[1], [pathname])
+        else:
+            pathname = None
+    else:
+        # plain name
+        try:
+            file, pathname, description = imp.find_module(
+                dotted_name, pathlist)
+            if file:
+                file.close()
+            if description[2] not in [imp.PY_SOURCE, imp.PKG_DIRECTORY]:
+                pathname = None
+        except ImportError:
+            pathname = None
+
+    return pathname
+
+
+def getFilesForName(name):
+    """Get a list of module files for a filename, a module or package name,
+    or a directory.
+    """
+    if not os.path.exists(name):
+        # check for glob chars
+        if containsAny(name, "*?[]"):
+            files = glob.glob(name)
+            list = []
+            for file in files:
+                list.extend(getFilesForName(file))
+            return list
+
+        # try to find module or package
+        name = _get_modpkg_path(name)
+        if not name:
+            return []
+
+    if os.path.isdir(name):
+        # find all python files in directory
+        list = []
+        os.path.walk(name, _visit_pyfiles, list)
+        return list
+    elif os.path.exists(name):
+        # a single file
+        return [name]
+
+    return []
+
+
+class TokenEater:
+    def __init__(self, options):
+        self.__options = options
+        self.__messages = {}
+        self.__state = self.__waiting
+        self.__data = []
+        self.__lineno = -1
+        self.__freshmodule = 1
+        self.__curfile = None
+
+    def __call__(self, ttype, tstring, stup, etup, line):
+        # dispatch
+##        import token
+##        print >> sys.stderr, 'ttype:', token.tok_name[ttype], \
+##              'tstring:', tstring
+        self.__state(ttype, tstring, stup[0])
+
+    def __waiting(self, ttype, tstring, lineno):
+        opts = self.__options
+        # Do docstring extractions, if enabled
+        if opts.docstrings and not opts.nodocstrings.get(self.__curfile):
+            # module docstring?
+            if self.__freshmodule:
+                if ttype == tokenize.STRING:
+                    self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
+                    self.__freshmodule = 0
+                elif ttype not in (tokenize.COMMENT, tokenize.NL):
+                    self.__freshmodule = 0
+                return
+            # class docstring?
+            if ttype == tokenize.NAME and tstring in ('class', 'def'):
+                self.__state = self.__suiteseen
+                return
+        if ttype == tokenize.NAME and tstring in opts.keywords:
+            self.__state = self.__keywordseen
+
+    def __suiteseen(self, ttype, tstring, lineno):
+        # ignore anything until we see the colon
+        if ttype == tokenize.OP and tstring == ':':
+            self.__state = self.__suitedocstring
+
+    def __suitedocstring(self, ttype, tstring, lineno):
+        # ignore any intervening noise
+        if ttype == tokenize.STRING:
+            self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
+            self.__state = self.__waiting
+        elif ttype not in (tokenize.NEWLINE, tokenize.INDENT,
+                           tokenize.COMMENT):
+            # there was no class docstring
+            self.__state = self.__waiting
+
+    def __keywordseen(self, ttype, tstring, lineno):
+        if ttype == tokenize.OP and tstring == '(':
+            self.__data = []
+            self.__lineno = lineno
+            self.__state = self.__openseen
+        else:
+            self.__state = self.__waiting
+
+    def __openseen(self, ttype, tstring, lineno):
+        if ttype == tokenize.OP and tstring == ')':
+            # We've seen the last of the translatable strings.  Record the
+            # line number of the first line of the strings and update the list
+            # of messages seen.  Reset state for the next batch.  If there
+            # were no strings inside _(), then just ignore this entry.
+            if self.__data:
+                self.__addentry(EMPTYSTRING.join(self.__data))
+            self.__state = self.__waiting
+        elif ttype == tokenize.STRING:
+            self.__data.append(safe_eval(tstring))
+        elif ttype not in [tokenize.COMMENT, token.INDENT, token.DEDENT,
+                           token.NEWLINE, tokenize.NL]:
+            # warn if we see anything else than STRING or whitespace
+            print >> sys.stderr, _(
+                '*** %(file)s:%(lineno)s: Seen unexpected token "%(token)s"'
+                ) % {
+                'token': tstring,
+                'file': self.__curfile,
+                'lineno': self.__lineno
+                }
+            self.__state = self.__waiting
+
+    def __addentry(self, msg, lineno=None, isdocstring=0):
+        if lineno is None:
+            lineno = self.__lineno
+        if not msg in self.__options.toexclude:
+            entry = (self.__curfile, lineno)
+            self.__messages.setdefault(msg, {})[entry] = isdocstring
+
+    def set_filename(self, filename):
+        self.__curfile = filename
+        self.__freshmodule = 1
+
+    def write(self, fp):
+        options = self.__options
+        timestamp = time.strftime('%Y-%m-%d %H:%M+%Z')
+        # The time stamp in the header doesn't have the same format as that
+        # generated by xgettext...
+        print >> fp, pot_header % {'time': timestamp, 'version': __version__}
+        # Sort the entries.  First sort each particular entry's keys, then
+        # sort all the entries by their first item.
+        reverse = {}
+        for k, v in self.__messages.items():
+            keys = v.keys()
+            keys.sort()
+            reverse.setdefault(tuple(keys), []).append((k, v))
+        rkeys = reverse.keys()
+        rkeys.sort()
+        for rkey in rkeys:
+            rentries = reverse[rkey]
+            rentries.sort()
+            for k, v in rentries:
+                isdocstring = 0
+                # If the entry was gleaned out of a docstring, then add a
+                # comment stating so.  This is to aid translators who may wish
+                # to skip translating some unimportant docstrings.
+                if reduce(operator.__add__, v.values()):
+                    isdocstring = 1
+                # k is the message string, v is a dictionary-set of (filename,
+                # lineno) tuples.  We want to sort the entries in v first by
+                # file name and then by line number.
+                v = v.keys()
+                v.sort()
+                if not options.writelocations:
+                    pass
+                # location comments are different b/w Solaris and GNU:
+                elif options.locationstyle == options.SOLARIS:
+                    for filename, lineno in v:
+                        d = {'filename': filename, 'lineno': lineno}
+                        print >>fp, _(
+                            '# File: %(filename)s, line: %(lineno)d') % d
+                elif options.locationstyle == options.GNU:
+                    # fit as many locations on one line, as long as the
+                    # resulting line length doesn't exceeds 'options.width'
+                    locline = '#:'
+                    for filename, lineno in v:
+                        d = {'filename': filename, 'lineno': lineno}
+                        s = _(' %(filename)s:%(lineno)d') % d
+                        if len(locline) + len(s) <= options.width:
+                            locline = locline + s
+                        else:
+                            print >> fp, locline
+                            locline = "#:" + s
+                    if len(locline) > 2:
+                        print >> fp, locline
+                if isdocstring:
+                    print >> fp, '#, docstring'
+                print >> fp, 'msgid', normalize(k)
+                print >> fp, 'msgstr ""\n'
+
+
+
+def main():
+    global default_keywords
+    try:
+        opts, args = getopt.getopt(
+            sys.argv[1:],
+            'ad:DEhk:Kno:p:S:Vvw:x:X:',
+            ['extract-all', 'default-domain=', 'escape', 'help',
+             'keyword=', 'no-default-keywords',
+             'add-location', 'no-location', 'output=', 'output-dir=',
+             'style=', 'verbose', 'version', 'width=', 'exclude-file=',
+             'docstrings', 'no-docstrings',
+             ])
+    except getopt.error, msg:
+        usage(1, msg)
+
+    # for holding option values
+    class Options:
+        # constants
+        GNU = 1
+        SOLARIS = 2
+        # defaults
+        extractall = 0 # FIXME: currently this option has no effect at all.
+        escape = 0
+        keywords = []
+        outpath = ''
+        outfile = 'messages.pot'
+        writelocations = 1
+        locationstyle = GNU
+        verbose = 0
+        width = 78
+        excludefilename = ''
+        docstrings = 0
+        nodocstrings = {}
+
+    options = Options()
+    locations = {'gnu' : options.GNU,
+                 'solaris' : options.SOLARIS,
+                 }
+
+    # parse options
+    for opt, arg in opts:
+        if opt in ('-h', '--help'):
+            usage(0)
+        elif opt in ('-a', '--extract-all'):
+            options.extractall = 1
+        elif opt in ('-d', '--default-domain'):
+            options.outfile = arg + '.pot'
+        elif opt in ('-E', '--escape'):
+            options.escape = 1
+        elif opt in ('-D', '--docstrings'):
+            options.docstrings = 1
+        elif opt in ('-k', '--keyword'):
+            options.keywords.append(arg)
+        elif opt in ('-K', '--no-default-keywords'):
+            default_keywords = []
+        elif opt in ('-n', '--add-location'):
+            options.writelocations = 1
+        elif opt in ('--no-location',):
+            options.writelocations = 0
+        elif opt in ('-S', '--style'):
+            options.locationstyle = locations.get(arg.lower())
+            if options.locationstyle is None:
+                usage(1, _('Invalid value for --style: %s') % arg)
+        elif opt in ('-o', '--output'):
+            options.outfile = arg
+        elif opt in ('-p', '--output-dir'):
+            options.outpath = arg
+        elif opt in ('-v', '--verbose'):
+            options.verbose = 1
+        elif opt in ('-V', '--version'):
+            print _('pygettext.py (xgettext for Python) %s') % __version__
+            sys.exit(0)
+        elif opt in ('-w', '--width'):
+            try:
+                options.width = int(arg)
+            except ValueError:
+                usage(1, _('--width argument must be an integer: %s') % arg)
+        elif opt in ('-x', '--exclude-file'):
+            options.excludefilename = arg
+        elif opt in ('-X', '--no-docstrings'):
+            fp = open(arg)
+            try:
+                while 1:
+                    line = fp.readline()
+                    if not line:
+                        break
+                    options.nodocstrings[line[:-1]] = 1
+            finally:
+                fp.close()
+
+    # calculate escapes
+    make_escapes(options.escape)
+
+    # calculate all keywords
+    options.keywords.extend(default_keywords)
+
+    # initialize list of strings to exclude
+    if options.excludefilename:
+        try:
+            fp = open(options.excludefilename)
+            options.toexclude = fp.readlines()
+            fp.close()
+        except IOError:
+            print >> sys.stderr, _(
+                "Can't read --exclude-file: %s") % options.excludefilename
+            sys.exit(1)
+    else:
+        options.toexclude = []
+
+    # resolve args to module lists
+    expanded = []
+    for arg in args:
+        if arg == '-':
+            expanded.append(arg)
+        else:
+            expanded.extend(getFilesForName(arg))
+    args = expanded
+
+    # slurp through all the files
+    eater = TokenEater(options)
+    for filename in args:
+        if filename == '-':
+            if options.verbose:
+                print _('Reading standard input')
+            fp = sys.stdin
+            closep = 0
+        else:
+            if options.verbose:
+                print _('Working on %s') % filename
+            fp = open(filename)
+            closep = 1
+        try:
+            eater.set_filename(filename)
+            try:
+                tokenize.tokenize(fp.readline, eater)
+            except tokenize.TokenError, e:
+                print >> sys.stderr, '%s: %s, line %d, column %d' % (
+                    e[0], filename, e[1][0], e[1][1])
+        finally:
+            if closep:
+                fp.close()
+
+    # write the output
+    if options.outfile == '-':
+        fp = sys.stdout
+        closep = 0
+    else:
+        if options.outpath:
+            options.outfile = os.path.join(options.outpath, options.outfile)
+        fp = open(options.outfile, 'w')
+        closep = 1
+    try:
+        eater.write(fp)
+    finally:
+        if closep:
+            fp.close()
+
+
+if __name__ == '__main__':
+    main()
+    # some more test strings
+    _(u'a unicode string')
+    # this one creates a warning
+    _('*** Seen unexpected token "%(token)s"') % {'token': 'test'}
+    _('more' 'than' 'one' 'string')

Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2008-11-06 01:28:37 UTC (rev 3324)
+++ CalendarServer/trunk/twistedcaldav/config.py	2008-11-06 01:45:12 UTC (rev 3325)
@@ -233,6 +233,9 @@
                 "Password"      : "",    # For account receving mail
             },
             "AddressPatterns"   : [],    # Reg-ex patterns to match iMIP-able calendar user addresses
+            "MailTemplatesDirectory": "/usr/share/caldavd/templates", # Directory containing HTML templates for email invitations (invite.html, cancel.html)
+            "MailIconsDirectory": "/usr/share/caldavd/images/mail", # Directory containing language-specific subdirectories containing date-specific icons for email invitations (cal-icon-mm-dd.png)
+            "InvitationDaysToLive" : 90, # How many days invitations are valid
         },
 
     },
@@ -288,7 +291,17 @@
     "IdleConnectionTimeOut": 15,
     "UIDReservationTimeOut": 30 * 60,
 
+
     #
+    # Localization
+    #
+    "Localization" : {
+        "LocalesDirectory" : "/usr/share/caldavd/locales",
+        "Language" : "en",
+    },
+
+
+    #
     # Implementation details
     #
     #    The following are specific to how the server is built, and useful

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2008-11-06 01:28:37 UTC (rev 3324)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2008-11-06 01:45:12 UTC (rev 3325)
@@ -1255,6 +1255,25 @@
 
         return results
 
+    def getAllAttendeeProperties(self):
+        """
+        Yield all attendees as Property objects.  Works on either a VCALENDAR or
+        on a component.
+        @return: a generator yielding Property objects
+        """
+
+        # Extract appropriate sub-component if this is a VCALENDAR
+        if self.name() == "VCALENDAR":
+            for component in self.subcomponents():
+                if component.name() != "VTIMEZONE":
+                    for attendee in component.getAllAttendeeProperties():
+                        yield attendee
+        else:
+            # Find the primary subcomponent
+            for attendee in self.properties("ATTENDEE"):
+                yield attendee
+
+
     def getMaskUID(self):
         """
         Get the X-CALENDARSEREVR-MASK-UID value. Works on either a VCALENDAR or on a component.

Modified: CalendarServer/trunk/twistedcaldav/mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/mail.py	2008-11-06 01:28:37 UTC (rev 3324)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2008-11-06 01:45:12 UTC (rev 3325)
@@ -18,6 +18,7 @@
 Mail Gateway for Calendar Server
 
 """
+from __future__ import with_statement
 
 from email.mime.image import MIMEImage
 from email.mime.multipart import MIMEMultipart
@@ -41,7 +42,9 @@
 from twistedcaldav.log import Logger, LoggingMixIn
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.scheduling.scheduler import IMIPScheduler
+from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
 from twistedcaldav.sql import AbstractSQLDatabase
+from twistedcaldav.localization import translationTo
 
 from zope.interface import implements
 
@@ -260,6 +263,8 @@
     log.debug("Injecting to %s: %s %s" % (url, str(headers), data))
     factory = client.HTTPClientFactory(url, method='POST', headers=headers,
         postdata=data, agent="iMIP gateway")
+    factory.noisy = False
+
     if useSSL:
         reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
     else:
@@ -287,7 +292,7 @@
 
     Token Database:
 
-    ROW: TOKEN, ORGANIZER, ATTENDEE
+    ROW: TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP
 
     """
 
@@ -301,14 +306,14 @@
             path = os.path.join(path, MailGatewayTokensDatabase.dbFilename)
         super(MailGatewayTokensDatabase, self).__init__(path, True)
 
-    def createToken(self, organizer, attendee, token=None):
+    def createToken(self, organizer, attendee, icaluid, token=None):
         if token is None:
             token = str(uuid.uuid4())
         self._db_execute(
             """
-            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE)
-            values (:1, :2, :3)
-            """, token, organizer, attendee
+            insert into TOKENS (TOKEN, ORGANIZER, ATTENDEE, ICALUID, DATESTAMP)
+            values (:1, :2, :3, :4, :5)
+            """, token, organizer, attendee, icaluid, datetime.date.today()
         )
         self._db_commit()
         return token
@@ -317,7 +322,7 @@
         results = list(
             self._db_execute(
                 """
-                select ORGANIZER, ATTENDEE from TOKENS
+                select ORGANIZER, ATTENDEE, ICALUID from TOKENS
                 where TOKEN = :1
                 """, token
             )
@@ -328,13 +333,20 @@
 
         return results[0]
 
-    def getToken(self, organizer, attendee):
+    def getToken(self, organizer, attendee, icaluid):
         token = self._db_value_for_sql(
             """
             select TOKEN from TOKENS
-            where ORGANIZER = :1 and ATTENDEE = :2
-            """, organizer, attendee
+            where ORGANIZER = :1 and ATTENDEE = :2 and ICALUID = :3
+            """, organizer, attendee, icaluid
         )
+        if token is not None:
+            # update the datestamp on the token to keep it from being purged
+            self._db_execute(
+                """
+                update TOKENS set DATESTAMP = :1 WHERE TOKEN = :2
+                """, datetime.date.today(), token
+            )
         return token
 
     def deleteToken(self, token):
@@ -343,7 +355,16 @@
             delete from TOKENS where TOKEN = :1
             """, token
         )
+        self._db_commit()
 
+    def purgeOldTokens(self, before):
+        self._db_execute(
+            """
+            delete from TOKENS where DATESTAMP < :1
+            """, before
+        )
+        self._db_commit()
+
     def _db_version(self):
         """
         @return: the schema version assigned to this index.
@@ -370,7 +391,9 @@
             create table TOKENS (
                 TOKEN       text,
                 ORGANIZER   text,
-                ATTENDEE    text
+                ATTENDEE    text,
+                ICALUID     text,
+                DATESTAMP   date
             )
             """
         )
@@ -480,8 +503,9 @@
             # Compute token, add to db, generate email and send it
             calendar = ical.Component.fromString(request.content.read())
             headers = request.getAllHeaders()
+            language = config.Localization["Language"]
             self.mailer.outbound(headers['originator'], headers['recipient'],
-                calendar)
+                calendar, language=language)
 
             # TODO: what to return?
             return """
@@ -497,6 +521,9 @@
         if dataRoot is None:
             dataRoot = config.DataRoot
         self.db = MailGatewayTokensDatabase(dataRoot)
+        days = config.Scheduling['iMIP']['InvitationDaysToLive']
+        self.db.purgeOldTokens(datetime.date.today() -
+            datetime.timedelta(days=days))
 
     def checkDSN(self, message):
         # returns (isDSN, Action, icalendar attachment)
@@ -561,9 +588,10 @@
             self.log_error("Mail gateway found a token (%s) but didn't recognize it in DSN %s" % (token, msgId))
             return
 
-        organizer, attendee = result
+        organizer, attendee, icaluid = result
         organizer = str(organizer)
         attendee = str(attendee)
+        icaluid = str(icaluid)
         calendar.removeAllButOneAttendee(attendee)
         calendar.getOrganizerProperty().setValue(organizer)
         for comp in calendar.subcomponents():
@@ -576,7 +604,7 @@
             # TODO: what to do in this case?
             pass
 
-        self.log_error("Mail gateway processing DSN %s" % (msgId,))
+        self.log_warn("Mail gateway processing DSN %s" % (msgId,))
         return fn(organizer, attendee, calendar, msgId)
 
     def processReply(self, msg, fn):
@@ -611,9 +639,10 @@
             self.log_error("Mail gateway found a token (%s) but didn't recognize it in message %s" % (token, msg['Message-ID']))
             return
 
-        organizer, attendee = result
+        organizer, attendee, icaluid = result
         organizer = str(organizer)
         attendee = str(attendee)
+        icaluid = str(icaluid)
         calendar.removeAllButOneAttendee(attendee)
         organizerProperty = calendar.getOrganizerProperty()
         if organizerProperty is None:
@@ -627,39 +656,71 @@
 
 
     def inbound(self, message, fn=injectMessage):
-        msg = email.message_from_string(message)
+        try:
+            msg = email.message_from_string(message)
 
-        isDSN, action, calBody = self.checkDSN(msg)
-        if isDSN:
-            if action == 'failed' and calBody:
-                # This is a DSN we can handle
-                return self.processDSN(calBody, msg['Message-ID'], fn)
-            else:
-                # It's a DSN without enough to go on
-                self.log_error("Mail gateway can't process DSN %s" % (msg['Message-ID'],))
-                return
+            isDSN, action, calBody = self.checkDSN(msg)
+            if isDSN:
+                if action == 'failed' and calBody:
+                    # This is a DSN we can handle
+                    return self.processDSN(calBody, msg['Message-ID'], fn)
+                else:
+                    # It's a DSN without enough to go on
+                    self.log_error("Mail gateway can't process DSN %s" % (msg['Message-ID'],))
+                    return
 
-        self.log_info("Mail gateway received message %s from %s to %s" %
-            (msg['Message-ID'], msg['From'], msg['To']))
+            self.log_info("Mail gateway received message %s from %s to %s" %
+                (msg['Message-ID'], msg['From'], msg['To']))
 
-        return self.processReply(msg, fn)
+            return self.processReply(msg, fn)
 
+        except Exception, e:
+            # Don't let a failure of any kind stop us
+            self.log_error("Failed to process message: %s" % (e,))
 
 
-    def outbound(self, organizer, attendee, calendar):
+
+
+    def outbound(self, organizer, attendee, calendar, language='en'):
         # create token, send email
-        token = self.db.getToken(organizer, attendee)
+
+        component = calendar.masterComponent()
+        if component is None:
+            component = calendar.mainComponent(True)
+        icaluid = component.propertyValue("UID")
+
+        token = self.db.getToken(organizer, attendee, icaluid)
         if token is None:
-            token = self.db.createToken(organizer, attendee)
-            self.log_info("Mail gateway created token %s for %s (organizer) and %s (attendee)" % (token, organizer, attendee))
+            token = self.db.createToken(organizer, attendee, icaluid)
+            self.log_debug("Mail gateway created token %s for %s (organizer), %s (attendee) and %s (icaluid)" % (token, organizer, attendee, icaluid))
+            newInvitation = True
         else:
-            self.log_info("Mail gateway reusing token %s for %s (organizer) and %s (attendee)" % (token, organizer, attendee))
+            self.log_debug("Mail gateway reusing token %s for %s (organizer), %s (attendee) and %s (icaluid)" % (token, organizer, attendee, icaluid))
+            newInvitation = False
 
         settings = config.Scheduling['iMIP']['Sending']
         fullServerAddress = settings['Address']
         name, serverAddress = email.utils.parseaddr(fullServerAddress)
         pre, post = serverAddress.split('@')
         addressWithToken = "%s+%s@%s" % (pre, token, post)
+
+        attendees = []
+        for attendeeProp in calendar.getAllAttendeeProperties():
+            params = attendeeProp.params()
+            cutype = params.get('CUTYPE', (None,))[0]
+            if cutype == "INDIVIDUAL":
+                cn = params.get("CN", (None,))[0]
+                cuaddr = normalizeCUAddr(attendeeProp.value())
+                if cuaddr.startswith("mailto:"):
+                    mailto = cuaddr[7:]
+                    if not cn:
+                        cn = mailto
+                else:
+                    mailto = None
+
+                if cn or mailto:
+                    attendees.append( (cn, mailto) )
+
         calendar.getOrganizerProperty().setValue("mailto:%s" %
             (addressWithToken,))
 
@@ -667,29 +728,34 @@
         if organizerAttendee is not None:
             organizerAttendee.setValue("mailto:%s" % (addressWithToken,))
 
-        msgId, message = self._generateTemplateMessage(calendar, organizer)
 
         # The email's From will include the organizer's real name email
         # address if available.  Otherwise it will be the server's email
         # address (without # + addressing)
         if organizer.startswith("mailto:"):
-            fromAddr = organizer[7:]
+            orgEmail = fromAddr = organizer[7:]
         else:
             fromAddr = serverAddress
-        cn = calendar.getOrganizerProperty().params().get('CN',
-            ['Calendar Server'])[0]
+            orgEmail = None
+        cn = calendar.getOrganizerProperty().params().get('CN', (None,))[0]
+        if cn is None:
+            cn = 'Calendar Server'
+            orgCN = orgEmail
+        else:
+            orgCN = cn
         formattedFrom = "%s <%s>" % (cn, fromAddr)
-        message = message.replace("${fromaddress}", formattedFrom)
 
         # Reply-to address will be the server+token address
-        message = message.replace("${replytoaddress}", addressWithToken)
 
         toAddr = attendee
         if not attendee.startswith("mailto:"):
             raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (attendee,))
         attendee = attendee[7:]
-        message = message.replace("${toaddress}", attendee)
 
+        msgId, message = self.generateEmail(newInvitation, calendar, orgEmail,
+            orgCN, attendees, formattedFrom, addressWithToken, attendee,
+            language=language)
+
         self.log_debug("Sending: %s" % (message,))
         def _success(result, msgId, fromAddr, toAddr):
             self.log_info("Mail gateway sent message %s from %s to %s" %
@@ -706,62 +772,202 @@
         deferred.addErrback(_failure, msgId, fromAddr, toAddr)
 
 
-    def _generateTemplateMessage(self, calendar, organizer):
+    def generateEmail(self, newInvitation, calendar, orgEmail, orgCN,
+        attendees, fromAddress, replyToAddress, toAddress, language='en'):
 
-        title, summary = self._generateCalendarSummary(calendar, organizer)
+        details = self.getEventDetails(calendar, language=language)
 
-        msg = MIMEMultipart()
-        msg["From"] = "${fromaddress}"
-        msg["Reply-To"] = "${replytoaddress}"
-        msg["To"] = "${toaddress}"
-        msg["Date"] = rfc822date()
-        msgId = messageid()
-        msg["Message-ID"] = msgId
+        iconDir = config.Scheduling["iMIP"]["MailIconsDirectory"].rstrip("/")
+        iconName = "cal-icon-%02d-%02d.png" % (details['month'],
+            details['day'])
+        iconPath = os.path.join(iconDir, language, iconName)
+        if not os.path.exists(iconPath):
+            # Try the generic (numeric) version
+            iconPath = os.path.join(iconDir, 'generic', iconName)
 
-        msgAlt = MIMEMultipart("alternative")
-        msg.attach(msgAlt)
+        with translationTo(language):
+            msg = MIMEMultipart()
+            msg["From"] = fromAddress
+            msg["Reply-To"] = replyToAddress
+            msg["To"] = toAddress
+            msg["Date"] = rfc822date()
+            msgId = messageid()
+            msg["Message-ID"] = msgId
 
-        # plain text version
-        if calendar.propertyValue("METHOD") == "CANCEL":
-            msg["Subject"] = "Event cancelled"
-            plainText = u"An event has been cancelled.  Click the link below.\n"
-        else:
-            msg["Subject"] = "Event invitation: %s" % (title,)
-            plainText = u"You've been invited to the following event:  %s To accept or decline this invitation, click the link below.\n" % (summary,)
+            canceled = (calendar.propertyValue("METHOD") == "CANCEL")
+            if canceled:
+                formatString = _("Event canceled: %(summary)s")
+            elif newInvitation:
+                formatString = _("Event invitation: %(summary)s")
+            else:
+                formatString = _("Event update: %(summary)s")
 
-        msgPlain = MIMEText(plainText.encode("UTF-8"), "plain", "UTF-8")
-        msgAlt.attach(msgPlain)
+            details['subject'] = msg['Subject'] = formatString % {
+                'summary' : details['summary']
+            }
 
-        # html version
-        msgHtmlRelated = MIMEMultipart("related", type="text/html")
-        msgAlt.attach(msgHtmlRelated)
-        htmlText = u"""
-<html><body><div>
-<img src="cid:ical.jpg">
-%s
-</div></body></html>
-""" % plainText
+            msgAlt = MIMEMultipart("alternative")
+            msg.attach(msgAlt)
+
+            # Get localized labels
+            if canceled:
+                details['inviteLabel'] = _("Event Canceled")
+            else:
+                if newInvitation:
+                    details['inviteLabel'] = _("Event Invitation")
+                else:
+                    details['inviteLabel'] = _("Event Update")
+
+            details['dateLabel'] = _("Date")
+            details['timeLabel'] = _("Time")
+            details['durationLabel'] = _("Duration")
+            details['recurrenceLabel'] = _("Occurs")
+            details['descLabel'] = _("Description")
+            details['orgLabel'] = _("Organizer")
+            details['attLabel'] = _("Attendees")
+            details['locLabel'] = _("Location")
+
+
+            plainAttendeeList = []
+            for cn, mailto in attendees:
+                if cn:
+                    plainAttendeeList.append(cn if not mailto else
+                        "%s <%s>" % (cn, mailto))
+                elif mailto:
+                    plainAttendeeList.append("<%s>" % (mailto,))
+
+            details['plainAttendees'] = ", ".join(plainAttendeeList)
+
+            details['plainOrganizer'] = (orgCN if not orgEmail else
+                "%s <%s>" % (orgCN, orgEmail))
+
+            # plain text version
+            if canceled:
+                plainTemplate = u"""%(subject)s
+
+%(orgLabel)s: %(plainOrganizer)s
+%(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s
+%(timeLabel)s: %(timeInfo)s %(durationInfo)s
+"""
+            else:
+                plainTemplate = u"""%(subject)s
+
+%(orgLabel)s: %(plainOrganizer)s
+%(locLabel)s: %(location)s
+%(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s
+%(timeLabel)s: %(timeInfo)s %(durationInfo)s
+%(descLabel)s: %(description)s
+%(attLabel)s: %(plainAttendees)s
+"""
+
+            plainText = plainTemplate % details
+
+            msgPlain = MIMEText(plainText.encode("UTF-8"), "plain", "UTF-8")
+            msgAlt.attach(msgPlain)
+
+            # html version
+            msgHtmlRelated = MIMEMultipart("related", type="text/html")
+            msgAlt.attach(msgHtmlRelated)
+
+
+            htmlAttendees = []
+            for cn, mailto in attendees:
+                if mailto:
+                    htmlAttendees.append('<a href="mailto:%s">%s</a>' %
+                        (mailto, cn))
+                else:
+                    htmlAttendees.append(cn)
+
+            details['htmlAttendees'] = ", ".join(htmlAttendees)
+
+            if orgEmail:
+                details['htmlOrganizer'] = '<a href="mailto:%s">%s</a>' % (
+                    orgEmail, orgCN)
+            else:
+                details['htmlOrganizer'] = orgCN
+
+            details['iconName'] = iconName
+
+            templateDir = config.Scheduling["iMIP"]["MailTemplatesDirectory"].rstrip("/")
+            templateName = "cancel.html" if canceled else "invite.html"
+            templatePath = os.path.join(templateDir, templateName)
+
+            if not os.path.exists(templatePath):
+                # Fall back to built-in simple templates:
+                if canceled:
+
+                    htmlTemplate = u"""<html>
+    <body><div>
+
+    <h1>%(subject)s</h1>
+    <p>
+    <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s
+    </p>
+    <p>
+    <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s
+    </p>
+    <p>
+    <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s
+    </p>
+
+    """
+
+                else:
+
+                    htmlTemplate = u"""<html>
+    <body><div>
+    <p>%(inviteLabel)s</p>
+
+    <h1>%(summary)s</h1>
+    <p>
+    <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s
+    </p>
+    <p>
+    <h3>%(locLabel)s:</h3> %(location)s
+    </p>
+    <p>
+    <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s
+    </p>
+    <p>
+    <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s
+    </p>
+    <p>
+    <h3>%(descLabel)s:</h3> %(description)s
+    </p>
+    <p>
+    <h3>%(attLabel)s:</h3> %(htmlAttendees)s
+    </p>
+
+    """
+            else: # HTML template file exists
+
+                with open(templatePath) as templateFile:
+                    htmlTemplate = templateFile.read()
+
+            htmlText = htmlTemplate % details
+
         msgHtml = MIMEText(htmlText.encode("UTF-8"), "html", "UTF-8")
         msgHtmlRelated.attach(msgHtml)
 
         # an image for html version
-        imageName = "ical.jpg"
-        imageFile = open(os.path.join(os.path.dirname(__file__),
-            "images", "mail", imageName))
-        msgImage = MIMEImage(imageFile.read(),
-            _subtype='jpeg;x-apple-mail-type=stationery;name="%s"' %
-            (imageName,))
-        imageFile.close()
-        msgImage.add_header("Content-ID", "<%s>" % (imageName,))
-        msgImage.add_header("Content-Disposition", "inline;filename=%s" %
-            (imageName,))
-        msgHtmlRelated.attach(msgImage)
+        if os.path.exists(iconPath) and htmlTemplate.find("cid:%(iconName)s") != -1:
 
+            with open(iconPath) as iconFile:
+                msgIcon = MIMEImage(iconFile.read(),
+                    _subtype='png;x-apple-mail-type=stationery;name="%s"' %
+                    (iconName,))
+
+            msgIcon.add_header("Content-ID", "<%s>" % (iconName,))
+            msgIcon.add_header("Content-Disposition", "inline;filename=%s" %
+                (iconName,))
+            msgHtmlRelated.attach(msgIcon)
+
         # the icalendar attachment
         self.log_debug("Mail gateway sending calendar body: %s" % (str(calendar)))
         msgIcal = MIMEText(str(calendar), "calendar", "UTF-8")
         method = calendar.propertyValue("METHOD").lower()
         msgIcal.set_param("method", method)
+        msgIcal.add_header("Content-ID", "<invitation.ics>")
         msgIcal.add_header("Content-Disposition",
             "inline;filename=invitation.ics")
         msg.attach(msgIcal)
@@ -769,135 +975,55 @@
         return msgId, msg.as_string()
 
 
-    def _generateCalendarSummary(self, calendar, organizer):
+    def getEventDetails(self, calendar, language='en'):
 
         # Get the most appropriate component
         component = calendar.masterComponent()
         if component is None:
             component = calendar.mainComponent(True)
 
-        organizerProp = component.getOrganizerProperty()
-        if "CN" in organizerProp.params():
-            organizer = "%s <%s>" % (organizerProp.params()["CN"][0],
-                organizer,)
+        results = { }
 
-        if calendar.propertyValue("METHOD") == "CANCEL":
-            dtinfo = ""
-        else:
-            dtinfo = self._getDateTimeInfo(component)
+        dtStart = component.propertyNativeValue("DTSTART")
+        results['month'] = dtStart.month
+        results['day'] = dtStart.day
 
         summary = component.propertyValue("SUMMARY")
         if summary is None:
             summary = ""
+        results['summary'] = summary
 
         description = component.propertyValue("DESCRIPTION")
         if description is None:
             description = ""
+        results['description'] = description
 
-        return summary, """
+        location = component.propertyValue("LOCATION")
+        if location is None:
+            location = ""
+        results['location'] = location
 
-Summary: %s
-Organizer: %s
-%sDescription: %s
+        with translationTo(language) as trans:
+            results['dateInfo'] = trans.date(component)
+            results['timeInfo'], duration = trans.time(component)
+            results['durationInfo'] = "(%s)" % (duration,) if duration else ""
 
-""" % (summary, organizer, dtinfo, description,)
-
-    def _getDateTimeInfo(self, component):
-
-        dtstart = component.propertyNativeValue("DTSTART")
-        tzid_start = component.getProperty("DTSTART").params().get("TZID", "UTC")
-
-        dtend = component.propertyNativeValue("DTEND")
-        if dtend:
-            tzid_end = component.getProperty("DTEND").params().get("TZID", "UTC")
-            duration = dtend - dtstart
-        else:
-            duration = component.propertyNativeValue("DURATION")
-            if duration:
-                dtend = dtstart + duration
-                tzid_end = tzid_start
+            for propertyName in ("RRULE", "RDATE", "EXRULE", "EXDATE",
+                "RECURRENCE-ID"):
+                if component.hasProperty(propertyName):
+                    results['recurrenceInfo'] = _("(Repeating)")
+                    break
             else:
-                if isinstance(dtstart, datetime.date):
-                    dtend = None
-                    duration = datetime.timedelta(days=1)
-                else:
-                    dtend = dtstart + datetime.timedelta(days=1)
-                    dtend.hour = dtend.minute = dtend.second = 0
-                    duration = dtend - dtstart
-        result = "Starts:      %s\n" % (self._getDateTimeText(dtstart, tzid_start),)
-        if dtend is not None:
-            result += "Ends:        %s\n" % (self._getDateTimeText(dtend, tzid_end),)
-        result += "Duration:    %s\n" % (self._getDurationText(duration),)
+                results['recurrenceInfo'] = ""
 
-        if not isinstance(dtstart, datetime.datetime):
-            result += "All Day\n"
+        return results
 
-        for property_name in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
-            if component.hasProperty(property_name):
-                result += "Recurring\n"
-                break
 
-        return result
 
-    def _getDateTimeText(self, dtvalue, tzid):
 
-        if isinstance(dtvalue, datetime.datetime):
-            timeformat = "%A, %B %e, %Y %I:%M %p"
-        elif isinstance(dtvalue, datetime.date):
-            timeformat = "%A, %B %e, %Y"
-            tzid = ""
-        if tzid:
-            tzid = " (%s)" % (tzid,)
 
-        return "%s%s" % (dtvalue.strftime(timeformat), tzid,)
 
-    def _getDurationText(self, duration):
 
-        result = ""
-        if duration.days > 0:
-            result += "%d %s" % (
-                duration.days,
-                self._pluralize(duration.days, "day", "days")
-            )
-
-        hours = duration.seconds / 3600
-        minutes = divmod(duration.seconds / 60, 60)[1]
-        seconds = divmod(duration.seconds, 60)[1]
-
-        if hours > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                hours,
-                self._pluralize(hours, "hour", "hours")
-            )
-
-        if minutes > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                minutes,
-                self._pluralize(minutes, "minute", "minutes")
-            )
-
-        if seconds > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                seconds,
-                self._pluralize(seconds, "second", "seconds")
-            )
-
-        return result
-
-    def _pluralize(self, number, unit1, unitS):
-        return unit1 if number == 1 else unitS
-
-
-
-
-
-
 #
 # POP3
 #
@@ -972,6 +1098,7 @@
             from twisted.internet import reactor
         self.reactor = reactor
         self.nextPoll = None
+        self.noisy = False
 
     def retry(self, connector=None):
         # TODO: if connector is None:
@@ -1147,6 +1274,7 @@
         if reactor is None:
             from twisted.internet import reactor
         self.reactor = reactor
+        self.noisy = False
 
 
     def handleMessage(self, message):

Deleted: CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.mo
===================================================================
(Binary files differ)

Copied: CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.mo (from rev 3323, CalendarServer/branches/users/sagen/localization-3308/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.mo)
===================================================================
(Binary files differ)

Deleted: CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po
===================================================================
--- CalendarServer/branches/users/sagen/localization-3308/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po	2008-11-06 01:01:07 UTC (rev 3323)
+++ CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po	2008-11-06 01:45:12 UTC (rev 3325)
@@ -1,249 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR ORGANIZATION
-# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: 2008-10-24 15:03+PDT\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
-"Language-Team: LANGUAGE <LL at li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: pygettext.py 1.5\n"
-
-
-#: localization.py:171
-msgid "All day"
-msgstr ""
-
-#: localization.py:177
-msgid "%(startTime)s to %(endTime)s"
-msgstr ""
-
-#: localization.py:191
-msgid "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
-msgstr ""
-
-#: localization.py:207
-msgid "AM"
-msgstr ""
-
-#: localization.py:207
-msgid "PM"
-msgstr ""
-
-#: localization.py:213
-msgid "%(hour12Number)d:%(minuteNumber)02d %(ampm)s"
-msgstr ""
-
-#: localization.py:236
-msgid "Monday"
-msgstr ""
-
-#: localization.py:237
-msgid "Tuesday"
-msgstr ""
-
-#: localization.py:238
-msgid "Wednesday"
-msgstr ""
-
-#: localization.py:239
-msgid "Thursday"
-msgstr ""
-
-#: localization.py:240
-msgid "Friday"
-msgstr ""
-
-#: localization.py:241
-msgid "Saturday"
-msgstr ""
-
-#: localization.py:242
-msgid "Sunday"
-msgstr ""
-
-#: localization.py:246
-msgid "Mon"
-msgstr ""
-
-#: localization.py:247
-msgid "Tue"
-msgstr ""
-
-#: localization.py:248
-msgid "Wed"
-msgstr ""
-
-#: localization.py:249
-msgid "Thu"
-msgstr ""
-
-#: localization.py:250
-msgid "Fri"
-msgstr ""
-
-#: localization.py:251
-msgid "Sun"
-msgstr ""
-
-#: localization.py:252
-msgid "Sat"
-msgstr ""
-
-#: localization.py:257
-msgid "January"
-msgstr ""
-
-#: localization.py:258
-msgid "February"
-msgstr ""
-
-#: localization.py:259
-msgid "March"
-msgstr ""
-
-#: localization.py:260
-msgid "April"
-msgstr ""
-
-#: localization.py:261 localization.py:277
-msgid "May"
-msgstr ""
-
-#: localization.py:262
-msgid "June"
-msgstr ""
-
-#: localization.py:263
-msgid "July"
-msgstr ""
-
-#: localization.py:264
-msgid "August"
-msgstr ""
-
-#: localization.py:265
-msgid "September"
-msgstr ""
-
-#: localization.py:266
-msgid "October"
-msgstr ""
-
-#: localization.py:267
-msgid "November"
-msgstr ""
-
-#: localization.py:268
-msgid "December"
-msgstr ""
-
-#: localization.py:273
-msgid "Jan"
-msgstr ""
-
-#: localization.py:274
-msgid "Feb"
-msgstr ""
-
-#: localization.py:275
-msgid "Mar"
-msgstr ""
-
-#: localization.py:276
-msgid "Apr"
-msgstr ""
-
-#: localization.py:278
-msgid "Jun"
-msgstr ""
-
-#: localization.py:279
-msgid "Jul"
-msgstr ""
-
-#: localization.py:280
-msgid "Aug"
-msgstr ""
-
-#: localization.py:281
-msgid "Sep"
-msgstr ""
-
-#: localization.py:282
-msgid "Oct"
-msgstr ""
-
-#: localization.py:283
-msgid "Nov"
-msgstr ""
-
-#: localization.py:284
-msgid "Dec"
-msgstr ""
-
-#: mail.py:726 mail.py:755 mail.py:792
-msgid "Event cancelled"
-msgstr ""
-
-#: mail.py:727
-msgid "Event invitation: %(summary)s"
-msgstr ""
-
-#: mail.py:736
-msgid "Event Invitation"
-msgstr ""
-
-#: mail.py:737
-msgid "Date"
-msgstr ""
-
-#: mail.py:738
-msgid "Time"
-msgstr ""
-
-#: mail.py:739
-msgid "Description"
-msgstr ""
-
-#: mail.py:740
-msgid "Organizer"
-msgstr ""
-
-#: mail.py:741
-msgid "Attendees"
-msgstr ""
-
-#: mail.py:742
-msgid "Location"
-msgstr ""
-
-
-msgid "1 day"
-msgstr
-
-msgid "%(dayCount)d days"
-msgstr
-
-msgid "1 hour"
-
-msgid "%(hourCount)d hours"
-msgstr
-
-msgid "1 minute"
-msgstr
-
-msgid "%(minuteCount)d minutes"
-msgstr
-
-msgid "1 second"
-msgstr
-
-msgid "%(secondCount)d seconds"
-msgstr
-

Copied: CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po (from rev 3323, CalendarServer/branches/users/sagen/localization-3308/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/data/locales/en/LC_MESSAGES/calendarserver.po	2008-11-06 01:45:12 UTC (rev 3325)
@@ -0,0 +1,249 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2008-10-24 15:03+PDT\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: localization.py:171
+msgid "All day"
+msgstr ""
+
+#: localization.py:177
+msgid "%(startTime)s to %(endTime)s"
+msgstr ""
+
+#: localization.py:191
+msgid "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
+msgstr ""
+
+#: localization.py:207
+msgid "AM"
+msgstr ""
+
+#: localization.py:207
+msgid "PM"
+msgstr ""
+
+#: localization.py:213
+msgid "%(hour12Number)d:%(minuteNumber)02d %(ampm)s"
+msgstr ""
+
+#: localization.py:236
+msgid "Monday"
+msgstr ""
+
+#: localization.py:237
+msgid "Tuesday"
+msgstr ""
+
+#: localization.py:238
+msgid "Wednesday"
+msgstr ""
+
+#: localization.py:239
+msgid "Thursday"
+msgstr ""
+
+#: localization.py:240
+msgid "Friday"
+msgstr ""
+
+#: localization.py:241
+msgid "Saturday"
+msgstr ""
+
+#: localization.py:242
+msgid "Sunday"
+msgstr ""
+
+#: localization.py:246
+msgid "Mon"
+msgstr ""
+
+#: localization.py:247
+msgid "Tue"
+msgstr ""
+
+#: localization.py:248
+msgid "Wed"
+msgstr ""
+
+#: localization.py:249
+msgid "Thu"
+msgstr ""
+
+#: localization.py:250
+msgid "Fri"
+msgstr ""
+
+#: localization.py:251
+msgid "Sun"
+msgstr ""
+
+#: localization.py:252
+msgid "Sat"
+msgstr ""
+
+#: localization.py:257
+msgid "January"
+msgstr ""
+
+#: localization.py:258
+msgid "February"
+msgstr ""
+
+#: localization.py:259
+msgid "March"
+msgstr ""
+
+#: localization.py:260
+msgid "April"
+msgstr ""
+
+#: localization.py:261 localization.py:277
+msgid "May"
+msgstr ""
+
+#: localization.py:262
+msgid "June"
+msgstr ""
+
+#: localization.py:263
+msgid "July"
+msgstr ""
+
+#: localization.py:264
+msgid "August"
+msgstr ""
+
+#: localization.py:265
+msgid "September"
+msgstr ""
+
+#: localization.py:266
+msgid "October"
+msgstr ""
+
+#: localization.py:267
+msgid "November"
+msgstr ""
+
+#: localization.py:268
+msgid "December"
+msgstr ""
+
+#: localization.py:273
+msgid "Jan"
+msgstr ""
+
+#: localization.py:274
+msgid "Feb"
+msgstr ""
+
+#: localization.py:275
+msgid "Mar"
+msgstr ""
+
+#: localization.py:276
+msgid "Apr"
+msgstr ""
+
+#: localization.py:278
+msgid "Jun"
+msgstr ""
+
+#: localization.py:279
+msgid "Jul"
+msgstr ""
+
+#: localization.py:280
+msgid "Aug"
+msgstr ""
+
+#: localization.py:281
+msgid "Sep"
+msgstr ""
+
+#: localization.py:282
+msgid "Oct"
+msgstr ""
+
+#: localization.py:283
+msgid "Nov"
+msgstr ""
+
+#: localization.py:284
+msgid "Dec"
+msgstr ""
+
+#: mail.py:726 mail.py:755 mail.py:792
+msgid "Event cancelled"
+msgstr ""
+
+#: mail.py:727
+msgid "Event invitation: %(summary)s"
+msgstr ""
+
+#: mail.py:736
+msgid "Event Invitation"
+msgstr ""
+
+#: mail.py:737
+msgid "Date"
+msgstr ""
+
+#: mail.py:738
+msgid "Time"
+msgstr ""
+
+#: mail.py:739
+msgid "Description"
+msgstr ""
+
+#: mail.py:740
+msgid "Organizer"
+msgstr ""
+
+#: mail.py:741
+msgid "Attendees"
+msgstr ""
+
+#: mail.py:742
+msgid "Location"
+msgstr ""
+
+
+msgid "1 day"
+msgstr
+
+msgid "%(dayCount)d days"
+msgstr
+
+msgid "1 hour"
+
+msgid "%(hourCount)d hours"
+msgstr
+
+msgid "1 minute"
+msgstr
+
+msgid "%(minuteCount)d minutes"
+msgstr
+
+msgid "1 second"
+msgstr
+
+msgid "%(secondCount)d seconds"
+msgstr
+

Deleted: CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.mo
===================================================================
(Binary files differ)

Copied: CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.mo (from rev 3323, CalendarServer/branches/users/sagen/localization-3308/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.mo)
===================================================================
(Binary files differ)

Deleted: CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po
===================================================================
--- CalendarServer/branches/users/sagen/localization-3308/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2008-11-06 01:01:07 UTC (rev 3323)
+++ CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2008-11-06 01:45:12 UTC (rev 3325)
@@ -1,250 +0,0 @@
-# Pig Latin Translation
-# Copyright (C) YEAR ORGANIZATION
-# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"POT-Creation-Date: 2008-10-24 15:03+PDT\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
-"Language-Team: LANGUAGE <LL at li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: pygettext.py 1.5\n"
-
-
-#: localization.py:171
-msgid "All day"
-msgstr "Allway ayday"
-
-#: localization.py:177
-msgid "%(startTime)s to %(endTime)s"
-msgstr "%(startTime)s otay %(endTime)s"
-
-#: localization.py:191
-msgid "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
-msgstr "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
-
-#: localization.py:207
-msgid "AM"
-msgstr "AMWAY"
-
-#: localization.py:207
-msgid "PM"
-msgstr "PMAY"
-
-#: localization.py:213
-msgid "%(hour12Number)d:%(minuteNumber)02d %(ampm)s"
-msgstr "%(hour24Number)02d:%(minuteNumber)02d"
-
-#: localization.py:236
-msgid "Monday"
-msgstr "Ondaymay"
-
-#: localization.py:237
-msgid "Tuesday"
-msgstr "Uesdaytay"
-
-#: localization.py:238
-msgid "Wednesday"
-msgstr "Ednesdayway"
-
-#: localization.py:239
-msgid "Thursday"
-msgstr "Ursdaythay"
-
-#: localization.py:240
-msgid "Friday"
-msgstr "Idayfray"
-
-#: localization.py:241
-msgid "Saturday"
-msgstr "Aturdaysay"
-
-#: localization.py:242
-msgid "Sunday"
-msgstr "Undaysay"
-
-#: localization.py:246
-msgid "Mon"
-msgstr ""
-
-#: localization.py:247
-msgid "Tue"
-msgstr ""
-
-#: localization.py:248
-msgid "Wed"
-msgstr ""
-
-#: localization.py:249
-msgid "Thu"
-msgstr ""
-
-#: localization.py:250
-msgid "Fri"
-msgstr ""
-
-#: localization.py:251
-msgid "Sun"
-msgstr ""
-
-#: localization.py:252
-msgid "Sat"
-msgstr ""
-
-#: localization.py:257
-msgid "January"
-msgstr "Anuaryjay"
-
-#: localization.py:258
-msgid "February"
-msgstr "Ebruaryfay"
-
-#: localization.py:259
-msgid "March"
-msgstr "Archmay"
-
-#: localization.py:260
-msgid "April"
-msgstr "Aprilway"
-
-#: localization.py:261 localization.py:277
-msgid "May"
-msgstr "Aymay"
-
-#: localization.py:262
-msgid "June"
-msgstr "Unejay"
-
-#: localization.py:263
-msgid "July"
-msgstr "Ulyjay"
-
-#: localization.py:264
-msgid "August"
-msgstr "Augustway"
-
-#: localization.py:265
-msgid "September"
-msgstr "Eptembersay"
-
-#: localization.py:266
-msgid "October"
-msgstr "Octoberway"
-
-#: localization.py:267
-msgid "November"
-msgstr "Ovembernay"
-
-#: localization.py:268
-msgid "December"
-msgstr "Ecemberday"
-
-#: localization.py:273
-msgid "Jan"
-msgstr ""
-
-#: localization.py:274
-msgid "Feb"
-msgstr ""
-
-#: localization.py:275
-msgid "Mar"
-msgstr ""
-
-#: localization.py:276
-msgid "Apr"
-msgstr ""
-
-#: localization.py:278
-msgid "Jun"
-msgstr ""
-
-#: localization.py:279
-msgid "Jul"
-msgstr ""
-
-#: localization.py:280
-msgid "Aug"
-msgstr ""
-
-#: localization.py:281
-msgid "Sep"
-msgstr ""
-
-#: localization.py:282
-msgid "Oct"
-msgstr ""
-
-#: localization.py:283
-msgid "Nov"
-msgstr ""
-
-#: localization.py:284
-msgid "Dec"
-msgstr ""
-
-#: mail.py:726 mail.py:755 mail.py:792
-msgid "Event cancelled"
-msgstr "Eventway ancelledcay"
-
-#: mail.py:727
-msgid "Event invitation: %(summary)s"
-msgstr "Eventway invitationway: %(summary)s"
-
-#: mail.py:736
-msgid "Event Invitation"
-msgstr "Eventway invitationway"
-
-#: mail.py:737
-msgid "Date"
-msgstr "Ateday"
-
-#: mail.py:738
-msgid "Time"
-msgstr "Imetay"
-
-#: mail.py:739
-msgid "Description"
-msgstr "Escriptionday"
-
-#: mail.py:740
-msgid "Organizer"
-msgstr "Organizerway"
-
-#: mail.py:741
-msgid "Attendees"
-msgstr "Attendeesway"
-
-#: mail.py:742
-msgid "Location"
-msgstr "Ocationlay"
-
-
-msgid "1 day"
-msgstr "1 ayday"
-
-msgid "%(dayCount)d days"
-msgstr "%(dayCount)d aysday"
-
-msgid "1 hour"
-msgstr "1 ourhay"
-
-msgid "%(hourCount)d hours"
-msgstr "%(hourCount)d ourshay"
-
-msgid "1 minute"
-msgstr "1 inutemay"
-
-msgid "%(minuteCount)d minutes"
-msgstr "%(minuteCount)d inutesmay"
-
-msgid "1 second"
-msgstr "1 econdsay"
-
-msgid "%(secondCount)d seconds"
-msgstr "%(secondCount)d econdsay"
-

Copied: CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po (from rev 3323, CalendarServer/branches/users/sagen/localization-3308/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/data/locales/pig/LC_MESSAGES/calendarserver.po	2008-11-06 01:45:12 UTC (rev 3325)
@@ -0,0 +1,250 @@
+# Pig Latin Translation
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2008-10-24 15:03+PDT\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: localization.py:171
+msgid "All day"
+msgstr "Allway ayday"
+
+#: localization.py:177
+msgid "%(startTime)s to %(endTime)s"
+msgstr "%(startTime)s otay %(endTime)s"
+
+#: localization.py:191
+msgid "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
+msgstr "%(dayName)s, %(monthName)s %(dayNumber)d, %(yearNumber)d"
+
+#: localization.py:207
+msgid "AM"
+msgstr "AMWAY"
+
+#: localization.py:207
+msgid "PM"
+msgstr "PMAY"
+
+#: localization.py:213
+msgid "%(hour12Number)d:%(minuteNumber)02d %(ampm)s"
+msgstr "%(hour24Number)02d:%(minuteNumber)02d"
+
+#: localization.py:236
+msgid "Monday"
+msgstr "Ondaymay"
+
+#: localization.py:237
+msgid "Tuesday"
+msgstr "Uesdaytay"
+
+#: localization.py:238
+msgid "Wednesday"
+msgstr "Ednesdayway"
+
+#: localization.py:239
+msgid "Thursday"
+msgstr "Ursdaythay"
+
+#: localization.py:240
+msgid "Friday"
+msgstr "Idayfray"
+
+#: localization.py:241
+msgid "Saturday"
+msgstr "Aturdaysay"
+
+#: localization.py:242
+msgid "Sunday"
+msgstr "Undaysay"
+
+#: localization.py:246
+msgid "Mon"
+msgstr ""
+
+#: localization.py:247
+msgid "Tue"
+msgstr ""
+
+#: localization.py:248
+msgid "Wed"
+msgstr ""
+
+#: localization.py:249
+msgid "Thu"
+msgstr ""
+
+#: localization.py:250
+msgid "Fri"
+msgstr ""
+
+#: localization.py:251
+msgid "Sun"
+msgstr ""
+
+#: localization.py:252
+msgid "Sat"
+msgstr ""
+
+#: localization.py:257
+msgid "January"
+msgstr "Anuaryjay"
+
+#: localization.py:258
+msgid "February"
+msgstr "Ebruaryfay"
+
+#: localization.py:259
+msgid "March"
+msgstr "Archmay"
+
+#: localization.py:260
+msgid "April"
+msgstr "Aprilway"
+
+#: localization.py:261 localization.py:277
+msgid "May"
+msgstr "Aymay"
+
+#: localization.py:262
+msgid "June"
+msgstr "Unejay"
+
+#: localization.py:263
+msgid "July"
+msgstr "Ulyjay"
+
+#: localization.py:264
+msgid "August"
+msgstr "Augustway"
+
+#: localization.py:265
+msgid "September"
+msgstr "Eptembersay"
+
+#: localization.py:266
+msgid "October"
+msgstr "Octoberway"
+
+#: localization.py:267
+msgid "November"
+msgstr "Ovembernay"
+
+#: localization.py:268
+msgid "December"
+msgstr "Ecemberday"
+
+#: localization.py:273
+msgid "Jan"
+msgstr ""
+
+#: localization.py:274
+msgid "Feb"
+msgstr ""
+
+#: localization.py:275
+msgid "Mar"
+msgstr ""
+
+#: localization.py:276
+msgid "Apr"
+msgstr ""
+
+#: localization.py:278
+msgid "Jun"
+msgstr ""
+
+#: localization.py:279
+msgid "Jul"
+msgstr ""
+
+#: localization.py:280
+msgid "Aug"
+msgstr ""
+
+#: localization.py:281
+msgid "Sep"
+msgstr ""
+
+#: localization.py:282
+msgid "Oct"
+msgstr ""
+
+#: localization.py:283
+msgid "Nov"
+msgstr ""
+
+#: localization.py:284
+msgid "Dec"
+msgstr ""
+
+#: mail.py:726 mail.py:755 mail.py:792
+msgid "Event cancelled"
+msgstr "Eventway ancelledcay"
+
+#: mail.py:727
+msgid "Event invitation: %(summary)s"
+msgstr "Eventway invitationway: %(summary)s"
+
+#: mail.py:736
+msgid "Event Invitation"
+msgstr "Eventway invitationway"
+
+#: mail.py:737
+msgid "Date"
+msgstr "Ateday"
+
+#: mail.py:738
+msgid "Time"
+msgstr "Imetay"
+
+#: mail.py:739
+msgid "Description"
+msgstr "Escriptionday"
+
+#: mail.py:740
+msgid "Organizer"
+msgstr "Organizerway"
+
+#: mail.py:741
+msgid "Attendees"
+msgstr "Attendeesway"
+
+#: mail.py:742
+msgid "Location"
+msgstr "Ocationlay"
+
+
+msgid "1 day"
+msgstr "1 ayday"
+
+msgid "%(dayCount)d days"
+msgstr "%(dayCount)d aysday"
+
+msgid "1 hour"
+msgstr "1 ourhay"
+
+msgid "%(hourCount)d hours"
+msgstr "%(hourCount)d ourshay"
+
+msgid "1 minute"
+msgstr "1 inutemay"
+
+msgid "%(minuteCount)d minutes"
+msgstr "%(minuteCount)d inutesmay"
+
+msgid "1 second"
+msgstr "1 econdsay"
+
+msgid "%(secondCount)d seconds"
+msgstr "%(secondCount)d econdsay"
+

Copied: CalendarServer/trunk/twistedcaldav/test/test_localization.py (from rev 3323, CalendarServer/branches/users/sagen/localization-3308/twistedcaldav/test/test_localization.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_localization.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_localization.py	2008-11-06 01:45:12 UTC (rev 3325)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from __future__ import with_statement
+
+from twisted.trial.unittest import TestCase
+from twistedcaldav.localization import translationTo
+from twistedcaldav.ical import Component
+from datetime import datetime, time
+import os
+
+def getComp(str):
+    calendar = Component.fromString(str)
+    comp = calendar.masterComponent()
+    if comp is None:
+        comp = calendar.mainComponent(True)
+    return comp
+
+data = (
+    ('1-hour-long-morning', getComp('BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nMETHOD:REQUEST\r\nPRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN\r\nBEGIN:VTIMEZONE\r\nTZID:US/Pacific\r\nBEGIN:STANDARD\r\nDTSTART:20071104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nTZNAME:PST\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:20070311T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nTZNAME:PDT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:C7C037CC-1485-461B-8866-777C662C5930\r\nDTSTART;TZID=US/Pacific:20081025T091500\r\nDTEND;TZID=US/Pacific:20081025T101501\r\nATTENDEE;CN=test at systemcall.com;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSV\r\n P=TRUE:mailto:test at systemcall.com\r\nATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:testuser@\r\n example.com\r\nCREATED:20081024T181749Z\r\nDTSTAMP:20081024T183142Z\r\nORGANIZER;CN=Test User:mailto:testuser at ex
 ample.com\r\nSEQUENCE:5\r\nSUMMARY:testing\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n')),
+
+    ('2-hour-long-evening', getComp('BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nMETHOD:REQUEST\r\nPRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN\r\nBEGIN:VTIMEZONE\r\nTZID:US/Pacific\r\nBEGIN:STANDARD\r\nDTSTART:20071104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nTZNAME:PST\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:20070311T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nTZNAME:PDT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:C7C037CC-1485-461B-8866-777C662C5930\r\nDTSTART;TZID=US/Pacific:20081025T131500\r\nDTEND;TZID=US/Pacific:20081025T151502\r\nATTENDEE;CN=test at systemcall.com;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSV\r\n P=TRUE:mailto:test at systemcall.com\r\nATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:testuser@\r\n example.com\r\nCREATED:20081024T181749Z\r\nDTSTAMP:20081024T183142Z\r\nORGANIZER;CN=Test User:mailto:testuser at ex
 ample.com\r\nSEQUENCE:5\r\nSUMMARY:testing\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n')),
+
+    ('3-hour-long-cross-noon', getComp('BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nMETHOD:REQUEST\r\nPRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN\r\nBEGIN:VTIMEZONE\r\nTZID:US/Pacific\r\nBEGIN:STANDARD\r\nDTSTART:20071104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nTZNAME:PST\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:20070311T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nTZNAME:PDT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:C7C037CC-1485-461B-8866-777C662C5930\r\nDTSTART;TZID=US/Pacific:20081025T110500\r\nDTEND;TZID=US/Pacific:20081025T141500\r\nATTENDEE;CN=test at systemcall.com;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSV\r\n P=TRUE:mailto:test at systemcall.com\r\nATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:testuser@\r\n example.com\r\nCREATED:20081024T181749Z\r\nDTSTAMP:20081024T183142Z\r\nORGANIZER;CN=Test User:mailto:testuser
 @example.com\r\nSEQUENCE:5\r\nSUMMARY:testing\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n')),
+
+    ('all-day', getComp('BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nMETHOD:REQUEST\r\nPRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN\r\nBEGIN:VTIMEZONE\r\nTZID:US/Pacific\r\nBEGIN:STANDARD\r\nDTSTART:20071104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nTZNAME:PST\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:20070311T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nTZNAME:PDT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:C7C037CC-1485-461B-8866-777C662C5930\r\nDTSTART;TZID=US/Pacific:20081025\r\nATTENDEE;CN=test at systemcall.com;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSV\r\n P=TRUE:mailto:test at systemcall.com\r\nATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:testuser@\r\n example.com\r\nCREATED:20081024T181749Z\r\nDTSTAMP:20081024T183142Z\r\nORGANIZER;CN=Test User:mailto:testuser at example.com\r\nSEQUENCE:5\r\nSUMMARY:testing\r\nTRANSP:OPAQUE\
 r\nEND:VEVENT\r\nEND:VCALENDAR\r\n')),
+
+    ('instantaneous', getComp('BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nMETHOD:REQUEST\r\nPRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN\r\nBEGIN:VTIMEZONE\r\nTZID:US/Pacific\r\nBEGIN:STANDARD\r\nDTSTART:20071104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nTZNAME:PST\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:20070311T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nTZNAME:PDT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:C7C037CC-1485-461B-8866-777C662C5930\r\nDTSTART;TZID=US/Pacific:20081025T131500\r\nDTEND;TZID=US/Pacific:20081025T131500\r\nATTENDEE;CN=test at systemcall.com;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSV\r\n P=TRUE:mailto:test at systemcall.com\r\nATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:testuser@\r\n example.com\r\nCREATED:20081024T181749Z\r\nDTSTAMP:20081024T183142Z\r\nORGANIZER;CN=Test User:mailto:testuser at example.
 com\r\nSEQUENCE:5\r\nSUMMARY:testing\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n')),
+
+    ('cross-timezone', getComp('BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nMETHOD:REQUEST\r\nPRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN\r\nBEGIN:VTIMEZONE\r\nTZID:US/Pacific\r\nBEGIN:STANDARD\r\nDTSTART:20071104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nTZNAME:PST\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:20070311T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nTZNAME:PDT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nBEGIN:VTIMEZONE\r\nTZID:US/Eastern\r\nBEGIN:STANDARD\r\nDTSTART:20071104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nTZNAME:EST\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:20070311T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nTZNAME:EDT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:C7C037CC-1485-461B-8866-777C662C5930\r\nDTSTART;TZID=US/Pacific:
 20081025T110500\r\nDTEND;TZID=US/Eastern:20081025T181500\r\nATTENDEE;CN=test at systemcall.com;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSV\r\n P=TRUE:mailto:test at systemcall.com\r\nATTENDEE;CN=Test User;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:testuser@\r\n example.com\r\nCREATED:20081024T181749Z\r\nDTSTAMP:20081024T183142Z\r\nORGANIZER;CN=Test User:mailto:testuser at example.com\r\nSEQUENCE:5\r\nSUMMARY:testing\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n')),
+
+
+)
+
+localeDir = os.path.join(os.path.dirname(__file__), "data", "locales")
+
+class LocalizationTests(TestCase):
+
+    def test_BasicStringLocalization(self):
+
+        with translationTo('pig', localeDir=localeDir):
+
+            self.assertEquals(_("All day"), "Allway ayday")
+
+            self.assertEquals(_("%(startTime)s to %(endTime)s") %
+                { 'startTime' : 'a', 'endTime' : 'b' },
+                "a otay b"
+            )
+
+    def test_TimeFormattingAMPM(self):
+
+        with translationTo('en', localeDir=localeDir) as t:
+
+            self.assertEquals(t.dtTime(time(0,0)), "12:00 AM")
+            self.assertEquals(t.dtTime(time(12,0)), "12:00 PM")
+            self.assertEquals(t.dtTime(time(23,59)), "11:59 PM")
+            self.assertEquals(t.dtTime(time(6,5)), "6:05 AM")
+            self.assertEquals(t.dtTime(time(16,5)), "4:05 PM")
+
+    def test_TimeFormatting24Hour(self):
+
+        with translationTo('pig', localeDir=localeDir) as t:
+
+            self.assertEquals(t.dtTime(time(0,0)), "00:00")
+            self.assertEquals(t.dtTime(time(12,0)), "12:00")
+            self.assertEquals(t.dtTime(time(23,59)), "23:59")
+            self.assertEquals(t.dtTime(time(6,5)), "06:05")
+            self.assertEquals(t.dtTime(time(16,5)), "16:05")
+
+    def test_CalendarFormatting(self):
+
+        with translationTo('en', localeDir=localeDir) as t:
+
+            comp = data[0][1]
+            self.assertEquals(t.date(comp), "Saturday, October 25, 2008")
+            self.assertEquals(t.time(comp),
+                (u'9:15 AM to 10:15 AM PDT', u'1 hour 1 second'))
+
+            comp = data[1][1]
+            self.assertEquals(t.time(comp),
+                (u'1:15 PM to 3:15 PM PDT', u'2 hours 2 seconds'))
+
+            comp = data[2][1]
+            self.assertEquals(t.time(comp),
+                (u'11:05 AM to 2:15 PM PDT', u'3 hours 10 minutes'))
+
+            comp = data[3][1]
+            self.assertEquals(t.time(comp),
+                ("", u'All day'))
+
+            comp = data[4][1]
+            self.assertEquals(t.time(comp),
+                (u'1:15 PM PDT', ""))
+
+            comp = data[5][1]
+            self.assertEquals(t.time(comp),
+                (u'11:05 AM PDT to 6:15 PM EDT', u'4 hours 10 minutes'))
+
+        with translationTo('pig', localeDir=localeDir) as t:
+
+            comp = data[0][1]
+            self.assertEquals(t.date(comp), 'Aturdaysay, Octoberway 25, 2008')
+            self.assertEquals(t.time(comp),
+                (u'09:15 otay 10:15 PDT', u'1 ourhay 1 econdsay'))
+
+            comp = data[1][1]
+            self.assertEquals(t.time(comp),
+                (u'13:15 otay 15:15 PDT', u'2 ourshay 2 econdsay'))
+
+            comp = data[2][1]
+            self.assertEquals(t.time(comp),
+                (u'11:05 otay 14:15 PDT', u'3 ourshay 10 inutesmay'))
+
+            comp = data[3][1]
+            self.assertEquals(t.time(comp),
+                ("", u'Allway ayday'))
+
+            comp = data[4][1]
+            self.assertEquals(t.time(comp),
+                (u'13:15 PDT', ""))
+
+            comp = data[5][1]
+            self.assertEquals(t.time(comp),
+                (u'11:05 PDT otay 18:15 EDT', u'4 ourshay 10 inutesmay'))

Modified: CalendarServer/trunk/twistedcaldav/test/test_mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_mail.py	2008-11-06 01:28:37 UTC (rev 3324)
+++ CalendarServer/trunk/twistedcaldav/test/test_mail.py	2008-11-06 01:45:12 UTC (rev 3325)
@@ -57,7 +57,7 @@
 
         # Make sure a known token *is* processed
         token = self.handler.db.createToken("mailto:user01 at example.com",
-            "mailto:user02 at example.com")
+            "mailto:user02 at example.com", "1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C")
         calBody = template % token
         organizer, attendee, calendar, msgId = self.handler.processDSN(calBody,
             "xyzzy", echo)
@@ -78,7 +78,7 @@
         self.assertEquals(result, None)
 
         # Make sure a known token *is* processed
-        self.handler.db.createToken("mailto:user01 at example.com", "mailto:xyzzy at example.com", token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
+        self.handler.db.createToken("mailto:user01 at example.com", "mailto:xyzzy at example.com", icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C", token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
         organizer, attendee, calendar, msgId = self.handler.processReply(msg,
             echo)
         self.assertEquals(organizer, 'mailto:user01 at example.com')
@@ -90,7 +90,7 @@
             file(os.path.join(self.dataDir, 'reply_missing_organizer')).read()
         )
         # stick the token in the database first
-        self.handler.db.createToken("mailto:user01 at example.com", "mailto:xyzzy at example.com", token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
+        self.handler.db.createToken("mailto:user01 at example.com", "mailto:xyzzy at example.com", icaluid="1E71F9C8-AEDA-48EB-98D0-76E898F6BB5C", token="d7cdf68d-8b73-4df1-ad3b-f08002fb285f")
 
         organizer, attendee, calendar, msgId = self.handler.processReply(msg,
             echo)
@@ -105,9 +105,9 @@
     def test_tokens(self):
         self.assertEquals(self.db.lookupByToken("xyzzy"), None)
 
-        token = self.db.createToken("organizer", "attendee")
-        self.assertEquals(self.db.getToken("organizer", "attendee"), token)
+        token = self.db.createToken("organizer", "attendee", "icaluid")
+        self.assertEquals(self.db.getToken("organizer", "attendee", "icaluid"), token)
         self.assertEquals(self.db.lookupByToken(token),
-            ("organizer", "attendee"))
+            ("organizer", "attendee", "icaluid"))
         self.db.deleteToken(token)
         self.assertEquals(self.db.lookupByToken(token), None)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081105/6529df62/attachment-0001.html>


More information about the calendarserver-changes mailing list