Index: django/contrib/localflavor/cn/cn_provinces.py
===================================================================
--- django/contrib/localflavor/cn/cn_provinces.py	(revision 0)
+++ django/contrib/localflavor/cn/cn_provinces.py	(revision 0)
@@ -0,0 +1,49 @@
+#coding=utf-8
+
+"""
+An alphabetical list of provinces for use as `choices` in a formfield.
+
+Reference:
+http://en.wikipedia.org/wiki/ISO_3166-2:CN
+http://en.wikipedia.org/wiki/Province_%28China%29
+http://en.wikipedia.org/wiki/Direct-controlled_municipality
+http://en.wikipedia.org/wiki/Autonomous_regions_of_China
+"""
+
+
+CN_PROVINCE_CHOICES = (
+    ("Anhui", u"安徽"),
+    ("Beijing", u"北京"),
+    ("Chongqing", u"重庆"),
+    ("Fujian", u"福建"),
+    ("Gansu", u"甘肃"),
+    ("Guangdong", u"广东"),
+    ("Guangxi", u"广西壮族自治区"),
+    ("Guizhou", u"贵州"),
+    ("Hainan", u"海南"),
+    ("Hebei", u"河北"),
+    ("Heilongjiang", u"黑龙江"),
+    ("Henan", u"河南"),
+    ("Hong Kong", u"香港"),
+    ("Hubei", u"湖北"),
+    ("Hunan", u"湖南"),
+    ("Jiangsu", u"江苏"),
+    ("Jiangxi", u"江西"),
+    ("Jilin", u"吉林"),
+    ("Liaoning", u"辽宁"),
+    ("Macao", u"澳门"),
+    ("Nei Mongol", u"内蒙古自治区"),
+    ("Ningxia", u"宁夏回族自治区"),
+    ("Qinghai", u"青海"),
+    ("Shaanxi", u"陕西"),
+    ("Shandong", u"山东"),
+    ("Shanghai", u"上海"),
+    ("Shanxi", u"山西"),
+    ("Sichuan", u"四川"),
+    ("Taiwan", u"台湾"),
+    ("Tianjin", u"天津"),
+    ("Xinjiang", u"新疆维吾尔自治区"),
+    ("Xizang", u"西藏自治区"),
+    ("Yunnan", u"云南"),
+    ("Zhejiang", u"浙江"),
+)
Index: django/contrib/localflavor/cn/__init__.py
===================================================================
Index: django/contrib/localflavor/cn/cn_location_code.py
===================================================================
--- django/contrib/localflavor/cn/cn_location_code.py	(revision 0)
+++ django/contrib/localflavor/cn/cn_location_code.py	(revision 0)
@@ -0,0 +1,46 @@
+#coding=utf-8
+"""
+This file contains a tuple of valid Chinese location codes as was
+described in GB/T2260-1995.
+
+Please note that this tuple is not complete and detailed. We do not want to
+check the last four digits of the location code, which might be changed
+(relatively) frequently.
+"""
+
+CN_LOCATION_CODES = (
+     11 ,       # Beijing
+     12 ,       # Tianjin
+     13 ,       # Hebei
+     14 ,       # Shanxi
+     15 ,       # Nei Mongol
+     21 ,       # Liaoning
+     22 ,       # Jilin
+     23 ,       # Heilongjiang
+     31 ,       # Shanghai
+     32 ,       # Jiangsu
+     33 ,       # Zhejiang
+     34 ,       # Anhui
+     35 ,       # Fujian
+     36 ,       # Jiangxi
+     37 ,       # Shandong
+     41 ,       # Henan
+     42 ,       # Hubei
+     43 ,       # Hunan
+     44 ,       # Guangdong
+     45 ,       # Guangxi
+     46 ,       # Hainan
+     50 ,       # Chongqing
+     51 ,       # Sichuan
+     52 ,       # Guizhou
+     53 ,       # Yunnan
+     54 ,       # Xizang
+     61 ,       # Shaanxi
+     62 ,       # Gansu
+     63 ,       # Qinghai
+     64 ,       # Ningxia
+     65 ,       # Xinjiang
+     71 ,       # Taiwan
+     81 ,       # Hong Kong
+     91 ,       # Macao
+)
Index: django/contrib/localflavor/cn/forms.py
===================================================================
--- django/contrib/localflavor/cn/forms.py	(revision 0)
+++ django/contrib/localflavor/cn/forms.py	(revision 0)
@@ -0,0 +1,177 @@
+#coding=utf-8
+"""
+Chinese-specific form helpers
+"""
+import re
+
+from django.forms import ValidationError
+from django.forms.fields import CharField, RegexField, Select
+from django.utils.translation import ugettext_lazy as _
+
+
+__all__ = (
+    'CNProvinceSelect',
+    'CNPostCodeField',
+    'CNIDCardField',
+    'CNPhoneNumberField',
+    'CNCellNumberField',
+)
+
+
+ID_CARD_RE = r'^\d{15}(\d{2}[0-9xX])?$'
+POST_CODE_RE = r'^\d{6}$'
+PHONE_RE = r'^\d{3,4}-\d{7,8}(-\d+)?$'
+CELL_RE = r'^1[358]\d{9}$'
+
+
+class CNProvinceSelect(Select):
+    """
+    A select widget with list of Chinese provinces as choices.
+    """
+    def __init__(self, attrs=None):
+        from cn_provinces import CN_PROVINCE_CHOICES
+        super(CNProvinceSelect, self).__init__(
+            attrs, choices=CN_PROVINCE_CHOICES,
+        )
+
+
+class CNPostCodeField(RegexField):
+    """
+    A form field that validates as Chinese post code.
+    Valid code is XXXXXX where X is digit.
+    """
+    default_error_messages = {
+        'invalid': _(u'Enter a post code in the format XXXXXX.'),
+    }
+
+    def __init__(self, *args, **kwargs):
+        super(CNPostCodeField, self).__init__(POST_CODE_RE, *args, **kwargs)
+
+
+class CNIDCardField(CharField):
+    """
+    A form field that validates as Chinese Identification Card Number.
+
+    This field would check the following restrictions:
+        * the length could only be 15 or 18.
+        * if the length is 18, the last digit could be x or X.
+        * has a valid checksum.(length 18 only)
+        * has a valid birthdate.
+        * has a valid location.
+
+    The checksum algorithm is described in GB11643-1999.
+    """
+    default_error_messages = {
+        'invalid': _(u'ID Card Number consists of 15 or 18 digits.'),
+        'checksum': _(u'Invalid ID Card Number: Wrong checksum'),
+        'birthday': _(u'Invalid ID Card Number: Wrong birthdate'),
+        'location': _(u'Invalid ID Card Number: Wrong location code'),
+    }
+
+    def __init__(self, max_length=18, min_length=15, *args, **kwargs):
+        super(CNIDCardField, self).__init__(max_length, min_length, *args,
+                                         **kwargs)
+
+    def clean(self, value):
+        """
+        Check whether the input is a valid ID Card Number.
+        """
+        # Check the length of the ID card number.
+        super(CNIDCardField, self).clean(value)
+        if not value:
+            return u""
+        # Check whether this ID card number has valid format
+        if not re.match(ID_CARD_RE, value):
+            raise ValidationError(self.error_messages['invalid'])
+        # Check the birthday of the ID card number.
+        if not self.has_valid_birthday(value):
+            raise ValidationError(self.error_messages['birthday'])
+        # Check the location of the ID card number.
+        if not self.has_valid_location(value):
+            raise ValidationError(self.error_messages['location'])
+        # Check the checksum of the ID card number.
+        value = value.upper()
+        if not self.has_valid_checksum(value):
+            raise ValidationError(self.error_messages['checksum'])
+        return u'%s' % value
+
+    def has_valid_birthday(self, value):
+        """
+        This function would grab the birthdate from the ID card number and test
+        whether it is a valid date.
+        """
+        from datetime import datetime
+        if len(value) == 15:
+            # 1st generation ID card
+            time_string = value[6:12]
+            format_string = "%y%m%d"
+        else:
+            # 2nd generation ID card
+            time_string = value[6:14]
+            format_string = "%Y%m%d"
+        try:
+            datetime.strptime(time_string, format_string)
+            return True
+        except ValueError:
+            # invalid date
+            return False
+
+    def has_valid_location(self, value):
+        """
+        This function would check the first two digits in the ID card number to
+        see whether it is valid.
+        """
+        from cn_location_code import CN_LOCATION_CODES
+        location_code = int(value[:2])
+        return location_code in CN_LOCATION_CODES
+
+    def has_valid_checksum(self, value):
+        """
+        This function would check whether the last digit of the ID card number
+        is a valid checksum.
+        """
+        # If the length of the number is not 18, then the number is a 1st
+        # generation ID card number, and there is no checksum to be checked.
+        if len(value) != 18:
+            return True
+        checksum_index = sum(
+            map(
+                lambda a,b:a*(ord(b)-ord('0')),
+                (7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2),
+                value[:17],
+            ),
+        ) % 11
+        return '10X98765432'[checksum_index] == value[-1]
+
+
+class CNPhoneNumberField(RegexField):
+    """
+    A form field that validates as Chinese phone number
+    A valid phone number could be like:
+        010-55555555
+    Considering there might be extension phone numbers, so this could also be:
+        010-55555555-35
+    """
+    default_error_messages = {
+        'invalid': _(u'Enter a valid phone number.'),
+    }
+
+    def __init__(self, *args, **kwargs):
+        super(CNPhoneNumberField, self).__init__(PHONE_RE, *args, **kwargs)
+
+
+class CNCellNumberField(RegexField):
+    """
+    A form field that validates as Chinese cell number
+    A valid cell number could be like:
+        13012345678
+    We used a rough rule here, the first digit should be 1, the second could be
+    3, 5 and 8, the rest could be what so ever.
+    The length of the cell number should be 11.
+    """
+    default_error_messages = {
+        'invalid': _(u'Enter a valid cell number.'),
+    }
+
+    def __init__(self, *args, **kwargs):
+        super(CNCellNumberField, self).__init__(CELL_RE, *args, **kwargs)
Index: tests/regressiontests/forms/localflavor/cn.py
===================================================================
--- tests/regressiontests/forms/localflavor/cn.py	(revision 0)
+++ tests/regressiontests/forms/localflavor/cn.py	(revision 0)
@@ -0,0 +1,177 @@
+tests = r"""
+
+############################################################
+##################### CNProvinceSelect #####################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNProvinceSelect
+>>> s = CNProvinceSelect()
+>>> s.render('provinces', 'Hubei')
+u'<select name="provinces">
+    <option value="Anhui">\u5b89\u5fbd</option>
+    <option value="Beijing">\u5317\u4eac</option>
+    <option value="Chongqing">\u91cd\u5e86</option>
+    <option value="Fujian">\u798f\u5efa</option>
+    <option value="Gansu">\u7518\u8083</option>
+    <option value="Guangdong">\u5e7f\u4e1c</option>
+    <option value="Guangxi">\u5e7f\u897f\u58ee\u65cf\u81ea\u6cbb\u533a</option>
+    <option value="Guizhou">\u8d35\u5dde</option>
+    <option value="Hainan">\u6d77\u5357</option>
+    <option value="Hebei">\u6cb3\u5317</option>
+    <option value="Heilongjiang">\u9ed1\u9f99\u6c5f</option>
+    <option value="Henan">\u6cb3\u5357</option>
+    <option value="Hong Kong">\u9999\u6e2f</option>
+    <option value="Hubei" selected="selected">\u6e56\u5317</option>
+    <option value="Hunan">\u6e56\u5357</option>
+    <option value="Jiangsu">\u6c5f\u82cf</option>
+    <option value="Jiangxi">\u6c5f\u897f</option>
+    <option value="Jilin">\u5409\u6797</option>
+    <option value="Liaoning">\u8fbd\u5b81</option>
+    <option value="Macao">\u6fb3\u95e8</option>
+    <option value="Nei Mongol">\u5185\u8499\u53e4\u81ea\u6cbb\u533a</option>
+    <option value="Ningxia">\u5b81\u590f\u56de\u65cf\u81ea\u6cbb\u533a</option>
+    <option value="Qinghai">\u9752\u6d77</option>
+    <option value="Shaanxi">\u9655\u897f</option>
+    <option value="Shandong">\u5c71\u4e1c</option>
+    <option value="Shanghai">\u4e0a\u6d77</option>
+    <option value="Shanxi">\u5c71\u897f</option>
+    <option value="Sichuan">\u56db\u5ddd</option>
+    <option value="Taiwan">\u53f0\u6e7e</option>
+    <option value="Tianjin">\u5929\u6d25</option>
+    <option value="Xinjiang">\u65b0\u7586\u7ef4\u543e\u5c14\u81ea\u6cbb\u533a</option>
+    <option value="Xizang">\u897f\u85cf\u81ea\u6cbb\u533a</option>
+    <option value="Yunnan">\u4e91\u5357</option>
+    <option value="Zhejiang">\u6d59\u6c5f</option>
+</select>'
+
+############################################################
+##################### CNPostCodeField ######################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNPostCodeField
+>>> f = CNPostCodeField(required=False)
+>>> f.clean('')
+u''
+>>> f.clean('091209')
+u'091209'
+>>> f.clean('09120')
+Traceback (most recent call last):
+    ...
+ValidationError: [u'Enter a post code in the format XXXXXX.']
+>>> f.clean('09120916')
+Traceback (most recent call last):
+    ...
+ValidationError: [u'Enter a post code in the format XXXXXX.']
+
+############################################################
+##################### CNIDCardField ########################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNIDCardField
+>>> f = CNIDCardField(required=False)
+>>> f.clean('')
+u''
+
+>>> # A string of 16 characters.
+>>> f.clean("abcdefghijklmnop")
+Traceback (most recent call last):
+    ...
+ValidationError: [u'ID Card Number consists of 15 or 18 digits.']
+
+>>> # A string of 16 digits.
+>>> f.clean("1010101010101010")
+Traceback (most recent call last):
+    ...
+ValidationError: [u'ID Card Number consists of 15 or 18 digits.']
+
+>>> # A valid 1st generation ID Card number.
+>>> f.clean('110101491001001')
+u'110101491001001'
+
+>>> # A 1st generation ID Card number with invalid location
+>>> f.clean('010101491001001')  # 01, an invalid location.
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong location code']
+
+>>> # A 1st generation ID Card number with invalid birthdate.
+>>> f.clean('110101491041001')  # 491041, an invalid day, 41
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong birthdate']
+
+>>> # A valid 2nd generation ID Card number.
+>>> f.clean('11010119491001001X')
+u'11010119491001001X'
+
+>>> # Another valid 2nd generation ID Card number, notice that the case of the last
+>>> # character is changed.
+>>> f.clean('11010119491001001x')
+u'11010119491001001X'
+>>> # An invalid 2nd generation ID Card number.
+>>> f.clean('92010119491001001X')   # 92 is an invalid location code.
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong location code']
+
+>>> # Another invalid 2nd generation ID Card number.
+>>> f.clean('91010119491301001X')   # 19491301 is an invalid date.
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong birthdate']
+
+>>> # Yet another invalid 2nd generation ID Card number.
+>>> f.clean('910101194910010014')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid ID Card Number: Wrong checksum']
+
+############################################################
+##################### CNPhoneNumberField ###################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNPhoneNumberField
+>>> f = CNPhoneNumberField(required=False)
+>>> f.clean('')
+u''
+>>> f.clean('010-12345678')
+u'010-12345678'
+>>> f.clean('010-1234567')
+u'010-1234567'
+>>> f.clean('0101-12345678')
+u'0101-12345678'
+>>> f.clean('0101-1234567')
+u'0101-1234567'
+>>> f.clean('01x-12345678')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number.']
+>>> f.clean('01123-12345678')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number.']
+>>> f.clean('010-123456789')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number.']
+>>> f.clean('010-12345678-020')
+u'010-12345678-020'
+>>> f.clean('010-12345678-')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number.']
+
+############################################################
+##################### CNCellNumberField ####################
+############################################################
+>>> from django.contrib.localflavor.cn.forms import CNCellNumberField
+>>> f = CNCellNumberField(required=False)
+>>> f.clean('')
+u''
+>>> f.clean('13012345678')
+u'13012345678'
+>>> f.clean('130123456789')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid cell number.']
+>>> f.clean('14012345678')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid cell number.']
+"""
Index: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py	(revision 11866)
+++ tests/regressiontests/forms/tests.py	(working copy)
@@ -9,6 +9,7 @@
 from localflavor.ca import tests as localflavor_ca_tests
 from localflavor.ch import tests as localflavor_ch_tests
 from localflavor.cl import tests as localflavor_cl_tests
