Index: includes/locale.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/locale.inc,v
retrieving revision 1.107
diff -u -d -F^\s*function -r1.107 locale.inc
--- includes/locale.inc	11 Feb 2007 08:44:32 -0000	1.107
+++ includes/locale.inc	11 Feb 2007 15:12:41 -0000
@@ -428,6 +428,8 @@ function _locale_string_edit_submit($for
     else {
       db_query("INSERT INTO {locales_target} (lid, translation, locale) VALUES (%d, '%s', '%s')", $lid, $value, $key);
     }
+    // Refresh the JS file for this translation.
+    _locale_rebuild_js($key);
   }
   drupal_set_message(t('The string has been saved.'));
 
@@ -443,8 +445,12 @@ function _locale_string_edit_submit($for
  * Delete a language string.
  */
 function _locale_string_delete($lid) {
+  $locale = db_fetch_object(db_query('SELECT locale FROM {locales_source} WHERE lid = %d', $lid));
   db_query('DELETE FROM {locales_source} WHERE lid = %d', $lid);
   db_query('DELETE FROM {locales_target} WHERE lid = %d', $lid);
+  if ($locale && $locale->locale) {
+    _locale_rebuild_js($locale->locale);
+  }
   locale_refresh_cache();
   drupal_set_message(t('The string has been removed.'));
 
@@ -489,6 +495,7 @@ function _locale_import_po($file, $lang,
 
   // rebuild locale cache
   cache_clear_all("locale:$lang", 'cache');
+  _locale_rebuild_js($lang);
 
   // rebuild the menu, strings may have changed
   menu_rebuild();
@@ -1400,6 +1407,111 @@ function _locale_string_seek() {
   return $output;
 }
 
+/**
+ * (Re-)Creates the JavaScript translation file for a locale.
+ *
+ * @param $locale
+ *   The locale, the translation file should be recreated for.
+ */
+function _locale_rebuild_js($locale) {
+  // Get information about the locale.
+  $meta = db_fetch_object(db_query("SELECT formula, javascript FROM {locales_meta} WHERE locale = '%s'", $locale));
+
+  // Construct the array for JavaScript translations.
+  // We sort on plural so that we have all plural forms before singular forms.
+  $result = db_query("SELECT s.lid, s.source, t.plid, t.plural, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE t.locale = '%s' AND s.location LIKE '%%.js%%' ORDER BY t.plural DESC", $locale);
+
+  $translations = $plurals = array();
+  while ($data = db_fetch_object($result)) {
+    // Only add this to the translations array when there is actually a translation.
+    if (!empty($data->translation)) {
+      if ($data->plural) {
+        // When the translation is a plural form, first add it to another array and
+        // wait for the singular (parent) translation.
+        if (!isset($plurals[$data->plid])) {
+          $plurals[$data->plid] = array($data->plural => $data->translation);
+        }
+        else {
+          $plurals[$data->plid] += array($data->plural => $data->translation);
+        }
+      }
+      elseif (isset($plurals[$data->lid])) {
+        // There are plural translations for this translation, so get them from
+        // the plurals array and add them to the final translations array.
+        $translations[$data->source] = array($data->plural => $data->translation) + $plurals[$data->lid];
+        unset($plurals[$data->lid]);
+      }
+      else {
+        // There are no plural forms for this translation, so just add it to
+        // the translations array.
+        $translations[$data->source] = $data->translation;
+      }
+    }
+  }
+
+  // Only operate when there are translations.
+  if (!empty($translations)) {
+    // Construct the JavaScript file.
+    $data = "Drupal.locale = {\n".
+      "  'pluralFormula': function(\$n) { return Number({$meta->formula}); },\n".
+      "  'strings': ". drupal_to_js($translations) ."\n".
+      "};";
+
+    // When there is currently another file, create the hash for comparison
+    if (!empty($meta->javascript)) {
+      $current = file_create_path($meta->javascript);
+      $current_md5 = md5_file($current);
+    }
+
+    // Recreate the file (and optionally delete the old file) when there is a
+    // change. This prevents browsers from reloading the same file with just
+    // another file name.
+    if (!isset($current_md5) || $current_md5 != md5($data)) {
+      // Construct the directory where JS translation files are stored.
+      // There is (on purpose) no front end to edit that variable.
+      $dir = file_create_path(variable_get('locale_js_directory', 'locale'));
+
+      if (isset($current)) {
+        // We are recreating the new file, so delete the old one.
+        file_delete(file_create_path($meta->javascript));
+      }
+      else {
+        // The directory might not exist when there has not been a translation
+        // file before, so create it.
+        file_check_directory($dir, TRUE);
+      }
+
+      // The file name of the new JavaScript translation file.
+      $dest = $dir .'/'. $locale .'_'. substr(md5(microtime()), 0, 8) .'.js';
+      $path = file_save_data($data, $dest);
+      db_query("UPDATE {locales_meta} SET javascript = '%s' WHERE locale = '%s'", $path ? $path : '', $locale);
+
+      // Only report updated or creation when there is actually a file created.
+      if ($path) {
+        // There has already been a translation file before.
+        if (isset($current)) {
+          watchdog('locale', t('Updated JavaScript translation file for the locale %locale.', array('%locale' => $locale)));
+        }
+        else {
+          watchdog('locale', t('Created JavaScript translation file for the locale %locale.', array('%locale' => $locale)));
+        }
+      }
+      else {
+        watchdog('locale', t('Deleted JavaScript translation file for the locale %locale.', array('%locale' => $locale)));
+      }
+    }
+  }
+
+  // There are no strings for JavaScript translation. However, if there is a file,
+  // delete it and reset the database.
+  elseif (!empty($meta->javascript)) {
+    // Delete the old JavaScript file
+    file_delete(file_create_path($meta->javascript));
+    db_query("UPDATE {locales_meta} SET javascript = '' WHERE locale = '%s'", $locale);
+    watchdog('locale', t('Deleted JavaScript translation file for the locale %locale.', array('%locale' => $locale)));
+  }
+}
+
 // ---------------------------------------------------------------------------------
 // List of some of the most common languages (administration only)
 
Index: misc/drupal.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/drupal.js,v
retrieving revision 1.29
diff -u -d -F^\s*function -r1.29 drupal.js
--- misc/drupal.js	14 Oct 2006 02:39:48 -0000	1.29
+++ misc/drupal.js	11 Feb 2007 15:12:42 -0000
@@ -1,6 +1,6 @@
 // $Id: drupal.js,v 1.29 2006/10/14 02:39:48 unconed Exp $
 
-var Drupal = Drupal || {};
+var Drupal = Drupal || { 'settings': {}, 'themes': {} };
 
 /**
  * Set the variable that indicates if JavaScript behaviors should be applied
@@ -22,6 +22,87 @@
 };
 
 /**
+ * Encode special characters in a plain-text string for display as HTML.
+ */
+Drupal.checkPlain = function(str) {
+  str = String(str);
+  var replace = { '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
+  for (var character in replace) {
+    str = str.replace(character, replace[character]);
+  }
+  return str;
+}
+
+/**
+ * Translate strings to the current locale.
+ */
+Drupal.t = function(str, args) {
+  if (typeof str == 'object') {
+    if (Drupal.locale.strings[str[0]]) {
+      str = Drupal.locale.strings[str[0]];
+    }
+    if (args && args['@count']) {
+      if (Drupal.locale.pluralFormula) {
+        str = str[Drupal.locale.pluralFormula(args['@count'])];
+      }
+      else {
+        str = str[(args['@count'] == 1) ? 0 : 1];
+      }
+    }
+    else {
+      str = str[0];
+    }
+  }
+  else if (Drupal.locale[str]) {
+    str = Drupal.locale[str]
+  }
+
+  if (args) {
+    // Transform arguments before inserting them
+    for (var key in args) {
+      switch (key[0]) {
+        // Escaped only
+        case '@':
+          args[key] = Drupal.checkPlain(args[key]);
+        break;
+        // Pass-through
+        case '!':
+          break;
+        // Escaped and placeholder
+        case '%':
+        default:
+          args[key] = Drupal.theme('placeholder', args[key]);
+          break;
+      }
+      str = str.replace(key, args[key]);
+    }
+  }
+  return str;
+}
+
+/**
+ * Generate the themed representation of a Drupal object.
+ */
+Drupal.theme = function(func) {
+  for (var i = 1, args = []; i < arguments.length; i++) {
+    args.push(arguments[i]);
+  }
+
+  var theme = (Drupal.settings.theme && Drupal.themes[Drupal.settings.theme] && Drupal.themes[Drupal.settings.theme][func]) ? Drupal.settings.theme : 'default';
+  return Drupal.themes[theme][func].apply(Drupal, args);
+}
+
+/**
+ * The default themes.
+ */
+Drupal.themes['default'] = {
+  // Formats text for emphasized display in a placeholder inside a sentence.
+  placeholder: function(str) {
+    return '<em>' + Drupal.checkPlain(str) + '</em>';
+  }
+};
+
+/**
  * Redirects a button's form submission to a hidden iframe and displays the result
  * in a given wrapper. The iframe should contain a call to
  * window.parent.iframeHandler() after submission.
Index: misc/progress.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/progress.js,v
retrieving revision 1.14
diff -u -d -F^\s*function -r1.14 progress.js
--- misc/progress.js	14 Dec 2006 14:21:36 -0000	1.14
+++ misc/progress.js	11 Feb 2007 15:12:42 -0000
@@ -86,7 +86,7 @@
         pb.timer = setTimeout(function() { pb.sendPing(); }, pb.delay);
       },
       error: function (xmlhttp) {
-        pb.displayError('An HTTP error '+ xmlhttp.status +' occured.\n'+ pb.uri);
+        pb.displayError(Drupal.t('An HTTP error @status occured.', { '@status': xmlhttp.status }) +'\n'+ pb.uri);
       }
     });
   }
Index: misc/update.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/update.js,v
retrieving revision 1.10
diff -u -d -F^\s*function -r1.10 update.js
--- misc/update.js	27 Dec 2006 14:11:45 -0000	1.10
+++ misc/update.js	11 Feb 2007 15:12:42 -0000
@@ -18,13 +18,13 @@
       var errorCallback = function (pb) {
         var div = document.createElement('p');
         div.className = 'error';
-        $(div).html('An unrecoverable error has occured. You can find the error message below. It is advised to copy it to the clipboard for reference. Please continue to the <a href="update.php?op=error">update summary</a>');
+        $(div).html(Drupal.t('An unrecoverable error has occured. You can find the error message below. It is advised to copy it to the clipboard for reference. Please continue to the <a href="@url">update summary</a>', { '@url': 'update.php?op=error' }));
         $(holder).prepend(div);
         $('#wait').hide();
       }
 
       var progress = new Drupal.progressBar('updateprogress', updateCallback, "POST", errorCallback);
-      progress.setProgress(-1, 'Starting updates');
+      progress.setProgress(-1, Drupal.t('Starting updates'));
       $(holder).append(progress.element);
       progress.startMonitoring('update.php?op=do_update', 0);
     });
Index: misc/upload.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/upload.js,v
retrieving revision 1.11
diff -u -d -F^\s*function -r1.11 upload.js
--- misc/upload.js	31 Aug 2006 23:31:25 -0000	1.11
+++ misc/upload.js	11 Feb 2007 15:12:42 -0000
@@ -33,7 +33,7 @@
 Drupal.jsUpload.prototype.onsubmit = function () {
   // Insert progressbar and stretch to take the same space.
   this.progress = new Drupal.progressBar('uploadprogress');
-  this.progress.setProgress(-1, 'Uploading file');
+  this.progress.setProgress(-1, Drupal.t('Uploading file'));
 
   var hide = this.hide;
   var el = this.progress.element;
@@ -98,7 +98,7 @@
  * Handler for the form redirection error.
  */
 Drupal.jsUpload.prototype.onerror = function (error) {
-  alert('An error occurred:\n\n'+ error);
+  alert(Drupal.t('An error occurred:\n\n@error', { '@error': error }));
   // Remove progressbar
   $(this.progress.element).remove();
   this.progress = null;
Index: modules/locale/locale.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.install,v
retrieving revision 1.7
diff -u -d -F^\s*function -r1.7 locale.install
--- modules/locale/locale.install	14 Nov 2006 06:20:40 -0000	1.7
+++ modules/locale/locale.install	11 Feb 2007 15:12:43 -0000
@@ -18,6 +18,7 @@ function locale_install() {
         isdefault int NOT NULL default '0',
         plurals int NOT NULL default '0',
         formula varchar(128) NOT NULL default '',
+        javascript varchar(32) NOT NULL default '',
         PRIMARY KEY (locale)
       ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
 
@@ -50,6 +51,7 @@ function locale_install() {
         isdefault int NOT NULL default '0',
         plurals int NOT NULL default '0',
         formula varchar(128) NOT NULL default '',
+        javascript varchar(32) NOT NULL default '',
         PRIMARY KEY (locale)
       )");
 
@@ -81,7 +83,32 @@ function locale_install() {
  * Implementation of hook_uninstall().
  */
 function locale_uninstall() {
+  // Delete all JavaScript translation files
+  $files = db_query('SELECT javascript FROM {locales_meta}');
+  while ($file = db_fetch_object($files)) {
+    if (!empty($file)) {
+      file_delete(file_create_path($file->javascript));
+    }
+  }
+
   db_query('DROP TABLE {locales_meta}');
   db_query('DROP TABLE {locales_source}');
   db_query('DROP TABLE {locales_target}');
 }
+
+/**
+ * Adds a column to store the filename of the JavaScript translation file.
+ */
+function locale_update_1() {
+  $ret = array();
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+    $ret[] = update_sql("ALTER TABLE {locales_meta} ADD javascript varchar(32) NOT NULL default ''");
+      break;
+    case 'pgsql':
+      db_add_column($ret, 'locales_meta', 'javascript', 'varchar(32)', array('default' => "''", 'not null' => TRUE));
+      break;
+  }
+  return $ret;
+}
Index: modules/locale/locale.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v
retrieving revision 1.159
diff -u -d -F^\s*function -r1.159 locale.module
--- modules/locale/locale.module	11 Feb 2007 08:44:32 -0000	1.159
+++ modules/locale/locale.module	11 Feb 2007 15:12:47 -0000
@@ -114,6 +114,22 @@ function locale_menu() {
 }
 
 /**
+ * Implementation of hook_init().
+ */
+function locale_init() {
+  global $locale;
+  $locales = locale_supported_languages(FALSE, TRUE);
+  
+  if (!empty($locales['javascript'][$locale])) {
+    $path = $locales['javascript'][$locale];
+    if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE) {
+      $path = 'system/files/'. $path;
+    }
+    drupal_add_js($path, 'core');
+  }
+}
+
+/**
  * Implementation of hook_perm().
  */
 function locale_perm() {
@@ -244,13 +260,15 @@ function locale_supported_languages($res
   if (!isset($enabled)) {
     $enabled = $all = array();
     $all['name'] = $all['formula'] = $enabled['name'] = $enabled['formula'] = array();
-    $result = db_query('SELECT locale, name, formula, enabled FROM {locales_meta} ORDER BY isdefault DESC, enabled DESC, name ASC');
+    $result = db_query('SELECT locale, name, formula, enabled, javascript FROM {locales_meta} ORDER BY isdefault DESC, enabled DESC, name ASC');
     while ($row = db_fetch_object($result)) {
       $all['name'][$row->locale] = $row->name;
       $all['formula'][$row->locale] = $row->formula;
+      $all['javascript'][$row->locale] = $row->javascript;
       if ($row->enabled) {
         $enabled['name'][$row->locale] = $row->name;
         $enabled['formula'][$row->locale] = $row->formula;
+        $enabled['javascript'][$row->locale] = $row->javascript;
       }
     }
   }
