diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..af40312
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[run]
+source = src
+
+[report]
+exclude_lines =
+    pragma: no cover
diff --git a/.travis.yml b/.travis.yml
index 4f6ff3e..ddcc8dc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,15 +1,27 @@
 language: python
 sudo: false
 python:
-    - 2.7
-    - 3.3
-    - 3.4
-    - 3.5
-    - 3.6
-    - pypy
-install:
-    - pip install .
+  - 2.7
+  - 3.3
+  - 3.4
+  - 3.5
+  - 3.6
+  - pypy-5.4.1
 script:
-    - python setup.py -q test -q
+  - coverage run -m zope.testrunner --test-path=src  --auto-color --auto-progress
+
+after_success:
+  - coveralls
 notifications:
-    email: false
+  email: false
+
+install:
+  - pip install -U pip setuptools
+  - pip install -U coveralls coverage
+  - pip install -U -e ".[test]"
+
+
+cache: pip
+
+before_cache:
+    - rm -f $HOME/.cache/pip/log/debug.log
diff --git a/CHANGES.rst b/CHANGES.rst
index b21324c..34e40f2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -8,6 +8,11 @@ Changes
 
 - Add support for Python 3.5 and 3.6.
 
+- Fix the ``domain`` of MessageID fields to be a native string.
+  Previously on Python 3 they were bytes, which meant that they
+  couldn't be used to find translation utilities registered by
+  zope.i18n. See `issue 17 <https://github.com/zopefoundation/zope.configuration/issues/17>`_.
+
 4.0.3 (2014-03-19)
 ------------------
 
diff --git a/MANIFEST.in b/MANIFEST.in
index 4bed54d..d93dadc 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,6 +3,7 @@ include *.txt
 include *.py
 include buildout.cfg
 include tox.ini
+include .coveragerc
 
 recursive-include docs *.rst *.bat *.py Makefile
 recursive-include src *.zcml *.in
diff --git a/setup.py b/setup.py
index 3adb48b..103a1c4 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,8 @@ import os
 from setuptools import setup, find_packages
 
 def read(*rnames):
-    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+    with open(os.path.join(os.path.dirname(__file__), *rnames)) as f:
+        return f.read()
 
 def _modname(path, base, name=''):
     if path == base:
@@ -36,7 +37,7 @@ def alltests():
 
     class NullHandler(logging.Handler):
         level = 50
-        
+
         def emit(self, record):
             pass
 
@@ -56,10 +57,12 @@ def alltests():
                     suite.addTest(mod.test_suite())
     return suite
 