+from localflavor.cn import tests as localflavor_cn_tests
 from localflavor.cz import tests as localflavor_cz_tests
 from localflavor.de import tests as localflavor_de_tests
 from localflavor.es import tests as localflavor_es_tests
@@ -44,6 +45,7 @@
     'localflavor_ca_tests': localflavor_ca_tests,
     'localflavor_ch_tests': localflavor_ch_tests,
     'localflavor_cl_tests': localflavor_cl_tests,
+    'localflavor_cn_tests': localflavor_cn_tests,
     'localflavor_cz_tests': localflavor_cz_tests,
     'localflavor_de_tests': localflavor_de_tests,
     'localflavor_es_tests': localflavor_es_tests,
Index: AUTHORS
===================================================================
--- AUTHORS	(revision 11866)
+++ AUTHORS	(working copy)
@@ -197,6 +197,7 @@
     Brant Harris
     Hawkeye
     Joe Heck <http://www.rhonabwy.com/wp/>
+    Xia Kai <http://blog.xiaket.org/>
     Joel Heenan <joelh-django@planetjoel.com>
     Mikko Hellsing <mikko@sorl.net>
     Sebastian Hillig <sebastian.hillig@gmail.com>
Index: docs/ref/contrib/localflavor.txt
===================================================================
--- docs/ref/contrib/localflavor.txt	(revision 11866)
+++ docs/ref/contrib/localflavor.txt	(working copy)
@@ -44,6 +44,7 @@
     * Brazil_
     * Canada_
     * Chile_
