Finished (or nearly finished) the admin language CP
authorDan
Sat, 26 Jan 2008 11:51:19 -0500
changeset 376 66732bd4532c
parent 375 8d0e3a5a6990
child 377 bb3e6c3bd4f4
Finished (or nearly finished) the admin language CP
includes/functions.php
includes/lang.php
index.php
language/english/admin.json
language/english/core.json
language/english/install.json
plugins/SpecialAdmin.php
plugins/admin/LangManager.php
--- a/includes/functions.php	Thu Jan 24 22:14:40 2008 -0500
+++ b/includes/functions.php	Sat Jan 26 11:51:19 2008 -0500
@@ -4075,6 +4075,36 @@
 // Might as well start the profiler, it has no external dependencies except from this file.
 profiler_start();
 
+/**
+ * Returns the number of times a character occurs in a given string.
+ * @param string Haystack
+ * @param string Needle
+ * @return int
+ */
+
+function get_char_count($string, $char)
+{
+  $char = substr($char, 0, 1);
+  $count = 0;
+  for ( $i = 0; $i < strlen($string); $i++ )
+  {
+    if ( $string{$i} == $char )
+      $count++;
+  }
+  return $count;
+}
+
+/**
+ * Returns the number of lines in a string.
+ * @param string String to check
+ * @return int
+ */
+
+function get_line_count($string)
+{
+  return ( get_char_count($string, "\n") ) + 1;
+}
+
 //die('<pre>Original:  01010101010100101010100101010101011010'."\nProcessed: ".uncompress_bitfield(compress_bitfield('01010101010100101010100101010101011010')).'</pre>');
 
 ?>
--- a/includes/lang.php	Thu Jan 24 22:14:40 2008 -0500
+++ b/includes/lang.php	Sat Jan 26 11:51:19 2008 -0500
@@ -456,6 +456,22 @@
   
   function get($string_id, $substitutions = false)
   {
+    if ( !is_array($substitutions) )
+      $substitutions = array();
+    return $this->substitute($this->get_uncensored($string_id), $substitutions);
+  }
+  
+  /**
+   * The same as get(), but does not perform any substitution or filtering. Used in get() (of course) and in the admin panel, where
+   * strings are updated only if they were changed.
+   *
+   * @param string ID of the string to fetch. This will always be in the format of category_stringid.
+   * @param array Optional. Associative array of substitutions.
+   * @return string
+   */
+  
+  function get_uncensored($string_id, $substitutions = false)
+  {
     // Extract the category and string ID
     $category = substr($string_id, 0, ( strpos($string_id, '_') ));
     $string_name = substr($string_id, ( strpos($string_id, '_') + 1 ));
@@ -486,7 +502,7 @@
         {
           if ( !is_object($this->default) )
             $this->default = new Language($lang_default);
-          return $this->default->get($string_id, $substitutions);
+          return $this->default->get_uncensored($string_id);
         }
       }
     }
@@ -496,12 +512,7 @@
       return $string_id;
     }
     // Found it!
