Repository: getnikola/nikola

Issue:
Passing wrong locale in nikola init crashes
If you do a `nikola init foo` and enter something even slightly wrong as locale (en, pt or en, pt_BR) Nikola throws an exception.



Discussion:
Reproduced, fix upcoming.


Resolving patch:
diff --git a/nikola/nikola.py b/nikola/nikola.py
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -124,6 +124,14 @@
         'ur': 'Urdu',
         'zh_cn': 'Chinese (Simplified)',
     },
+    '_TRANSLATIONS_WITH_COUNTRY_SPECIFIERS': {
+        # This dict is used in `init` in case of locales that exist with a
+        # country specifier.  If there is no other locale that has the same
+        # language with a different country, ``nikola init`` (but nobody else!)
+        # will accept it, warning the user about it.
+        'pt': 'pt_br',
+        'zh': 'zh_cn'
+    },
     'RTL_LANGUAGES': ('fa', 'ur'),
     'COLORBOX_LOCALES': defaultdict(
         str,
@@ -608,7 +616,7 @@ def _get_themes(self):
             try:
                 self._THEMES = utils.get_theme_chain(self.config['THEME'])
             except Exception:
-                utils.LOGGER.warn('''Can't load theme "{0}", using 'bootstrap' instead.'''.format(self.config['THEME']))
+                utils.LOGGER.warn('''Cannot load theme "{0}", using 'bootstrap' instead.'''.format(self.config['THEME']))
                 self.config['THEME'] = 'bootstrap'
                 return self._get_themes()
             # Check consistency of USE_CDN and the current THEME (Issue #386)
@@ -623,9 +631,13 @@ def _get_themes(self):
     THEMES = property(_get_themes)
 
     def _get_messages(self):
-        return utils.load_messages(self.THEMES,
-                                   self.translations,
-                                   self.default_lang)
+        try:
+            return utils.load_messages(self.THEMES,
+                                       self.translations,
+                                       self.default_lang)
+        except utils.LanguageNotFoundError as e:
+            utils.LOGGER.error('''Cannot load language "{0}".  Please make sure it is supported by Nikola itself, or that you have the appropriate messages files in your themes.'''.format(e.lang))
+            sys.exit(1)
 
     MESSAGES = property(_get_messages)
 
@@ -947,7 +959,7 @@ def slug_path(self, name, lang):
         """slug path handler"""
         results = [p for p in self.timeline if p.meta('slug') == name]
         if not results:
-            utils.LOGGER.warning("Can't resolve path request for slug: {0}".format(name))
+            utils.LOGGER.warning("Cannot resolve path request for slug: {0}".format(name))
         else:
             if len(results) > 1:
                 utils.LOGGER.warning('Ambiguous path request for slug: {0}'.format(name))
@@ -957,7 +969,7 @@ def filename_path(self, name, lang):
         """filename path handler"""
         results = [p for p in self.timeline if p.source_path == name]
         if not results:
-            utils.LOGGER.warning("Can't resolve path request for filename: {0}".format(name))
+            utils.LOGGER.warning("Cannot resolve path request for filename: {0}".format(name))
         else:
             if len(results) > 1:
                 utils.LOGGER.error("Ambiguous path request for filename: {0}".format(name))
diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py
--- a/nikola/plugins/command/init.py
+++ b/nikola/plugins/command/init.py
@@ -264,17 +264,23 @@ def ask(query, default):
             inp = inpf("{query}{default_q}: ".format(query=query, default_q=default_q)).strip()
             return inp if inp else default
 
-        def lhandler(default, toconf):
-            print("We will now ask you to provide the list of languages you want to use.")
-            print("Please list all the desired languages, comma-separated, using ISO 639-1 codes.  The first language will be used as the default.")
-            print("Type '?' (a question mark, sans quotes) to list available languages.")
+        def lhandler(default, toconf, show_header=True):
+            if show_header:
+                print("We will now ask you to provide the list of languages you want to use.")
+                print("Please list all the desired languages, comma-separated, using ISO 639-1 codes.  The first language will be used as the default.")
+                print("Type '?' (a question mark, sans quotes) to list available languages.")
             answer = ask('Language(s) to use', 'en')
             while answer.strip() == '?':
                 print('\n# Available languages:')
                 print(SAMPLE_CONF['_SUPPORTED_LANGUAGES'] + '\n')
                 answer = ask('Language(s) to use', 'en')
 
-            langs = answer.split(',')
+            langs = [i.strip().lower().replace('-', '_') for i in answer.split(',')]
+            for partial, full in LEGAL_VALUES['_TRANSLATIONS_WITH_COUNTRY_SPECIFIERS'].items():
+                if partial in langs:
+                    langs[langs.index(partial)] = full
+                    print("NOTICE: Assuming '{0}' instead of '{1}'.".format(full, partial))
+
             default = langs.pop(0)
             SAMPLE_CONF['DEFAULT_LANG'] = default
             # format_default_translations_config() is intelligent enough to
@@ -288,8 +294,14 @@ def lhandler(default, toconf):
                 tr[l] = './' + l
             # Assuming that base contains all the locales, and that base does
             # not inherit from anywhere.
-            messages = load_messages(['base'], tr, default)
-            SAMPLE_CONF['NAVIGATION_LINKS'] = format_navigation_links(langs, default, messages)
+            try:
+                messages = load_messages(['base'], tr, default)
+                SAMPLE_CONF['NAVIGATION_LINKS'] = format_navigation_links(langs, default, messages)
+            except nikola.utils.LanguageNotFoundError as e:
+                print("ERROR: the language '{0}' is not supported.".format(e.lang))
+                print("\nAre you sure you spelled the name correctly?  Names are case-sensitive and need to be reproduced as-is (complete with the country specifier, if any).")
+                print("Type '?' (a question mark, sans quotes) to list available languages.")
+                lhandler(default, toconf, show_header=False)
 
         def chandler(default, toconf):
             print("You can configure comments now.  Type '?' (a question mark, sans quotes) to list available comment systems.  If you do not want any comments, just leave the field blank.")
diff --git a/nikola/utils.py b/nikola/utils.py
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -466,6 +466,15 @@ def get_theme_chain(theme):
 warned = []
 
 
+class LanguageNotFoundError(Exception):
+    def __init__(self, lang, orig):
+        self.lang = lang
+        self.orig = orig
+
+    def __str__(self):
+        return 'cannot find language {0}'.format(self.lang)
+
+
 def load_messages(themes, translations, default_lang):
     """ Load theme's messages into context.
 
@@ -481,18 +490,21 @@ def load_messages(themes, translations, default_lang):
         sys.path.insert(0, msg_folder)
         english = __import__('messages_en')
         for lang in list(translations.keys()):
-            # If we don't do the reload, the module is cached
-            translation = __import__('messages_' + lang)
-            reload(translation)
-            if sorted(translation.MESSAGES.keys()) !=\
-                    sorted(english.MESSAGES.keys()) and \
-                    lang not in warned:
-                warned.append(lang)
-                LOGGER.warn("Incomplete translation for language "
-                            "'{0}'.".format(lang))
-            messages[lang].update(english.MESSAGES)
-            messages[lang].update(translation.MESSAGES)
-            del(translation)
+            try:
+                translation = __import__('messages_' + lang)
+                # If we don't do the reload, the module is cached
+                reload(translation)
+                if sorted(translation.MESSAGES.keys()) !=\
+                        sorted(english.MESSAGES.keys()) and \
+                        lang not in warned:
+                    warned.append(lang)
+                    LOGGER.warn("Incomplete translation for language "
+                                "'{0}'.".format(lang))
+                messages[lang].update(english.MESSAGES)
+                messages[lang].update(translation.MESSAGES)
+                del(translation)
+            except ImportError as orig:
+                raise LanguageNotFoundError(lang, orig)
     sys.path = oldpath
     return messages
 


Test patch: