summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJim Fulton <jim@zope.com>2005-11-18 19:31:10 +0000
committerJim Fulton <jim@zope.com>2005-11-18 19:31:10 +0000
commit1ce588f06a2f35ac13cdc8c446ac816ec62c9a47 (patch)
treebf3b08397b1bdddc763f891055fab9a39387cbb3
downloadzope-i18n-monolithic-zope3-jim-i18n-dev.tar.gz
Added an API for collating text and a fallback implementation.monolithic-zope3-jim-i18n-dev
(Apps that really care will probably use an ICU-based adapter that we will provide soonish.)
-rw-r--r--interfaces/locales.py639
-rw-r--r--locales/fallbackcollator.py33
-rw-r--r--locales/fallbackcollator.txt63
-rw-r--r--locales/tests/test_fallbackcollator.py25
4 files changed, 760 insertions, 0 deletions
diff --git a/interfaces/locales.py b/interfaces/locales.py
new file mode 100644
index 0000000..c0f77d1
--- /dev/null
+++ b/interfaces/locales.py
@@ -0,0 +1,639 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Interfaces related to Locales
+
+$Id$
+"""
+import re
+from zope.interface import Interface, Attribute
+from zope.schema import \
+ Field, Text, TextLine, Int, Bool, Tuple, List, Dict, Date
+from zope.schema import Container, Choice
+
+class ILocaleProvider(Interface):
+ """This interface is our connection to the Zope 3 service. From it
+ we can request various Locale objects that can perform all sorts of
+ fancy operations.
+
+ This service will be singelton global service, since it doe not make much
+ sense to have many locale facilities, especially since this one will be so
+ complete, since we will the ICU XML Files as data. """
+
+ def loadLocale(language=None, country=None, variant=None):
+ """Load the locale with the specs that are given by the arguments of
+ the method. Note that the LocaleProvider must know where to get the
+ locales from."""
+
+ def getLocale(language=None, country=None, variant=None):
+ """Get the Locale object for a particular language, country and
+ variant."""
+
+
+class ILocaleIdentity(Interface):
+ """Identity information class for ILocale objects.
+
+ Three pieces of information are required to identify a locale:
+
+ o language -- Language in which all of the locale text information are
+ returned.
+
+ o script -- Script in which all of the locale text information are
+ returned.
+
+ o territory -- Territory for which the locale's information are
+ appropriate. None means all territories in which language is spoken.
+
+ o variant -- Sometimes there are regional or historical differences even
+ in a certain country. For these cases we use the variant field. A good
+ example is the time before the Euro in Germany for example. Therefore
+ a valid variant would be 'PREEURO'.
+
+ Note that all of these attributes are read-only once they are set (usually
+ done in the constructor)!
+
+ This object is also used to uniquely identify a locale.
+ """
+
+ language = TextLine(
+ title = u"Language Type",
+ description = u"The language for which a locale is applicable.",
+ constraint = re.compile(r'[a-z]{2}').match,
+ required = True,
+ readonly = True)
+
+ script = TextLine(
+ title = u"Script Type",
+ description = u"""The script for which the language/locale is
+ applicable.""",
+ constraint = re.compile(r'[a-z]*').match)
+
+ territory = TextLine(
+ title = u"Territory Type",
+ description = u"The territory for which a locale is applicable.",
+ constraint = re.compile(r'[A-Z]{2}').match,
+ required = True,
+ readonly = True)
+
+ variant = TextLine(
+ title = u"Variant Type",
+ description = u"The variant for which a locale is applicable.",
+ constraint = re.compile(r'[a-zA-Z]*').match,
+ required = True,
+ readonly = True)
+
+ version = Field(
+ title = u"Locale Version",
+ description = u"The value of this field is an ILocaleVersion object.",
+ readonly = True)
+
+ def __repr__(self):
+ """Defines the representation of the id, which should be a compact
+ string that references the language, country and variant."""
+
+
+class ILocaleVersion(Interface):
+ """Represents the version of a locale.
+
+ The locale version is part of the ILocaleIdentity object.
+ """
+
+ number = TextLine(
+ title = u"Version Number",
+ description = u"The version number of the locale.",
+ constraint = re.compile(r'^([0-9].)*[0-9]$').match,
+ required = True,
+ readonly = True)
+
+ generationDate = Date(
+ title = u"Generation Date",
+ description = u"Specifies the creation date of the locale.",
+ constraint = lambda date: date < datetime.now(),
+ readonly = True)
+
+ notes = Text(
+ title = u"Notes",
+ description = u"Some release notes for the version of this locale.",
+ readonly = True)
+
+
+class ILocaleDisplayNames(Interface):
+ """Localized Names of common text strings.
+
+ This object contains localized strings for many terms, including
+ language, script and territory names. But also keys and types used
+ throughout the locale object are localized here.
+ """
+
+ languages = Dict(
+ title = u"Language type to translated name",
+ key_type = TextLine(title=u"Language Type"),
+ value_type = TextLine(title=u"Language Name"))
+
+ scripts = Dict(
+ title = u"Script type to script name",
+ key_type = TextLine(title=u"Script Type"),
+ value_type = TextLine(title=u"Script Name"))
+
+ territories = Dict(
+ title = u"Territory type to translated territory name",
+ key_type = TextLine(title=u"Territory Type"),
+ value_type = TextLine(title=u"Territory Name"))
+
+ variants = Dict(
+ title = u"Variant type to name",
+ key_type = TextLine(title=u"Variant Type"),
+ value_type = TextLine(title=u"Variant Name"))
+
+ keys = Dict(
+ title = u"Key type to name",
+ key_type = TextLine(title=u"Key Type"),
+ value_type = TextLine(title=u"Key Name"))
+
+ types = Dict(
+ title = u"Type type and key to localized name",
+ key_type = Tuple(title=u"Type Type and Key"),
+ value_type = TextLine(title=u"Type Name"))
+
+
+class ILocaleTimeZone(Interface):
+ """Represents and defines various timezone information. It mainly manages
+ all the various names for a timezone and the cities contained in it.
+
+ Important: ILocaleTimeZone objects are not intended to provide
+ implementations for the standard datetime module timezone support. They
+ are merily used for Locale support.
+ """
+
+ type = TextLine(
+ title = u"Time Zone Type",
+ description = u"Standard name of the timezone for unique referencing.",
+ required = True,
+ readonly = True)
+
+ cities = List(
+ title = u"Cities",
+ description = u"Cities in Timezone",
+ value_type = TextLine(title=u"City Name"),
+ required = True,
+ readonly = True)
+
+
+ names = Dict(
+ title = u"Time Zone Names",
+ description = u"Various names of the timezone.",
+ key_type = Choice(
+ title = u"Time Zone Name Type",
+ values = (u'generic', u'standard', u'daylight')),
+ value_type = Tuple(title=u"Time Zone Name and Abbreviation",
+ min_length=2, max_length=2),
+ required = True,
+ readonly = True)
+
+
+class ILocaleFormat(Interface):
+ """Specifies a format for a particular type of data."""
+
+ type = TextLine(
+ title=u"Format Type",
+ description=u"The name of the format",
+ required = False,
+ readonly = True)
+
+ displayName = TextLine(
+ title = u"Display Name",
+ description = u"Name of the calendar, for example 'gregorian'.",
+ required = False,
+ readonly = True)
+
+ pattern = TextLine(
+ title = u"Format Pattern",
+ description = u"The pattern that is used to format the object.",
+ required = True,
+ readonly = True)
+
+
+class ILocaleFormatLength(Interface):
+ """The format length describes a class of formats."""
+
+ type = Choice(
+ title = u"Format Length Type",
+ description = u"Name of the format length",
+ values = (u'full', u'long', u'medium', u'short')
+ )
+
+ default = TextLine(
+ title=u"Default Format",
+ description=u"The name of the defaulkt format.")
+
+ formats = Dict(
+ title = u"Formats",
+ description = u"Maps format types to format objects",
+ key_type = TextLine(title = u"Format Type"),
+ value_type = Field(
+ title = u"Format Object",
+ description = u"Values are ILocaleFormat objects."),
+ required = True,
+ readonly = True)
+
+
+class ILocaleCalendar(Interface):
+ """There is a massive amount of information contained in the calendar,
+ which made it attractive to be added."""
+
+ type = TextLine(
+ title=u"Calendar Type",
+ description=u"Name of the calendar, for example 'gregorian'.")
+
+ months = Dict(
+ title = u"Month Names",
+ description = u"A mapping of all month names and abbreviations",
+ key_type = Int(title=u"Type", min=1, max=12),
+ value_type = Tuple(title=u"Month Name and Abbreviation",
+ min_length=2, max_length=2))
+
+ days = Dict(
+ title=u"Weekdays Names",
+ description = u"A mapping of all month names and abbreviations",
+ key_type = Choice(title=u"Type",
+ values=(u'sun', u'mon', u'tue', u'wed',
+ u'thu', u'fri', u'sat')),
+ value_type = Tuple(title=u"Weekdays Name and Abbreviation",
+ min_length=2, max_length=2))
+
+ week = Dict(
+ title=u"Week Information",
+ description = u"Contains various week information",
+ key_type = Choice(
+ title=u"Type",
+ description=u"""
+ Varies Week information:
+
+ - 'minDays' is just an integer between 1 and 7.
+
+ - 'firstDay' specifies the first day of the week by integer.
+
+ - The 'weekendStart' and 'weekendEnd' are tuples of the form
+ (weekDayNumber, datetime.time)
+ """,
+ values=(u'minDays', u'firstDay',
+ u'weekendStart', u'weekendEnd')))
+
+ am = TextLine(title=u"AM String")
+
+ pm = TextLine(title=u"PM String")
+
+ eras = Dict(
+ title = u"Era Names",
+ key_type = Int(title=u"Type", min=0),
+ value_type = Tuple(title=u"Era Name and Abbreviation",
+ min_length=2, max_length=2))
+
+ defaultDateFormat = TextLine(title=u"Default Date Format Type")
+
+ dateFormats = Dict(
+ title=u"Date Formats",
+ description = u"Contains various Date Formats.",
+ key_type = Choice(
+ title=u"Type",
+ description = u"Name of the format length",
+ values = (u'full', u'long', u'medium', u'short')),
+ value_type = Field(title=u"ILocaleFormatLength object"))
+
+ defaultTimeFormat = TextLine(title=u"Default Time Format Type")
+
+ timeFormats = Dict(
+ title=u"Time Formats",
+ description = u"Contains various Time Formats.",
+ key_type = Choice(
+ title=u"Type",
+ description = u"Name of the format length",
+ values = (u'full', u'long', u'medium', u'short')),
+ value_type = Field(title=u"ILocaleFormatLength object"))
+
+ defaultDateTimeFormat = TextLine(title=u"Default Date-Time Format Type")
+
+ dateTimeFormats = Dict(
+ title=u"Date-Time Formats",
+ description = u"Contains various Date-Time Formats.",
+ key_type = Choice(
+ title=u"Type",
+ description = u"Name of the format length",
+ values = (u'full', u'long', u'medium', u'short')),
+ value_type = Field(title=u"ILocaleFormatLength object"))
+
+ def getMonthNames():
+ """Return a list of month names."""
+
+ def getMonthTypeFromName(name):
+ """Return the type of the month with the right name."""
+
+ def getMonthAbbreviations():
+ """Return a list of month abbreviations."""
+
+ def getMonthTypeFromAbbreviation(abbr):
+ """Return the type of the month with the right abbreviation."""
+
+ def getDayNames():
+ """Return a list of weekday names."""
+
+ def getDayTypeFromName(name):
+ """Return the id of the weekday with the right name."""
+
+ def getDayAbbr():
+ """Return a list of weekday abbreviations."""
+
+ def getDayTypeFromAbbr(abbr):
+ """Return the id of the weekday with the right abbr."""
+
+ def isWeekend(datetime):
+ """Determines whether a the argument lies in a weekend."""
+
+ def getFirstDayName():
+ """Return the the type of the first day in the week."""
+
+
+class ILocaleDates(Interface):
+ """This object contains various data about dates, times and time zones."""
+
+ localizedPatternChars = TextLine(
+ title = u"Localized Pattern Characters",
+ description = u"Localized pattern characters used in dates and times")
+
+ calendars = Dict(
+ title = u"Calendar type to ILocaleCalendar",
+ key_type = Choice(
+ title=u"Calendar Type",
+ values=(u'gregorian',
+ u'arabic',
+ u'chinese',
+ u'civil-arabic',
+ u'hebrew',
+ u'japanese',
+ u'thai-buddhist')),
+ value_type=Field(title=u"Calendar",
+ description=u"This is a ILocaleCalendar object."))
+
+ timezones = Dict(
+ title=u"Time zone type to ILocaleTimezone",
+ key_type=TextLine(title=u"Time Zone type"),
+ value_type=Field(title=u"Time Zone",
+ description=u"This is a ILocaleTimeZone object."))
+
+ def getFormatter(category, length=None, name=None, calendar=u'gregorian'):
+ """Get a date/time formatter.
+
+ `category` must be one of 'date', 'dateTime', 'time'.
+
+ The 'length' specifies the output length of the value. The allowed
+ values are: 'short', 'medium', 'long' and 'full'. If no length was
+ specified, the default length is chosen.
+ """
+
+
+class ILocaleCurrency(Interface):
+ """Defines a particular currency."""
+
+ type = TextLine(title=u'Type')
+
+ symbol = TextLine(title=u'Symbol')
+
+ displayName = TextLine(title=u'Official Name')
+
+ symbolChoice = Bool(title=u'Symbol Choice')
+
+class ILocaleNumbers(Interface):
+ """This object contains various data about numbers and currencies."""
+
+ symbols = Dict(
+ title = u"Number Symbols",
+ key_type = Choice(
+ title = u"Format Name",
+ values = (u'decimal', u'group', u'list', u'percentSign',
+ u'nativeZeroDigit', u'patternDigit', u'plusSign',
+ u'minusSign', u'exponential', u'perMille',
+ u'infinity', u'nan')),
+ value_type=TextLine(title=u"Symbol"))
+
+ defaultDecimalFormat = TextLine(title=u"Default Decimal Format Type")
+
+ decimalFormats = Dict(
+ title=u"Decimal Formats",
+ description = u"Contains various Decimal Formats.",
+ key_type = Choice(
+ title=u"Type",
+ description = u"Name of the format length",
+ values = (u'full', u'long', u'medium', u'short')),
+ value_type = Field(title=u"ILocaleFormatLength object"))
+
+ defaultScientificFormat = TextLine(title=u"Default Scientific Format Type")
+
+ scientificFormats = Dict(
+ title=u"Scientific Formats",
+ description = u"Contains various Scientific Formats.",
+ key_type = Choice(
+ title=u"Type",
+ description = u"Name of the format length",
+ values = (u'full', u'long', u'medium', u'short')),
+ value_type = Field(title=u"ILocaleFormatLength object"))
+
+ defaultPercentFormat = TextLine(title=u"Default Percent Format Type")
+
+ percentFormats = Dict(
+ title=u"Percent Formats",
+ description = u"Contains various Percent Formats.",
+ key_type = Choice(
+ title=u"Type",
+ description = u"Name of the format length",
+ values = (u'full', u'long', u'medium', u'short')),
+ value_type = Field(title=u"ILocaleFormatLength object"))
+
+ defaultCurrencyFormat = TextLine(title=u"Default Currency Format Type")
+
+ currencyFormats = Dict(
+ title=u"Currency Formats",
+ description = u"Contains various Currency Formats.",
+ key_type = Choice(
+ title=u"Type",
+ description = u"Name of the format length",
+ values = (u'full', u'long', u'medium', u'short')),
+ value_type = Field(title=u"ILocaleFormatLength object"))
+
+ currencies = Dict(
+ title=u"Currencies",
+ description = u"Contains various Currency data.",
+ key_type = TextLine(
+ title=u"Type",
+ description = u"Name of the format length"),
+ value_type = Field(title=u"ILocaleCurrency object"))
+
+
+ def getFormatter(category, length=None, name=u''):
+ """Get the NumberFormat based on the category, length and name of the
+ format.
+
+ The 'category' specifies the type of number format you would like to
+ have. The available options are: 'decimal', 'percent', 'scientific',
+ 'currency'.
+
+ The 'length' specifies the output length of the number. The allowed
+ values are: 'short', 'medium', 'long' and 'full'. If no length was
+ specified, the default length is chosen.
+
+ Every length can have actually several formats. In this case these
+ formats are named and you can specify the name here. If no name was
+ specified, the first unnamed format is chosen.
+ """
+
+ def getDefaultCurrency():
+ """Get the default currency."""
+
+_orientations = [u"left-to-right", u"right-to-left",
+ u"top-to-bottom", u"bottom-to-top"]
+class ILocaleOrientation(Interface):
+ """Information about the orientation of text."""
+
+ characters = Choice(
+ title = u"Orientation of characters",
+ values = _orientations,
+ default = u"left-to-right"
+ )
+
+ lines = Choice(
+ title = u"Orientation of characters",
+ values = _orientations,
+ default = u"top-to-bottom"
+ )
+
+class ILocale(Interface):
+ """This class contains all important information about the locale.
+
+ Usually a Locale is identified using a specific language, country and
+ variant. However, the country and variant are optional, so that a lookup
+ hierarchy develops. It is easy to recognize that a locale that is missing
+ the variant is more general applicable than the one with the variant.
+ Therefore, if a specific Locale does not contain the required information,
+ it should look one level higher. There will be a root locale that
+ specifies none of the above identifiers.
+ """
+
+ id = Field(
+ title = u"Locale identity",
+ description = u"ILocaleIdentity object identifying the locale.",
+ required = True,
+ readonly = True)
+
+ displayNames = Field(
+ title = u"Display Names",
+ description = u"""ILocaleDisplayNames object that contains localized
+ names.""")
+
+ dates = Field(
+ title = u"Dates",
+ description = u"ILocaleDates object that contains date/time data.")
+
+ numbers = Field(
+ title = u"Numbers",
+ description = u"ILocaleNumbers object that contains number data.")
+
+ orientation = Field(
+ title = u"Orientation",
+ description = u"ILocaleOrientation with text orientation info.")
+
+ delimiters = Dict(
+ title=u"Delimiters",
+ description = u"Contains various Currency data.",
+ key_type = Choice(
+ title=u"Delimiter Type",
+ description = u"Delimiter name.",
+ values=(u'quotationStart', u'quotationEnd',
+ u'alternateQuotationStart',
+ u'alternateQuotationEnd')),
+ value_type = Field(title=u"Delimiter symbol"))
+
+ def getLocaleID():
+ """Return a locale id as specified in the LDML specification"""
+
+
+class ILocaleInheritance(Interface):
+ """Locale inheritance support.
+
+ Locale-related objects implementing this interface are able to ask for its
+ inherited self. For example, 'en_US.dates.monthNames' can call on itself
+ 'getInheritedSelf()' and get the value for 'en.dates.monthNames'.
+ """
+
+ __parent__ = Attribute("The parent in the location hierarchy")
+
+ __name__ = TextLine(
+ title = u"The name within the parent",
+ description=u"""The parent can be traversed with this name to get
+ the object.""")
+
+ def getInheritedSelf():
+ """Return itself but in the next higher up Locale."""
+
+
+class IAttributeInheritance(ILocaleInheritance):
+ """Provides inheritance properties for attributes"""
+
+ def __setattr__(name, value):
+ """Set a new attribute on the object.
+
+ When a value is set on any inheritance-aware object and the value
+ also implements ILocaleInheritance, then we need to set the
+ '__parent__' and '__name__' attribute on the value.
+ """
+
+ def __getattributes__(name):
+ """Return the value of the attribute with the specified name.
+
+ If an attribute is not found or is None, the next higher up Locale
+ object is consulted."""
+
+
+class IDictionaryInheritance(ILocaleInheritance):
+ """Provides inheritance properties for dictionary keys"""
+
+ def __setitem__(key, value):
+ """Set a new item on the object.
+
+ Here we assume that the value does not require any inheritance, so
+ that we do not set '__parent__' or '__name__' on the value.
+ """
+
+ def __getitem__(key):
+ """Return the value of the item with the specified name.
+
+ If an key is not found or is None, the next higher up Locale
+ object is consulted.
+ """
+
+class ICollator(Interface):
+ """Provide support for collating text strings
+
+ This interface will typically be provided by adapting a locale.
+ """
+
+ def key(text):
+ """Return a collation key for the given text.
+ """
+
+ def cmp(text1, text2):
+ """Compare two text strings.
+
+ The return value is negative if text1 < text2, 0 is they are
+ equal, and positive if text1 > text2.
+ """
+
+
diff --git a/locales/fallbackcollator.py b/locales/fallbackcollator.py
new file mode 100644
index 0000000..ab1ddda
--- /dev/null
+++ b/locales/fallbackcollator.py
@@ -0,0 +1,33 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Fallback collator
+
+$Id$
+"""
+
+from unicodedata import normalize
+
+class FallbackCollator:
+
+ def __init__(self, locale):
+ pass
+
+ def key(self, s):
+ s = normalize('NFKC', s)
+ return s.lower(), s
+
+ def cmp(self, s1, s2):
+ return cmp(self.key(s1), self.key(s2))
+
+
diff --git a/locales/fallbackcollator.txt b/locales/fallbackcollator.txt
new file mode 100644
index 0000000..9457be2
--- /dev/null
+++ b/locales/fallbackcollator.txt
@@ -0,0 +1,63 @@
+Fallback Collator
+=================
+
+The zope.i18n.interfaces.locales.ICollator interface defines an API
+for collating text. Why is this important? Simply sorting unicode
+strings doesn't provide an ordering that users in a given locale will
+fine useful. Various languages have text sorting conventions that
+don't agree with the ordering of unicode code points. (This is even
+true for English. :)
+
+Text collation is a fairly involved process. Systems that need this,
+will likely use something like ICU
+(http://www-306.ibm.com/software/globalization/icu,
+http://pyicu.osafoundation.org/). We don't want to introduce a
+dependency on ICU and this time, so we are providing a fallback
+collator that:
+
+- Provides an implementation of the ICollator interface that can be
+ used for development, and
+
+- Provides a small amount of value, at least for English speakers. :)
+
+Application code should obtain a collator by adapting a locale to
+ICollator. Here we just call the collator factory with None. The
+fallback collator doesn't actually use the locale, although
+application code should certainly *not* count on this.
+
+ >>> import zope.i18n.locales.fallbackcollator
+ >>> collator = zope.i18n.locales.fallbackcollator.FallbackCollator(None)
+
+Now, we can pass the collator's key method to sort functions to sort
+strings in a slightly friendly way:
+
+ >>> sorted([u'Sam', u'sally', u'Abe', u'alice', u'Terry', u'tim'],
+ ... key=collator.key)
+ [u'Abe', u'alice', u'sally', u'Sam', u'Terry', u'tim']
+
+
+The collator has a very simple algorithm. It normalizes strings and
+then returns a tuple with the result of lower-casing the normalized
+string and the normalized string. We can see this by calling the key
+method, which converts unicode strings to collation keys:
+
+ >>> collator.key(u'Sam')
+ (u'sam', u'Sam')
+
+ >>> collator.key(u'\xc6\xf8a\u030a')
+ (u'\xe6\xf8\xe5', u'\xc6\xf8\xe5')
+
+There is also a cmp function for comparing strings:
+
+ >>> collator.cmp(u'Terry', u'sally')
+ 1
+
+
+ >>> collator.cmp(u'sally', u'Terry')
+ -1
+
+ >>> collator.cmp(u'terry', u'Terry')
+ 1
+
+ >>> collator.cmp(u'terry', u'terry')
+ 0
diff --git a/locales/tests/test_fallbackcollator.py b/locales/tests/test_fallbackcollator.py
new file mode 100644
index 0000000..071652d
--- /dev/null
+++ b/locales/tests/test_fallbackcollator.py
@@ -0,0 +1,25 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import unittest
+from zope.testing import doctest
+
+def test_suite():
+ return unittest.TestSuite((
+ doctest.DocFileSuite('../fallbackcollator.txt'),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+