diff --git a/genshi/filters/i18n.py b/genshi/filters/i18n.py
--- a/genshi/filters/i18n.py
+++ b/genshi/filters/i18n.py
@@ -51,14 +51,14 @@
 class I18NDirectiveExtract(I18NDirective):
     """Simple interface for directives to support messages extraction"""
 
-    def extract(self, stream, ctxt):
+    def extract(self, stream, ctxt, error_callback=None):
         raise NotImplementedError
 
 
 class CommentDirective(I18NDirective):
     """Implementation of the ``i18n:comment`` template directive which adds
     translation comments.
-    
+
     >>> from genshi.template import MarkupTemplate
     >>>
     >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
@@ -83,7 +83,7 @@
 class MsgDirective(I18NDirectiveExtract):
     r"""Implementation of the ``i18n:msg`` directive which marks inner content
     as translatable. Consider the following examples:
-    
+
     >>> from genshi.template import MarkupTemplate
     >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     ...   <div i18n:msg="">
@@ -92,7 +92,7 @@
     ...   </div>
     ...   <p i18n:msg="">Foo <em>bar</em>!</p>
     ... </html>''')
-    
+
     >>> translator = Translator()
     >>> translator.setup(tmpl)
     >>> list(translator.extract(tmpl.stream))
@@ -131,7 +131,7 @@
       <p>Foo <em>bar</em>!</p>
     </html>
     >>>
-    
+
     Starting and ending white-space is stripped of to make it simpler for
     translators. Stripping it is not that important since it's on the html
     source, the rendered output will remain the same.
@@ -179,8 +179,8 @@
 
         return _apply_directives(_generate(), directives, ctxt)
 
-    def extract(self, stream, ctxt):
-        msgbuf = MessageBuffer(self)
+    def extract(self, stream, ctxt, error_callback=None):
+        msgbuf = MessageBuffer(self, error_callback=error_callback)
 
         stream = iter(stream)
         previous = stream.next()
@@ -192,7 +192,9 @@
         if previous[0] is not END:
             msgbuf.append(*previous)
 
-        yield None, msgbuf.format(), filter(None, [ctxt.get('_i18n.comment')])
+        if msgbuf.valid:
+            yield (None, msgbuf.format(),
+                   filter(None, [ctxt.get('_i18n.comment')]))
 
 
 class InnerChooseDirective(I18NDirective):
@@ -241,13 +243,13 @@
 class ChooseDirective(I18NDirectiveExtract):
     """Implementation of the ``i18n:choose`` directive which provides plural
     internationalisation of strings.
-    
+
     This directive requires at least one parameter, the one which evaluates to
     an integer which will allow to choose the plural/singular form. If you also
     have expressions inside the singular and plural version of the string you
     also need to pass a name for those parameters. Consider the following
     examples:
-    
+
     >>> from genshi.template import MarkupTemplate
     >>>
     >>> translator = Translator()
@@ -285,7 +287,7 @@
       </div>
     </html>
     >>>
-    
+
     When used as a directive and not as an attribute:
     >>> tmpl = MarkupTemplate('''\
         <html xmlns:i18n="http://genshi.edgewall.org/i18n">
@@ -381,15 +383,15 @@
 
         ctxt.pop()
 
-    def extract(self, stream, ctxt):
+    def extract(self, stream, ctxt, error_callback=None):
 
         stream = iter(stream)
         previous = stream.next()
         if previous is START:
             stream.next()
 
-        singular_msgbuf = MessageBuffer(self)
-        plural_msgbuf = MessageBuffer(self)
+        singular_msgbuf = MessageBuffer(self, error_callback=error_callback)
+        plural_msgbuf = MessageBuffer(self, error_callback=error_callback)
 
         for kind, event, pos in stream:
             if kind is SUB:
@@ -407,15 +409,16 @@
             else:
                 singular_msgbuf.append(kind, event, pos)
                 plural_msgbuf.append(kind, event, pos)
-        yield 'ngettext', \
-            (singular_msgbuf.format(), plural_msgbuf.format()), \
-            filter(None, [ctxt.get('_i18n.comment')])
+        if singular_msgbuf.valid and plural_msgbuf.valid:
+            yield 'ngettext', \
+                (singular_msgbuf.format(), plural_msgbuf.format()), \
+                filter(None, [ctxt.get('_i18n.comment')])
 
 
 class DomainDirective(I18NDirective):
     """Implementation of the ``i18n:domain`` directive which allows choosing
     another i18n domain(catalog) to translate from.
-    
+
     >>> from genshi.template.markup import MarkupTemplate
 
     >>> class DummyTranslations(NullTranslations):
@@ -479,7 +482,7 @@
     def __init__(self, value, template, hints=None, namespaces=None,
                  lineno=-1, offset=-1):
         Directive.__init__(self, None, template, namespaces, lineno, offset)
-        self.domain = value and value.strip() or '__DEFAULT__' 
+        self.domain = value and value.strip() or '__DEFAULT__'
 
     @classmethod
     def attach(cls, template, stream, value, namespaces, pos):
@@ -498,9 +501,9 @@
 class Translator(DirectiveFactory):
     """Can extract and translate localizable strings from markup streams and
     templates.
-    
+
     For example, assume the following template:
-    
+
     >>> from genshi.template import MarkupTemplate
     >>>
     >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
@@ -512,10 +515,10 @@
     ...     <p>${_("Hello, %(name)s") % dict(name=username)}</p>
     ...   </body>
     ... </html>''', filename='example.html')
-    
+
     For demonstration, we define a dummy ``gettext``-style function with a
     hard-coded translation table, and pass that to the `Translator` initializer:
-    
+
     >>> def pseudo_gettext(string):
     ...     return {
     ...         'Example': 'Beispiel',
@@ -523,15 +526,15 @@
     ...     }[string]
     >>>
     >>> translator = Translator(pseudo_gettext)
-    
+
     Next, the translator needs to be prepended to any already defined filters
     on the template:
-    
+
     >>> tmpl.filters.insert(0, translator)
-    
+
     When generating the template output, our hard-coded translations should be
     applied as expected:
-    
+
     >>> print tmpl.generate(username='Hans', _=pseudo_gettext)
     <html>
       <head>
@@ -542,7 +545,7 @@
         <p>Hallo, Hans</p>
       </body>
     </html>
-    
+
     Note that elements defining ``xml:lang`` attributes that do not contain
     variable expressions are ignored by this filter. That can be used to
     exclude specific parts of a template from being extracted and translated.
@@ -569,7 +572,7 @@
     def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS,
                  include_attrs=INCLUDE_ATTRS, extract_text=True):
         """Initialize the translator.
-        
+
         :param translate: the translation function, for example ``gettext`` or
                           ``ugettext``.
         :param ignore_tags: a set of tag names that should not be localized
@@ -577,7 +580,7 @@
         :param extract_text: whether the content of text nodes should be
                              extracted, or only text in explicit ``gettext``
                              function calls
-        
+
         :note: Changed in 0.6: the `translate` parameter can now be either
                a ``gettext``-style function, or an object compatible with the
                ``NullTransalations`` or ``GNUTranslations`` interface
@@ -589,13 +592,13 @@
 
     def __call__(self, stream, ctxt=None, search_text=True):
         """Translate any localizable strings in the given stream.
-        
+
         This function shouldn't be called directly. Instead, an instance of
         the `Translator` class should be registered as a filter with the
         `Template` or the `TemplateLoader`, or applied as a regular stream
         filter. If used as a template filter, it should be inserted in front of
         all the default filters.
-        
+
         :param stream: the markup event stream
         :param ctxt: the template context (not used)
         :param search_text: whether text nodes should be translated (used
@@ -720,12 +723,13 @@
                          'ugettext', 'ungettext')
 
     def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS,
-                search_text=True, msgbuf=None, ctxt=Context()):
+                search_text=True, msgbuf=None, ctxt=Context(),
+                error_callback=None):
         """Extract localizable strings from the given template stream.
-        
+
         For every string found, this function yields a ``(lineno, function,
         message, comments)`` tuple, where:
-        
+
         * ``lineno`` is the number of the line on which the string was found,
         * ``function`` is the name of the ``gettext`` function used (if the
           string was extracted from embedded Python code), and
@@ -734,7 +738,7 @@
            arguments).
         *  ``comments`` is a list of comments related to the message, extracted
            from ``i18n:comment`` attributes found in the markup
-        
+
         >>> from genshi.template import MarkupTemplate
         >>>
         >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
@@ -754,7 +758,7 @@
         6, None, u'Example'
         7, '_', u'Hello, %(name)s'
         8, 'ngettext', (u'You have %d item', u'You have %d items', None)
-        
+
         :param stream: the event stream to extract strings from; can be a
                        regular stream or a template stream
         :param gettext_functions: a sequence of function names that should be
@@ -763,14 +767,17 @@
         :param search_text: whether the content of text nodes should be
                             extracted (used internally)
         :param ctxt: the current extraction context (used internaly)
-        
+        :param error_callback: a function that gets called in case of extraction
+                               errors with the following arguments:
+                                 (filename, line_number, error_message)
+
         :note: Changed in 0.4.1: For a function with multiple string arguments
                (such as ``ngettext``), a single item with a tuple of strings is
                yielded, instead an item for each string argument.
         :note: Changed in 0.6: The returned tuples now include a 4th element,
                which is a list of comments for the translator. Added an ``ctxt``
                argument which is used to pass arround the current extraction
-               context.
+               context. Added an error_callback used to show errors to the user.
         """
         if not self.extract_text:
             search_text = False
@@ -803,8 +810,9 @@
                                 yield pos[1], None, text, []
                     else:
                         for lineno, funcname, text, comments in self.extract(
-                                _ensure(value), gettext_functions,
-                                search_text=False):
+                                            _ensure(value), gettext_functions,
+                                            search_text=False,
+                                            error_callback=error_callback):
                             yield lineno, funcname, text, comments
 
                 if msgbuf:
@@ -821,7 +829,7 @@
 
             elif not skip and msgbuf and kind is END:
                 msgbuf.append(kind, data, pos)
-                if not msgbuf.depth:
+                if not msgbuf.depth and msgbuf.valid:
                     yield msgbuf.lineno, None, msgbuf.format(), \
                                                   filter(None, [msgbuf.comment])
                     msgbuf = None
@@ -850,7 +858,8 @@
                             messages = self.extract(
                                 substream, gettext_functions,
                                 search_text=search_text and not skip,
-                                msgbuf=msgbuf, ctxt=ctxt)
+                                msgbuf=msgbuf, ctxt=ctxt,
+                                error_callback=error_callback)
                             for lineno, funcname, text, comments in messages:
                                 yield lineno, funcname, text, comments
                         directives.pop(idx)
@@ -870,13 +879,16 @@
 
                 for directive in directives:
                     if isinstance(directive, I18NDirectiveExtract):
-                        messages = directive.extract(substream, ctxt)
+                        messages = directive.extract(
+                                substream, ctxt, error_callback=error_callback
+                        )
                         for funcname, text, comments in messages:
                             yield pos[1], funcname, text, comments
                     else:
                         messages = self.extract(
                             substream, gettext_functions,
-                            search_text=search_text and not skip, msgbuf=msgbuf)
+                            search_text=search_text and not skip, msgbuf=msgbuf,
+                            error_callback=error_callback)
                         for lineno, funcname, text, comments in messages:
                             yield lineno, funcname, text, comments
                 if comment:
@@ -885,7 +897,7 @@
     def setup(self, template):
         """Convenience function to register the `Translator` filter and the
         related directives with the given template.
-        
+
         :param template: a `Template` instance
         """
         template.filters.insert(0, self)
@@ -895,13 +907,13 @@
 
 class MessageBuffer(object):
     """Helper class for managing internationalized mixed content.
-    
+
     :since: version 0.5
     """
 
-    def __init__(self, directive=None):
+    def __init__(self, directive=None, error_callback=None):
         """Initialize the message buffer.
-        
+
         :param params: comma-separated list of parameter names
         :type params: `basestring`
         :param lineno: the line number on which the first stream event
@@ -917,16 +929,21 @@
         self.depth = 1
         self.order = 1
         self.stack = [0]
+        self.error_callback = error_callback
+        self.valid = True
         self.subdirectives = {}
 
     def append(self, kind, data, pos):
         """Append a stream event to the buffer.
-        
+
         :param kind: the stream event kind
         :param data: the event data
         :param pos: the position of the event in the source
         """
-        if kind is SUB:
+        if not self.valid:
+            # Buffer is already invalid, no point wasting process time here
+            return
+        elif kind is SUB:
             # The order needs to be +1 because a new START kind event will
             # happen and we we need to wrap those events into our custom kind(s)
             order = self.stack[-1] + 1
@@ -948,22 +965,29 @@
             if self.params:
                 param = self.params.pop(0)
             else:
-                params = ', '.join(['"%s"' % p for p in self.orig_params if p])
-                if params:
-                    params = "(%s)" % params
-                raise IndexError("%d parameters%s given to 'i18n:%s' but "
-                                 "%d or more expressions used in '%s', line %s"
-                                 % (len(self.orig_params), params, 
-                                    self.directive.tagname,
-                                    len(self.orig_params)+1,
-                                    os.path.basename(pos[0] or
-                                                     'In Memmory Template'),
-                                    pos[1]))
+                # Failed to get the expression param name
+                # Invalidate and if possible warn
+                self.valid = False
+                if self.error_callback:
+                    params = ', '.join(['"%s"' % p for p in
+                                        self.orig_params if p])
+                    fname, lineno = pos[0] or '(unknown)', pos[1]
+                    if params:
+                        params = "(%s)" % params
+                    self.error_callback(
+                        fname, lineno,
+                        "%d parameters%s given to 'i18n:%s' but %d or more "
+                        "expressions used. Stopped Processing File."
+                        % (len(self.orig_params), params,
+                           self.directive.tagname, len(self.orig_params)+1)
+                    )
+                # Nothing else to do here
+                return
             self.string.append('%%(%s)s' % param)
             self.events.setdefault(self.stack[-1], []).append((kind, data, pos))
             self.values[param] = (kind, data, pos)
         else:
-            if kind is START: 
+            if kind is START:
                 self.string.append(u'[%d:' % self.order)
                 self.stack.append(self.order)
                 self.events.setdefault(self.stack[-1],
@@ -986,11 +1010,11 @@
     def translate(self, string, regex=re.compile(r'%\((\w+)\)s')):
         """Interpolate the given message translation with the events in the
         buffer and return the translated stream.
-        
+
         :param string: the translated message string
         """
         substream = None
-        
+
         def yield_parts(string):
             for idx, part in enumerate(regex.split(string)):
                 if idx % 2:
@@ -1077,19 +1101,19 @@
 def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?<!\\)\]')):
     """Parse a translated message using Genshi mixed content message
     formatting.
-    
+
     >>> parse_msg("See [1:Help].")
     [(0, 'See '), (1, 'Help'), (0, '.')]
-    
+
     >>> parse_msg("See [1:our [2:Help] page] for details.")
     [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')]
-    
+
     >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].")
     [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')]
-    
+
     >>> parse_msg("[1:] Bilder pro Seite anzeigen.")
     [(1, ''), (0, ' Bilder pro Seite anzeigen.')]
-    
+
     :param string: the translated message string
     :return: a list of ``(order, string)`` tuples
     :rtype: `list`
@@ -1121,18 +1145,18 @@
 
 def extract_from_code(code, gettext_functions):
     """Extract strings from Python bytecode.
-    
+
     >>> from genshi.template.eval import Expression
-    
+
     >>> expr = Expression('_("Hello")')
     >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
     [('_', u'Hello')]
-    
+
     >>> expr = Expression('ngettext("You have %(num)s item", '
     ...                            '"You have %(num)s items", num)')
     >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
     [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))]
-    
+
     :param code: the `Code` object
     :type code: `genshi.template.eval.Code`
     :param gettext_functions: a sequence of function names
@@ -1172,7 +1196,7 @@
 
 def extract(fileobj, keywords, comment_tags, options):
     """Babel extraction method for Genshi templates.
-    
+
     :param fileobj: the file-like object the messages should be extracted from
     :param keywords: a list of keywords (i.e. function names) that should be
                      recognized as translation functions
@@ -1205,10 +1229,13 @@
     tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None),
                           encoding=encoding)
 
+    error_callback = options.get('error_callback')
+
     translator = Translator(None, ignore_tags, include_attrs, extract_text)
     if hasattr(tmpl, 'add_directives'):
         tmpl.add_directives(Translator.NAMESPACE, translator)
-    for message in translator.extract(tmpl.stream, gettext_functions=keywords):
+    for message in translator.extract(tmpl.stream, gettext_functions=keywords,
+                                      error_callback=error_callback):
         yield message
 
 
diff --git a/genshi/filters/tests/i18n.py b/genshi/filters/tests/i18n.py
--- a/genshi/filters/tests/i18n.py
+++ b/genshi/filters/tests/i18n.py
@@ -1427,6 +1427,38 @@
           <p>BarFoo</p>
         """, tmpl.generate().render())
 
+    def test_translator_error_callback(self):
+
+        self.test_translator_error_callback_error_called = False
+        self.test_translator_error_callback_error_msg = ''
+
+        def error_callback(filename, line_number, error_message):
+            self.test_translator_error_callback_error_called = True
+            self.test_translator_error_callback_error_msg = (
+                "Error '%s' in filename %s line number %d." % (
+                error_message, filename, line_number
+            ))
+
+        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
+            xmlns:i18n="http://genshi.edgewall.org/i18n">
+          <p i18n:msg="" py:strip="">
+            Please see ${ foo } <a href="help.html">Help</a> for details.
+          </p>
+        </html>""")
+        translator = Translator()
+        translator.setup(tmpl)
+
+        list(translator.extract(tmpl.stream, error_callback=error_callback))
+
+        self.assertEqual(
+            self.test_translator_error_callback_error_msg,
+            "Error '0 parameters given to 'i18n:msg' but 1 or more expressions "
+            "used. Stopped Processing File.' in filename (unknown) "
+            "line number 4.")
+        self.assertEqual(self.test_translator_error_callback_error_called, True)
+        del self.test_translator_error_callback_error_called
+        del self.test_translator_error_callback_error_msg
+
 class ExtractTestCase(unittest.TestCase):
 
     def test_markup_template_extraction(self):