-    // Perform substitutions.
-    // if ( is_array($substitutions) )
-    //   die('<pre>' . print_r($substitutions, true) . '</pre>');
-    if ( !is_array($substitutions) )
-      $substitutions = array();
-    return $this->substitute($string, $substitutions);
+    return $string;
   }
   
   /**
--- a/index.php	Thu Jan 24 22:14:40 2008 -0500
+++ b/index.php	Sat Jan 26 11:51:19 2008 -0500
@@ -19,7 +19,7 @@
   define('ENANO_INTERFACE_INDEX', '');
   
   // For the mighty and brave.
-  define('ENANO_DEBUG', '');
+  // define('ENANO_DEBUG', '');
  
   // Set up gzip encoding before any output is sent
   
--- a/language/english/admin.json	Thu Jan 24 22:14:40 2008 -0500
+++ b/language/english/admin.json	Sat Jan 26 11:51:19 2008 -0500
@@ -443,12 +443,60 @@
       btn_create_backup: 'Create backup',
     },
     acplm: {
+      // Language installation
       heading_install: 'Languages available for installation',
-      col_lang_code: 'ID',
+      col_lang_id: 'ID',
+      col_lang_code: 'Shorthand code',
       col_lang_name: 'Language name (native)',
       col_lang_name_eng: 'Language name (English)',
       btn_install_language: 'Install',
       msg_lang_install_success: 'The language pack %lang_name% has been installed.',
+      
+      // Editor portal
+      heading_editor_portal: 'Edit installed languages',
+      portal_btn_edit: 'Modify',
+      portal_btn_unin: 'Uninstall',
+      
+      // Properties table
+      heading_modify: 'Edit language info',
+      th_lang_basic: 'Basic language properties',
+      field_lang_name_native: 'Language name (native):',
+      field_lang_name_english: 'Language name (in English):',
+      field_lang_code: 'Shorthand code:',
+      field_lang_code_hint: 'You can\'t change this because it needs to be compliant with <a onclick="window.open(this.href); return false;" href="http://en.wikipedia.org/wiki/ISO_639-3">ISO 639-3</a>.',
+      msg_basic_save_success: 'Changes saved.',
+      
+      // String editor portal
+      heading_edit_strings_portal: 'Edit strings',
+      msg_edit_strings_portal_intro: 'You can edit the actual language strings used in this language. Be sure to preserve any variables (in the format of %variable_name%) even if you\'re translating a language. If you\'re translating all of Enano into a new language, you should edit the JSON files instead of using this console, so that comments in the language files can be preserved.',
+      btn_edit_strings_portal: 'Edit strings &raquo;',
+      
+      // Re-import button and explanation
+      heading_reimport_portal: 'Re-import this language',
+      msg_reimport_portal_intro: 'If you accidentally changed a lot of strings, you can re-import this language from the original language files. This will <b>destroy</b> any modifications you have made to the default set of strings, but any strings you\'ve added will be preserved. This is almost the same effect as re-installing the language. Don\'t stop this process while it\'s running, the re-import can take a long time.',
+      btn_reimport: 'Re-import language',
+      msg_reimport_success: 'The language was re-imported successfully. All Enano preset strings for this language have been restored.',
+      
+      // String editor
+      editor_heading: 'Editing category: %cat_name%',
+      editor_col_string_name: 'String name',
+      editor_col_string_content: 'String',
+      editor_btn_revert: 'Revert',
+      editor_btn_cancel: 'Cancel',
+      msg_string_save_success: 'Your changes have been saved.',
+      
+      // Backup interface
+      heading_backup: 'Backup language',
+      backup_intro: 'You can back up this language to make preserving custom strings easier if you ever migrate your Enano installation or re-install. Backed-up language files can be restored by using FTP or equivalent to copy the backup file to the language\'s folder and renaming it to "backup.json".',
+      btn_create_backup: 'Download backup',
+      
+      // Uninstaller
+      uninstall_confirm_title: 'Confirm uninstallation of language',
+      uninstall_confirm_body: 'Do you really want to uninstall this language? If you continue, all users that have selected this language will be reset to use the default. It is recommended that you create a backup of this language before you uninstall it if you have changed any strings.',
+      btn_uninstall_confirm: 'Confirm uninstallation',
+      btn_uninstall_cancel: 'Cancel',
+      err_cant_uninstall_default: 'You cannot uninstall the default language.',
+      msg_uninstall_success: 'The language has been uninstalled.',
     },
     acppg: {
       // Main menu
--- a/language/english/core.json	Thu Jan 24 22:14:40 2008 -0500
+++ b/language/english/core.json	Sat Jan 26 11:51:19 2008 -0500
@@ -21,6 +21,7 @@
   ],
   strings: {
     meta: {
+      meta: 'Category names and basic metadata',
       page: 'Page creation and control',
       comment: 'Comment display',
       onpage: 'On-page buttons and controls',
--- a/language/english/install.json	Thu Jan 24 22:14:40 2008 -0500
+++ b/language/english/install.json	Sat Jan 26 11:51:19 2008 -0500
@@ -196,21 +196,7 @@
       modetitle_long: 'Website information',
       header_blurb: 'The next step is to enter some information about your website. You can always change this information later, using the administration panel.',
       
-      field_name_title: 'Website name',
-      field_name_body: 'The display name of your website. Allowed characters are uppercase and lowercase letters, numerals, and spaces. This must not be blank or "Enano".',
-      field_desc_title: 'Website description',
-      field_desc_body: 'This text will be shown below the name of your website.',
-      field_copyright_title: 'Copyright info',
-      field_copyright_body: 'This should be a one-line legal notice that will appear at the bottom of all your pages.',
-      field_wikimode_title: 'Wiki mode',
-      field_wikimode_body: 'This feature allows people to create and edit pages on your site. Enano keeps a history of all page modifications, and you can protect pages to prevent editing.',
-      field_wikimode_checkbox: 'Yes, make my website a wiki.',
-      field_urlscheme_title: 'URL scheme',
-      field_urlscheme_body: 'Choose how the page URLs will look. Depending on your server configuration, you may need to select the first option. If you don\'t know, select the first option, and you can always change it later.',
-      field_urlscheme_ugly: 'Standard URLs - compatible with any web server (www.example.com/index.php?title=Page_name)',
-      field_urlscheme_short: 'Short URLs - requires Apache with a PHP module (www.example.com/index.php/Page_name)',
-      field_urlscheme_tiny: 'Tiny URLs - requires Apache on Linux/Unix/BSD with PHP module and mod_rewrite enabled (www.example.com/Page_name)',
-      field_urlscheme_helplink: 'Which URL scheme should I choose?',
+      // Need l10n 
       
       objective_verify: 'Verify that your site information is correct. Again, all of the above settings can be changed from the administration panel.',
     },
@@ -219,16 +205,7 @@
       header_blurb: 'Next, enter your desired username and password. The account you create here will be used to administer your site.',
       modetitle_long: 'Administration login',
       
-      field_username_title: 'Administration username',
-      field_username_body: 'The administration username you will use to log into your site.<br />This cannot be "anonymous" or in the form of an IP address.',
-      field_password_title: 'Administration password:',
-      field_password_confirm: 'Enter it again to confirm:',
-      field_email_title: 'Your e-mail address:',
-      field_allowphp_title: 'Allow administrators to embed PHP code into pages:',
-      field_allowphp_body: 'Do not under any circumstances enable this option without reading these %important_notes%.',
-      field_allowphp_isi: 'important security implications',
-      field_allowphp_disabled: 'Disabled',
-      field_allowphp_enabled: 'Enabled',
+      // Need l10n
       
       aes_blurb: 'If your browser supports Javascript, the password you enter here will be encrypted with AES before it is sent to the server.',
       
--- a/plugins/SpecialAdmin.php	Thu Jan 24 22:14:40 2008 -0500
+++ b/plugins/SpecialAdmin.php	Sat Jan 26 11:51:19 2008 -0500
@@ -48,6 +48,13 @@
 require(ENANO_ROOT . '/plugins/admin/UserManager.php');
 require(ENANO_ROOT . '/plugins/admin/LangManager.php');
 
+// For convenience and nothing more.
+function acp_start_form()
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  echo '<form action="'.makeUrl($paths->nslist['Special'].'Administration', ( isset($_GET['sqldbg']) ? 'sqldbg&' : '' ) . ( isset($_GET['nocompress']) ? 'nocompress&' : '' ) . 'module='.$paths->cpage['module']).'" method="post" enctype="multipart/form-data">';
+}
+
 // function names are IMPORTANT!!! The name pattern is: page_<namespace ID>_<page URLname, without namespace>
 
 function page_Admin_Home() {
@@ -951,7 +958,7 @@
       setConfig('max_file_size', $max_upload.'');
     }
   }
-  echo '<form name="main" action="'.htmlspecialchars(makeUrl($paths->nslist['Special'].'Administration', 'module='.$paths->cpage['module'])).'" method="post">';
+  acp_start_form();
   ?>
   <h3><?php echo $lang->get('acpup_heading_main'); ?></h3>
   
@@ -1051,7 +1058,7 @@
   <h3><?php echo $lang->get('acpft_heading_main'); ?></h3>
    <p><?php echo $lang->get('acpft_hint'); ?></p>
   <?php
-  echo '<form action="'.makeUrl($paths->nslist['Special'].'Administration', (( isset($_GET['sqldbg'])) ? 'sqldbg&amp;' : '') .'module='.$paths->cpage['module']).'" method="post">';
+  acp_start_form();
     $c = -1;
     $t = -1;
     $cl = 'row1';
@@ -1458,7 +1465,7 @@
     if(!$s) die('Error selecting name value: '.$db->get_error().'<br /><u>SQL:</u><br />'.$q);
     $r = $db->fetchrow_num($s);
     $db->free_result();
-    echo('<form action="'.makeUrl($paths->nslist['Special'].'Administration', 'module='.$paths->cpage['module']).'" method="post">');
+    acp_start_form();
     echo('<div class="question-box">
           Theme name displayed to users: <input type="text" name="name" value="'.$r[0].'" /><br /><br />
           Default stylesheet: <select name="defaultcss">');
@@ -1688,7 +1695,7 @@
     echo '<div class="info-box">' . $lang->get('acpcp_msg_save_success') . '</div>';
   }
   
-  echo '<form action="'.makeUrl($paths->nslist['Special'].'Administration', (( isset($_GET['sqldbg'])) ? 'sqldbg&amp;' : '') .'module='.$paths->cpage['module']).'" method="post">';
+  acp_start_form();
   
   echo '<div class="tblholder">';
   echo '<table border="0" cellspacing="1" cellpadding="4">';
@@ -1887,7 +1894,7 @@
   {
     echo '<div class="error-box">' . $lang->get('acpmm_err_demo') . '</div>';
   }
-  echo '<form action="'.makeUrl($paths->nslist['Special'].'Administration', 'module='.$paths->cpage['module']).'" method="post">';
+  acp_start_form();
   ?>
   <div class="tblholder">
     <table border="0" cellspacing="1" cellpadding="4">
@@ -2073,7 +2080,7 @@
   $db->free_result();
   echo '</table></div>';
   echo '<h3>' . $lang->get('acpbc_heading_create_new') . '</h3>';
-  echo '<form action="'.makeUrl($paths->nslist['Special'].'Administration', 'module='.$paths->cpage['module']).'" method="post">';
+  acp_start_form();
   ?>
   
   <?php echo $lang->get('acpbc_field_type'); ?>
--- a/plugins/admin/LangManager.php	Thu Jan 24 22:14:40 2008 -0500
+++ b/plugins/admin/LangManager.php	Sat Jan 26 11:51:19 2008 -0500
@@ -26,21 +26,35 @@
   if ( isset($_POST['action']) )
   {
     $action =& $_POST['action'];
+    // Parse parameters
     if ( strpos($action, ';') )
     {
+      // Parameter section
       $parms = substr($action, strpos($action, ';') + 1);
+      
+      // Action name section
       $action = substr($action, 0, strpos($action, ';'));
+      
+      // Match all parameters
       preg_match_all('/([a-z0-9_]+)=(.+?)(;|$)/', $parms, $matches);
       $parms = array();
+      
+      // For each full parameter, assign $parms an associative value
       foreach ( $matches[0] as $i => $_ )
       {
-        $parms[$matches[1][$i]] = $matches[2][$i];
+        $parm = $matches[2][$i];
+        
+        // Is this parameter in the form of an integer?
+        // (designed to ease validation later)
+        if ( preg_match('/^[0-9]+$/', $parm) )
+          // Yes, run intval(), this enabling is_int()-ish checks
+          $parm = intval($parm);
+        
+        $parms[$matches[1][$i]] = $parm;
       }
     }
     switch ( $action )
     {
-      case 'edit_language':
-        break;
       case 'install_language':
         $lang_list = list_available_languages();
         // Verify that we have this language's metadata
@@ -55,9 +69,16 @@
           {
             // Language installed. Import the language files.
             $lang_local = new Language($lang_code);
-            foreach ( array('core', 'admin', 'tools', 'user') as $file )
+            if ( file_exists(ENANO_ROOT . "/language/{$lang_data['dir']}/backup.json") )
             {
-              $lang_local->import(ENANO_ROOT . "/language/{$lang_data['dir']}/$file.json");
+              $lang_local->import(ENANO_ROOT . "/language/{$lang_data['dir']}/backup.json");
+            }
+            else
+            {
+              foreach ( array('core', 'admin', 'tools', 'user') as $file )
+              {
+                $lang_local->import(ENANO_ROOT . "/language/{$lang_data['dir']}/$file.json");
+              }
             }
             unset($lang_local);
             
@@ -65,9 +86,462 @@
           }
         }
         break;
+      case 'modify_language':
+        $lang_id =& $parms['lang_id'];
+        if ( !is_int($lang_id) )
+        {
+          echo 'Hacking attempt';
+          break;
+        }
+        
+        if ( isset($parms['finish']) && !empty($_POST['lang_name_native']) && !empty($_POST['lang_name_english']) )
+        {
+          // We just did validation above, it's safe to save.
+          $name_native = $db->escape($_POST['lang_name_native']);
+          $name_english = $db->escape($_POST['lang_name_english']);
+          
+          $q = $db->sql_query('UPDATE ' . table_prefix . "language SET lang_name_native = '$name_native', lang_name_default = '$name_english' WHERE lang_id = $lang_id;");
+          if ( !$q )
+            $db->_die();
+          
+          echo '<div class="info-box">' . $lang->get('acplm_msg_basic_save_success') . '</div>';
+        }
+        
+        // Select language data
+        $q = $db->sql_query('SELECT lang_name_native, lang_name_default, lang_code FROM ' . table_prefix . "language WHERE lang_id = $lang_id;");
+        if ( !$q )
+          $db->_die();
+        
+        list($name_native, $name_english, $lang_code) = $db->fetchrow_num();
+        
+        // Output properties table
+        echo '<h3>' . $lang->get('acplm_heading_modify') . '</h3>';
+        
+        acp_start_form();
+        
+        ?>
+          <div class="tblholder">
+            <table border="0" cellspacing="1" cellpadding="4">
+              <tr>
+                <th colspan="2">
+                  <?php
+                    echo $lang->get('acplm_th_lang_basic');
+                  ?>
+                </th>
+              </tr>
+              <tr>
+              <td class="row2" style="width: 50%;">
+                  <?php
+                    echo str_replace('"', '', $lang->get('acplm_field_lang_name_native'));
+                  ?>
+                </td>
+                <td class="row1">
+                  <input type="text" name="lang_name_native" value="<?php echo htmlspecialchars($name_native); ?>" />
+                </td>
+              </tr>
+              <tr>
+                <td class="row2">
+                  <?php
+                    echo $lang->get('acplm_field_lang_name_english');
+                  ?>
+                </td>
+                <td class="row1">
+                  <input type="text" name="lang_name_english" value="<?php echo htmlspecialchars($name_english); ?>" />
+                </td>
+              </tr>
+              <tr>
+                <td class="row2">
+                  <?php
+                    echo $lang->get('acplm_field_lang_code') . '<br />'
+                       . '<small>' . $lang->get('acplm_field_lang_code_hint') . '</small>';
+                  ?>
+                </td>
+                <td class="row1">
+                  <?php
+                    echo $lang_code;
+                  ?>
+                </td>
+              </tr>
+              <tr>
+                <th class="subhead" colspan="2">
+                  <button name="action" value="modify_language;finish=1;lang_id=<?php echo $lang_id; ?>"><?php echo $lang->get('etc_save_changes'); ?></button>
+                </th>
+              </tr>
+            </table>
+          </div>
+        </form>
+        
+        <?php
+        acp_start_form();
+        ?>
+        
+        <h3><?php echo $lang->get('acplm_heading_edit_strings_portal'); ?></h3>
+        <p><?php echo $lang->get('acplm_msg_edit_strings_portal_intro'); ?></p>
+        
+        <p>
+        
+        <?php
+        
+        // Grab a Language object
+        if ( $lang->lang_id == $lang_id )
+        {
+          $lang_local =& $lang;
+        }
+        else
+        {
+          $lang_local = new Language($lang_id);
+          $lang_local->fetch();
+        }
+        
+        $categories_loc = array();
+        
+        // Using the & here ensures that a reference is created, thus avoiding wasting memory
+        foreach ( $lang_local->strings as $cat => &$_ )
+        {
+          unset($_);
+          $categories_loc[$cat] = htmlspecialchars($lang->get("meta_$cat"));
+        }
+        
+        asort($categories_loc);
+        
+        echo '<select name="cat_id">';
+        foreach ( $categories_loc as $cat_id => $cat_name)
+        {
+          echo "<option value=\"$cat_id\">$cat_name</option>";
+        }
+        echo '</select>';
+        
+        ?>
+        <button name="action" value="edit_strings;lang_id=<?php echo $lang_id; ?>">
+          <?php echo $lang->get('acplm_btn_edit_strings_portal'); ?>
+        </button>
+        </p>
+        
+        <h3><?php echo $lang->get('acplm_heading_reimport_portal'); ?></h3>
+        <p><?php echo $lang->get('acplm_msg_reimport_portal_intro'); ?></p>
+        
+        <p>
+          <button name="action" value="reimport;iso639=<?php echo $lang_code; ?>;lang_id=<?php echo $lang_id; ?>">
+            <?php echo $lang->get('acplm_btn_reimport'); ?>
+          </button>
+        </p>
+        
+        </form>
+        
+        <?php
+        
+        echo '<h3>' . $lang->get('acplm_heading_backup') . '</h3>';
+        echo '<p>' . $lang->get('acplm_backup_intro') . '</p>';
+        
+        echo '<form action="' . makeUrlNS('Admin', 'LangManager') . '" method="post">';
+        echo '<button name="action" value="backup_language;lang_id=' . $lang_id . '">' . $lang->get('acplm_btn_create_backup') . '</button>';
+        echo '</form>';
+        
+        return true;
+      case 'edit_strings':
+        
+        $cat_id = @$_POST['cat_id'];
+        if ( !preg_match('/^[a-z0-9]+$/', $cat_id) || !is_int(@$parms['lang_id']) )
+          break;
+        
+        $lang_id =& $parms['lang_id'];
+        
+        if ( isset($parms['save']) )
+        {
+          // Grab a Language object
+          if ( $lang->lang_id == $lang_id )
+          {
+            $lang_local =& $lang;
+          }
+          else
+          {
+            $lang_local = new Language($lang_id);
+          }
+          // Main save loop
+          // Trying to minimize queries as much as possible here, but you know how that goes.
+          $count_upd = 0;
+          foreach ( $_POST['string'] as $string_id => $user_content )
+          {
+            $curr_content = $lang_local->get_uncensored("{$cat_id}_{$string_id}");
+            if ( $curr_content != $user_content )
+            {
+              $count_upd++;
+              $user_content = $db->escape($user_content);
+              $string_id = $db->escape($string_id);
+              $q = $db->sql_query('UPDATE ' . table_prefix . "language_strings SET string_content = '$user_content' WHERE lang_id = $lang_id AND string_category = '$cat_id' AND string_name = '$string_id';");
+              if ( !$q )
+                $db->_die();
+            }
+          }
+          if ( $count_upd > 0 )
+          {
+            // Update the cache
+            $lang_local->regen_caches();
+            
+            // Update modification time
+            $q = $db->sql_query('UPDATE ' . table_prefix . "language SET last_changed = " . time() . " WHERE lang_id = $lang_id;");
+            if ( !$q )
+              $db->_die();
+          }
+          
+          echo '<div class="info-box">' . $lang->get('acplm_msg_string_save_success') . '</div>';
+        }
+        
+        acp_start_form();
+        
+        $cat_name = $lang->get("meta_$cat_id");
+        echo '<h3>' . $lang->get('acplm_editor_heading', array('cat_name' => $cat_name)) . '</h3>';
+        
+        // Fetch all strings
+        // This is more efficient than iterating through $lang->strings, I think.
+        $q = $db->sql_query('SELECT string_id, string_name, string_content FROM ' . table_prefix . "language_strings WHERE string_category = '$cat_id' AND lang_id = $lang_id;");
+        if ( !$q )
+          $db->_die();
+        
+        ?>
+        <div class="tblholder">
+          <table border="0" cellspacing="1" cellpadding="4">
+            <tr>
+              <th style="width: 3%;"><?php echo $lang->get('acplm_editor_col_string_name'); ?></th>
+              <th><?php echo $lang->get('acplm_editor_col_string_content'); ?></th>
+            </tr>
+        <?php
+        
+        while ( $row = $db->fetchrow_num() )
+        {
+          list($string_id, $string_name, $string_content) = $row;
+          unset($row);
+          
+          echo '<tr>';
+          
+          if ( strpos($string_content, "\n") )
+          {
+            $editor = '<textarea rows="' . get_line_count($string_content) . '" cols="50" style="width: 99%;" ';
+            $editor .= 'name="string[' . htmlspecialchars($string_name) . ']" ';
+            $editor .= '>' . htmlspecialchars($string_content);
+            $editor .= '</textarea>';
+          }
+          else
+          {
+            $editor = '<input type="text" size="50" style="width: 99%;" ';
+            $editor .= 'name="string[' . htmlspecialchars($string_name) . ']" ';
+            $editor .= 'value="' . htmlspecialchars($string_content) . '" ';
+            $editor .= '/>';
+          }
+          
+          echo '<td class="row2">' . htmlspecialchars($string_name) . '</td>';
+          echo '<td class="row1">' . $editor . '</td>';
+          
+          
+          echo '</tr>';
+          echo "\n";
+        }
+        
+        echo '<tr>
+                <th class="subhead" colspan="2">';
+                
+        echo '<input type="hidden" name="cat_id" value="' . $cat_id . '" />';
+                
+        // Button: save
+        echo '<button name="action" value="edit_strings;lang_id=' . $lang_id . ';save=1" style="font-weight: bold;">' . $lang->get('etc_save_changes') . '</button> ';
+        // Button: revert
+        echo '<button name="action" value="edit_strings;lang_id=' . $lang_id . '" style="font-weight: normal;">' . $lang->get('acplm_editor_btn_revert') . '</button> ';
+        // Button: cancel
+        echo '<button name="action" value="modify_language;lang_id=' . $lang_id . '" style="font-weight: normal;">' . $lang->get('acplm_editor_btn_cancel') . '</button>';
+                
+        echo '  </th>
+              </tr>';
+        
+        ?>
+          </table>
+        </div>
+        <?php
+        echo '</form>';
+        
+        return true;
+      case 'reimport':
+        if ( !isset($parms['iso639']) || !is_int(@$parms['lang_id']) )
+          break;
+        
+        $lang_code =& $parms['iso639'];
+        $lang_id =& $parms['lang_id'];
+        
+        $lang_list = list_available_languages();
+        
+        if ( !isset($lang_list[$lang_code]) )
+          break;
+        
+        // Grab a Language object
+        if ( $lang->lang_id == $lang_id )
+        {
+          $lang_local =& $lang;
+        }
+        else
+        {
+          $lang_local = new Language($lang_id);
+        }
+        
+        $lang_data =& $lang_list[$lang_code];
+        
+        // This is the big re-import loop
+        if ( file_exists(ENANO_ROOT . "/language/{$lang_data['dir']}/backup.json") )
+        {
+          $lang_local->import(ENANO_ROOT . "/language/{$lang_data['dir']}/backup.json");
+        }
+        else
+        {
+          foreach ( array('core', 'admin', 'tools', 'user') as $file )
+          {
+            $lang_local->import(ENANO_ROOT . "/language/{$lang_data['dir']}/$file.json");
+          }
+        }
+        
+        echo '<div class="info-box">' . $lang->get('acplm_msg_reimport_success') . '</div>';
+        
+        break;
+      case 'backup_language':
+        if ( !is_int(@$parms['lang_id']) )
+          break;
+        
+        $lang_id =& $parms['lang_id'];
+        
+        // Grab a Language object
+        if ( $lang->lang_id == $lang_id )
+        {
+          $lang_local =& $lang;
+        }
+        else
+        {
+          $lang_local = new Language($lang_id);
+        }
+        
+        $filename = 'enano_lang_' . $lang_local->lang_code . '_' . enano_date('ymd') . '.json';
+        
+        // Free as much memory as possible
+        $db->close();
+        unset($GLOBALS['session'], $GLOBALS['paths'], $GLOBALS['template'], $GLOBALS['plugins']);
+        
+        // HTTP headers
+        header('Content-type: application/json');
+        header('Content-disposition: attachment; filename=' . $filename);
+        
+        // Export to JSON
+        $lang_struct = array(
+            'categories' => array_keys($lang_local->strings),
+            'strings' => $lang_local->strings
+          );
+        
+        $lang_struct = enano_json_encode($lang_struct);
+        
+        header('Content-length: ' . strlen($lang_struct));
+        echo $lang_struct;
+        
+        exit;
+        
+      case 'uninstall_language':
+        if ( !is_int(@$parms['lang_id']) )
+          break;
+        
+        $lang_id =& $parms['lang_id'];
+        
+        if ( isset($parms['confirm']) )
+        {
+          $lang_default = intval(getConfig('default_language'));
+          if ( $lang_default == $lang_id )
+          {
+            echo '<div class="error-box">' . $lang->get('acplm_err_cant_uninstall_default') . '</div>';
+            break;
+          }
+          if ( $lang_id == $lang->lang_id )
+          {
+            // Unload the current language since it's about to be uninstalled
+            unset($lang, $GLOBALS['lang']);
+            $GLOBALS['lang'] = new Language($lang_default);
+            global $lang;
+          }
+          // We're clear
+          
+          // Remove cache files
+          $cache_file = ENANO_ROOT . "/cache/lang_{$lang_id}.php";
+          if ( file_exists($cache_file) )
+            @unlink($cache_file);
+          
+          // Remove strings
+          $q = $db->sql_query('DELETE FROM ' . table_prefix . "language_strings WHERE lang_id = $lang_id;");
+          if ( !$q )
+            $db->_die();
+          
+          // Delete the language
+          $q = $db->sql_query('DELETE FROM ' . table_prefix . "language WHERE lang_id = $lang_id;");
+          if ( !$q )
+            $db->_die();
+          
+          echo '<div class="info-box">' . $lang->get('acplm_msg_uninstall_success') . '</div>';
+          break;
+        }
+        
+        acp_start_form();
+        
+        echo '<h3>' . $lang->get('acplm_uninstall_confirm_title') . '</h3>';
+        echo '<p>' . $lang->get('acplm_uninstall_confirm_body') . '</p>';
+        
+        echo '<p><button name="action" style="font-weight: bold;" value="uninstall_language;lang_id=' . $lang_id . ';confirm=1">' . $lang->get('acplm_btn_uninstall_confirm') . '</button> ';
+        echo '<button name="action" value="home">' . $lang->get('acplm_btn_uninstall_cancel') . '</button></p>';
+        
+        echo '</form>';
+        return true;
     }
   }
   
+  acp_start_form();
+  
+  // Select current languages
+  $q = $db->sql_query('SELECT lang_code, lang_name_native, lang_name_default, lang_id FROM ' . table_prefix . "language ORDER BY lang_id ASC;");
+  if ( !$q )
+    $db->_die();
+  
+  // Language properties/edit/delete portal table
+  echo '<h3>' . $lang->get('acplm_heading_editor_portal') . '</h3>';
+  
+  echo '<div class="tblholder">';
+  echo '<table border="0" cellspacing="1" cellpadding="4">';
+  echo '<tr>
+          <th>' . $lang->get('acplm_col_lang_id') . '</th>
+          <th>' . $lang->get('acplm_col_lang_code') . '</th>
+          <th>' . $lang->get('acplm_col_lang_name') . '</th>
+          <th>' . $lang->get('acplm_col_lang_name_eng') . '</th>
+          <th></th>
+        </tr>';
+  
+  $cls = 'row2';
+  
+  $btn_edit = $lang->get('acplm_portal_btn_edit');
+  $btn_unin = $lang->get('acplm_portal_btn_unin');
+  
+  while ( $row = $db->fetchrow($q) )
+  {
+    $cls = ( $cls == 'row1' ) ? 'row2' : 'row1';
+    
+    echo '<tr>';
+    
+    $lang_code = htmlspecialchars($row['lang_code']);
+    $lang_name_native  = htmlspecialchars($row['lang_name_native']);
+    $lang_name_english = htmlspecialchars($row['lang_name_default']);
+    
+    echo "<td class=\"$cls\" style=\"text-align: center;\">{$row['lang_id']}</td>";
+    echo "<td class=\"$cls\" style=\"text-align: center;\">{$lang_code}</td>";
+    echo "<td class=\"$cls\" style=\"text-align: center;\">{$lang_name_native}</td>";
+    echo "<td class=\"$cls\" style=\"text-align: center;\">{$lang_name_english}</td>";
+    echo "<td class=\"$cls\" style=\"text-align: center;\"><button name=\"action\" value=\"modify_language;lang_id={$row['lang_id']}\">$btn_edit</button> <button name=\"action\" value=\"uninstall_language;lang_id={$row['lang_id']}\">$btn_unin</button></td>";
+    
+    echo '</tr>';
+  }
+  
+  echo '</table></div>';
+  
+  // Reset the result pointer to zero so we can fetch that list of languages again
+  $db->sql_data_seek(0, $q);
+  
   // $lang_list is fetched by the posthandler sometimes
   if ( !isset($lang_list) )
   {
@@ -76,11 +550,6 @@
     $lang_list = list_available_languages();
   }
   
-  // Select current languages
-  $q = $db->sql_query('SELECT lang_code FROM ' . table_prefix . "language;");
-  if ( !$q )
-    $db->_die();
-  
   while ( $row = $db->fetchrow() )
   {
     $lang_code =& $row['lang_code'];
@@ -93,7 +562,6 @@
   
   if ( count($lang_list) > 0 )
   {
-    echo '<form action="'.makeUrl($paths->nslist['Special'].'Administration', 'module='.$paths->cpage['module']).'" method="post">';
     echo '<h3>' . $lang->get('acplm_heading_install') . '</h3>';
     echo '<div class="tblholder">
             <table border="0" cellspacing="1" cellpadding="4">
@@ -125,7 +593,7 @@
     echo '    </tr>
             </table>
           </div>';
-    echo '</form>';        
   }
+  echo '</form>';
 }