-TESTS_REQUIRE = []
+TESTS_REQUIRE = [
+    'zope.testrunner',
+]
 
 setup(name='zope.configuration',
-      version = '4.1.0.dev0',
+      version='4.1.0.dev0',
       author='Zope Foundation and Contributors',
       author_email='zope-dev@zope.org',
       description='Zope Configuration Markup Language (ZCML)',
@@ -68,8 +71,8 @@ setup(name='zope.configuration',
           + '\n\n' +
           read('CHANGES.rst')
           ),
-      keywords = "zope configuration zcml",
-      classifiers = [
+      keywords="zope configuration zcml",
+      classifiers=[
           'Development Status :: 5 - Production/Stable',
           'Environment :: Web Environment',
           'Intended Audience :: Developers',
@@ -95,16 +98,17 @@ setup(name='zope.configuration',
       namespace_packages=['zope'],
       extras_require={
         'docs': ['Sphinx', 'repoze.sphinx.autointerface'],
-        'test': [],
+        'test': TESTS_REQUIRE,
         'testing': TESTS_REQUIRE + ['nose', 'coverage'],
       },
-      install_requires=['zope.i18nmessageid',
-                        'zope.interface',
-                        'zope.schema',
-                        'setuptools',
-                       ],
+      install_requires=[
+          'setuptools',
+          'zope.i18nmessageid',
+          'zope.interface',
+          'zope.schema',
+      ],
       include_package_data=True,
       zip_safe=False,
-      tests_require = TESTS_REQUIRE,
+      tests_require=TESTS_REQUIRE,
       test_suite='__main__.alltests',
-      )
+)
diff --git a/src/zope/configuration/fields.py b/src/zope/configuration/fields.py
index c5d26eb..dd8405e 100644
--- a/src/zope/configuration/fields.py
+++ b/src/zope/configuration/fields.py
@@ -15,6 +15,7 @@
 """
 import os
 import re
+import sys
 import warnings
 
 from zope.interface import implementer
@@ -158,6 +159,14 @@ class MessageID(Text):
                 "You did not specify an i18n translation domain for the "\
                 "'%s' field in %s" % (self.getName(), context.info.file )
                 )
+        if not isinstance(domain, str):
+            # IZopeConfigure specifies i18n_domain as a BytesLine, but that's
+            # wrong on Python 3, where the filesystem uses str, and hence
+            # zope.i18n registers ITranslationDomain utilities with str names.
+            # If we keep bytes, we can't find those utilities.
+            enc = sys.getfilesystemencoding() or sys.getdefaultencoding()
+            domain = domain.decode(enc)
+
         v = super(MessageID, self).fromUnicode(u)
 
         # Check whether there is an explicit message is specified
diff --git a/tox.ini b/tox.ini
index 190f97a..7ce21b5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,38 +1,31 @@
 [tox]
-envlist = 
+envlist =
 # Jython support pending 2.7 support, due 2012-07-15 or so.  See:
 # http://fwierzbicki.blogspot.com/2012/03/adconion-to-fund-jython-27.html
 #   py27,py33,py34,py35,jython,pypy,coverage,docs
     py27,py33,py34,py35,py36,pypy,coverage,docs
 
 [testenv]
-# see https://github.com/zopefoundation/zope.configuration/issues/16 for
-# the reason we set usedevelop here
-usedevelop = true
 deps =
-    zope.event
-    zope.i18nmessageid
-    zope.interface
-    zope.schema
-commands = 
-    python setup.py -q test -q
+    .[test]
+commands =
+    zope-testrunner --test-path=src --all []
 
 [testenv:coverage]
 usedevelop = true
 basepython =
     python2.7
-commands = 
-    nosetests --with-xunit --with-xcoverage
+commands =
+    coverage run -m zope.testrunner --test-path=src --all
+    coverage report
 deps =
     {[testenv]deps}
-    nose
     coverage
-    nosexcover
 
 [testenv:docs]
 basepython =
     python2.7
-commands = 
+commands =
     sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
     sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest
 deps =

MessageID broken for Python 3; produces byte domains which can't find utilities
As discovered in [zope.app.form](https://github.com/zopefoundation/zope.app.form/pull/1/files#diff-cbfcb4b47015db9258707315ce537abaR66), when reading a MessageID field out of a ZCML file, the `domain` will be a `byte` string under Python 3 (e.g., `b'zope'`). But `zope.i18n` registers ITranslationDomain utilities with normal `str` names. Since no byte string can ever equal a `str` on Python 3, the utilities will never be found and translation will not happen.

It seems to me that either MessageID should ensure that the domain is a `str` (decoding from...utf-8?) or the context itself could ensure that its `i18n_domain` is a `str`. I think I like the latter better.
diff --git a/src/zope/configuration/tests/test_fields.py b/src/zope/configuration/tests/test_fields.py
index c135420..641e136 100644
--- a/src/zope/configuration/tests/test_fields.py
+++ b/src/zope/configuration/tests/test_fields.py
@@ -34,7 +34,7 @@ class PythonIdentifierTests(unittest.TestCase, _ConformsToIFromUnicode):
     def _getTargetClass(self):
         from zope.configuration.fields import PythonIdentifier
         return PythonIdentifier
-    
+
     def _makeOne(self, *args, **kw):
         return self._getTargetClass()(*args, **kw)
 
@@ -69,7 +69,7 @@ class GlobalObjectTests(unittest.TestCase, _ConformsToIFromUnicode):
     def _getTargetClass(self):
         from zope.configuration.fields import GlobalObject
         return GlobalObject
-    
+
     def _makeOne(self, *args, **kw):
         return self._getTargetClass()(*args, **kw)
 
@@ -143,7 +143,7 @@ class GlobalInterfaceTests(unittest.TestCase, _ConformsToIFromUnicode):
     def _getTargetClass(self):
         from zope.configuration.fields import GlobalInterface
         return GlobalInterface
-    
+
     def _makeOne(self, *args, **kw):
         return self._getTargetClass()(*args, **kw)
 
@@ -157,7 +157,7 @@ class TokensTests(unittest.TestCase, _ConformsToIFromUnicode):
     def _getTargetClass(self):
         from zope.configuration.fields import Tokens
         return Tokens
-    
+
     def _makeOne(self, *args, **kw):
         return self._getTargetClass()(*args, **kw)
 
@@ -188,7 +188,7 @@ class PathTests(unittest.TestCase, _ConformsToIFromUnicode):
     def _getTargetClass(self):
         from zope.configuration.fields import Path
         return Path
-    
+
     def _makeOne(self, *args, **kw):
         return self._getTargetClass()(*args, **kw)
 
@@ -214,7 +214,7 @@ class BoolTests(unittest.TestCase, _ConformsToIFromUnicode):
     def _getTargetClass(self):
         from zope.configuration.fields import Bool
         return Bool
-    
+
     def _makeOne(self, *args, **kw):
         return self._getTargetClass()(*args, **kw)
 
@@ -244,7 +244,7 @@ class MessageIDTests(unittest.TestCase, _ConformsToIFromUnicode):
     def _getTargetClass(self):
         from zope.configuration.fields import MessageID
         return MessageID
-    
+
     def _makeOne(self, *args, **kw):
         return self._getTargetClass()(*args, **kw)
 
@@ -306,14 +306,13 @@ class MessageIDTests(unittest.TestCase, _ConformsToIFromUnicode):
         self.assertEqual(context.i18n_strings,
                          {'testing_domain': {'testing': [('test_file', 42)]}})
 
+    def test_domain_decodes_bytes(self):
+        mid = self._makeOne()
+        context = self._makeContext(domain=b'domain')
+        bound = mid.bind(context)
+        msgid = bound.fromUnicode(u'msgid')
+        self.assertIsInstance(msgid.domain, str)
+        self.assertEqual(msgid.domain, 'domain')
 
 def test_suite():
-    return unittest.TestSuite((
-        unittest.makeSuite(PythonIdentifierTests),
-        unittest.makeSuite(GlobalObjectTests),
-        unittest.makeSuite(GlobalInterfaceTests),
-        unittest.makeSuite(TokensTests),
-        unittest.makeSuite(PathTests),
-        unittest.makeSuite(BoolTests),
-        unittest.makeSuite(MessageIDTests),
-        ))
+    return unittest.defaultTestLoader.loadTestsFromName(__name__)

pip install -e .[dev]
parse_log_pytest
requirements.txt
nose coverage Sphinx repoze.sphinx.autointerface pytest
3.6
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
attrs==22.2.0
Babel==2.11.0
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
docutils==0.18.1
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
iniconfig==1.1.1
Jinja2==3.0.3
MarkupSafe==2.0.1
nose==1.3.7
packaging==21.3
pluggy==1.0.0
py==1.11.0
Pygments==2.14.0
pyparsing==3.1.4
pytest==7.0.1
pytz==2025.2
repoze.sphinx.autointerface==1.0.0
requests==2.27.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==1.2.3
typing_extensions==4.1.1
urllib3==1.26.20
zipp==3.6.0
-e git+https://github.com/zopefoundation/zope.configuration.git@6e09fee3a67116babff6d9b3182792499ec3fe83#egg=zope.configuration
zope.event==4.6
zope.i18nmessageid==5.1.1
zope.interface==5.5.2
zope.schema==6.2.1

name: zope.configuration
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - attrs==22.2.0
      - babel==2.11.0
      - charset-normalizer==2.0.12
      - coverage==6.2
      - docutils==0.18.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - jinja2==3.0.3
      - markupsafe==2.0.1
      - nose==1.3.7
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytz==2025.2
      - repoze-sphinx-autointerface==1.0.0
      - requests==2.27.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
      - zope-event==4.6
      - zope-i18nmessageid==5.1.1
      - zope-interface==5.5.2
      - zope-schema==6.2.1
prefix: /opt/conda/envs/zope.configuration

src/zope/configuration/tests/test_fields.py::MessageIDTests::test_domain_decodes_bytes
src/zope/configuration/tests/test_fields.py::PythonIdentifierTests::test__validate_hit src/zope/configuration/tests/test_fields.py::PythonIdentifierTests::test__validate_miss src/zope/configuration/tests/test_fields.py::PythonIdentifierTests::test_class_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::PythonIdentifierTests::test_fromUnicode_empty src/zope/configuration/tests/test_fields.py::PythonIdentifierTests::test_fromUnicode_normal src/zope/configuration/tests/test_fields.py::PythonIdentifierTests::test_fromUnicode_strips_ws src/zope/configuration/tests/test_fields.py::PythonIdentifierTests::test_instance_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::GlobalObjectTests::test__validate_w_value_type src/zope/configuration/tests/test_fields.py::GlobalObjectTests::test__validate_wo_value_type src/zope/configuration/tests/test_fields.py::GlobalObjectTests::test_class_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::GlobalObjectTests::test_fromUnicode_w_resolve_but_validation_fails src/zope/configuration/tests/test_fields.py::GlobalObjectTests::test_fromUnicode_w_resolve_fails src/zope/configuration/tests/test_fields.py::GlobalObjectTests::test_fromUnicode_w_resolve_success src/zope/configuration/tests/test_fields.py::GlobalObjectTests::test_fromUnicode_w_star_and_extra_ws src/zope/configuration/tests/test_fields.py::GlobalObjectTests::test_instance_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::GlobalInterfaceTests::test_class_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::GlobalInterfaceTests::test_ctor src/zope/configuration/tests/test_fields.py::GlobalInterfaceTests::test_instance_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::TokensTests::test_class_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::TokensTests::test_fromUnicode_empty src/zope/configuration/tests/test_fields.py::TokensTests::test_fromUnicode_invalid src/zope/configuration/tests/test_fields.py::TokensTests::test_fromUnicode_strips_ws src/zope/configuration/tests/test_fields.py::TokensTests::test_instance_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::PathTests::test_class_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::PathTests::test_fromUnicode_absolute src/zope/configuration/tests/test_fields.py::PathTests::test_fromUnicode_relative src/zope/configuration/tests/test_fields.py::PathTests::test_instance_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::BoolTests::test_class_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::BoolTests::test_fromUnicode_w_false_values src/zope/configuration/tests/test_fields.py::BoolTests::test_fromUnicode_w_invalid src/zope/configuration/tests/test_fields.py::BoolTests::test_fromUnicode_w_true_values src/zope/configuration/tests/test_fields.py::BoolTests::test_instance_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::MessageIDTests::test_class_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::MessageIDTests::test_instance_conforms_to_IFromUnicode src/zope/configuration/tests/test_fields.py::MessageIDTests::test_w_empty_id src/zope/configuration/tests/test_fields.py::MessageIDTests::test_w_id_and_default src/zope/configuration/tests/test_fields.py::MessageIDTests::test_wo_domain src/zope/configuration/tests/test_fields.py::test_suite
MANIFEST.in setup.py .travis.yml tox.ini .coveragerc CHANGES.rst src/zope/configuration/fields.py
MANIFEST.in setup.py .travis.yml tox.ini .coveragerc CHANGES.rst src/zope/configuration/fields.py