+    * China_
     * Czech_
     * Finland_
     * France_
@@ -84,6 +85,7 @@
 .. _Brazil: `Brazil (br)`_
 .. _Canada: `Canada (ca)`_
 .. _Chile: `Chile (cl)`_
+.. _China: `China (cn)`_
 .. _Czech: `Czech (cz)`_
 .. _Finland: `Finland (fi)`_
 .. _France: `France (fr)`_
@@ -233,6 +235,35 @@
     A ``Select`` widget that uses a list of Chilean regions (Regiones) as its
     choices.
 
+China (``cn``)
+==============
+
+.. class:: cn.forms.CNProvinceSelect
+
+    A ``Select`` widget that uses a list of Chinese regions as its choices.
+
+.. class:: cn.forms.CNPostCodeField
+
+    A form field that validates input as a Chinese post code.
+    Valid formats are XXXXXX where X is digit.
+
+.. class:: cn.forms.CNIDCardField
+
+    A form field that validates input as a Chinese Identification Card Number.
+    Both 1st and 2nd generation ID Card Number are validated.
+
+.. class:: cn.forms.CNPhoneNumberField
+
+    A form field that validates input as a Chinese phone number.
+    Valid formats are 0XX-XXXXXXXX, composed of 3 or 4 digits of region code
+    and 7 or 8 digits of phone number.
+
+.. class:: cn.forms.CNCellNumberField
+
+    A form field that validates input as a Chinese mobile phone number.
+    Valid formats are like 1XXXXXXXXXX, where X is digit.
+    The second digit could only be 3, 5 and 8.
+
 Czech (``cz``)
 ==============
 
