diff --git l10n_community/ajax.inc l10n_community/ajax.inc
index 890e4ac..d14b82b 100644
--- l10n_community/ajax.inc
+++ l10n_community/ajax.inc
@@ -56,6 +56,9 @@ function l10n_community_string_details($langcode = NULL, $sid = 0) {
}
function l10n_community_string_suggestions($langcode = NULL, $sid = 0) {
+ // Look for an existing active translation, if any.
+ $existing_string = db_fetch_object(db_query("SELECT sid, tid, translation FROM {l10n_community_translation} WHERE sid = %d AND language = '%s' AND is_suggestion = 0 AND is_active = 1", $sid, $langcode));
+
// Existing, "unresolved" suggestions.
$suggestions = array();
$result = db_query("SELECT t.tid, t.sid, t.translation, t.uid_entered, t.time_entered, u.name FROM {l10n_community_translation} t LEFT JOIN {users} u ON u.uid = t.uid_entered WHERE t.language = '%s' AND t.sid = %d AND t.is_active = 1 AND t.is_suggestion = 1 ORDER BY t.time_entered", $langcode, $sid);
@@ -71,7 +74,27 @@ function l10n_community_string_suggestions($langcode = NULL, $sid = 0) {
// Since we no longer store an additional copy of strings in JS
// and the null byte is stripped out by browsers (?), we need to
// use a DOM-safe delimiter: "; "
- $translation = strpos($suggestion->translation, "\0") ? str_replace(chr(0), "; ", $suggestion->translation) : $suggestion->translation;
+ if (strpos($suggestion->translation, "\0")) {
+ $translation = str_replace(chr(0), "; ", $suggestion->translation);
+ }
+ else {
+ if (isset($existing_string->translation) && drupal_strlen($existing_string->translation) > 0) {
+ include_once 'diff.inc';
+
+ $diff = worddiff(
+ check_plain($existing_string->translation),
+ check_plain($suggestion->translation)
+ );
+
+ $translation = worddiff_render($diff);
+ $translation['original'] = $suggestion->translation;
+ }
+ else {
+ $existing_string = $suggestion;
+ $translation = $suggestion->translation;
+ }
+ }
+
$translation = l10n_community_format_text($translation, $suggestion->sid);
$suggestions[] = $translation ."
". $approve_button . $decline_button .'
'. t('by %user at %date', array('%user' => (empty($suggestion->name) ? variable_get('anonymous', t('Anonymous')) : $suggestion->name), '%date' => format_date($suggestion->time_entered))) .'
';
}
diff --git l10n_community/diff.inc l10n_community/diff.inc
new file mode 100755
index 0000000..9c7aa43
--- /dev/null
+++ l10n_community/diff.inc
@@ -0,0 +1,201 @@
+ $str) {
+ // Tokenize the string
+ $tokens[$key] = preg_split('/(?<=\W)|(?=\W)/u', trim($str));
+ }
+
+ // Remove identical prefix.
+ while ($tokens[0] && $tokens[1] && reset($tokens[0]) === reset($tokens[1])) {
+ $prefix .= array_shift($tokens[0]);
+ array_shift($tokens[1]);
+ }
+
+ // Remove identical suffixes.
+ while ($tokens[0] && $tokens[1] && end($tokens[0]) === end($tokens[1])) {
+ $suffix = array_pop($tokens[0]) . $suffix;
+ array_pop($tokens[1]);
+ }
+
+ // Calculate the length of the token arrays.
+ foreach (array_keys($tokens) as $key) {
+ $length[$key] = count($tokens[$key]);
+ }
+
+ return array($tokens, $length, $prefix, $suffix);
+}
+
+function _worddiff_lcs($tokens, $length) {
+ $matrix = array();
+
+ // Calculate the matrix.
+ for ($i = 0; $i < $length[0]; $i++) {
+ for ($j = 0; $j < $length[1]; $j++) {
+ if ($tokens[0][$i] === $tokens[1][$j]) {
+ $matrix[$i][$j] = (isset($matrix[$i - 1][$j - 1]) ? $matrix[$i - 1][$j - 1] : 0) + 1;
+ }
+ else {
+ $matrix[$i][$j] = max(
+ isset($matrix[$i][$j - 1]) ? $matrix[$i][$j - 1] : 0,
+ isset($matrix[$i - 1][$j]) ? $matrix[$i - 1][$j] : 0
+ );
+ }
+ }
+ }
+
+ return $matrix;
+}
+
+function _worddiff_expand_result($str) {
+ return array(-1 => '', 0 => $str, 1 => '');
+}
+
+function _worddiff_changeset($tokens, $matrix, $length) {
+ // Setup work.
+ $index = 0;
+ $mode = NULL;
+ $result = array_map('_worddiff_expand_result', $tokens[0]);
+
+ for ($i = $length[0] - 1, $j = $length[1] - 1; $i >= 0; $i--, $j--) {
+ if ($j < 0 || $tokens[0][$i] !== $tokens[1][$j]) {
+ if ($j < 0 || ($j > 0 && ($matrix[$i][$j - 1] < $matrix[$i - 1][$j]))) {
+ // Delete substring.
+ if ($mode === -1) {
+ // If we're already in delete mode, prepend it to the previous delete.
+ $result[$index][-1] = $result[$i][0] . $result[$index][-1];
+ unset($result[$i]);
+ }
+ else {
+ // Otherwise, move this token's string to the delete part.
+ $result[$i][-1] = $result[$i][0];
+ $result[$i][0] = '';
+ $index = $i;
+ $mode = -1;
+ }
+ $j++;
+ }
+ else {
+ // Add a new substring to the last token.
+ $result[$i][1] = $tokens[1][$j] . $result[$i][1];
+ $i++;
+ $mode = 1;
+ }
+ }
+ else {
+ // Preserve substring.
+ if ($mode === 0) {
+ // Merge this token into the previous one to minimize the amount of tokens.
+ $result[$index][0] = $result[$i][0] . $result[$index][0];
+ unset($result[$i]);
+ }
+ else {
+ $index = $i;
+ $mode = 0;
+ }
+ }
+ }
+
+ return array_values($result);
+}
+
+function _worddiff_find_prefix($a, $b) {
+ $prefix = '';
+
+ while (drupal_strlen($a) && drupal_strlen($b) && drupal_substr($a, 0, 1) === drupal_substr($b, 0, 1)) {
+ $prefix .= drupal_substr($a, 0, 1);
+ $a = drupal_substr($a, 1);
+ $b = drupal_substr($b, 1);
+ }
+
+ return array($prefix, $a, $b);
+}
+
+function _worddiff_aggregate($diff) {
+ for ($i = 0; isset($diff[$i]); $i++) {
+ if ($diff[$i][-1] == '' && drupal_strlen($diff[$i][0]) < 3 && $diff[$i][1] == '' &&
+ isset($diff[$i - 1]) && $diff[$i - 1][0] == '' &&
+ isset($diff[$i + 1]) && $diff[$i + 1][0] == '') {
+ $diff[$i + 1] = array(
+ -1 => $diff[$i - 1][-1] . $diff[$i][0] . $diff[$i + 1][-1],
+ 0 => '',
+ 1 => $diff[$i - 1][1] . $diff[$i][0] . $diff[$i + 1][1],
+ );
+
+ unset($diff[$i - 1], $diff[$i]);
+ }
+ }
+
+ // Find common prefixes.
+ $diff = array_values($diff);
+ for ($i = 0; isset($diff[$i]); $i++) {
+ if ($diff[$i][0] == '' && isset($diff[$i - 1]) && $diff[$i - 1][1] == '') {
+ list($prefix, $diff[$i][-1], $diff[$i][1]) = _worddiff_find_prefix($diff[$i][-1], $diff[$i][1]);
+ $diff[$i - 1][0] .= $prefix;
+ }
+ }
+
+ return $diff;
+}
+
+function worddiff($old, $new) {
+ // Preparation stuff.
+ list($tokens, $length, $prefix, $suffix) = _worddiff_tokenize(array($old, $new));
+ $matrix = _worddiff_lcs($tokens, $length);
+
+ // Generate the changeset.
+ $result = _worddiff_changeset($tokens, $matrix, $length);
+
+ // Add the previously trimmed prefix and suffix.
+ array_unshift($result, array(-1 => '', 0 => $prefix, 1 => ''));
+ array_push($result, array(-1 => '', 0 => $suffix, 1 => ''));
+
+ // Merge small changes together.
+ $result = _worddiff_aggregate($result);
+
+ return $result;
+}
+
+function _worddiff_render_deletions($changes) {
+ $value = '';
+
+ if (drupal_strlen($changes[-1])) {
+ $value .= ''. $changes[-1] .'';
+ }
+ if (drupal_strlen($changes[0])) {
+ $value .= $changes[0];
+ }
+
+ return $value;
+}
+
+function _worddiff_render_insertions($changes) {
+ $value = '';
+
+ if (drupal_strlen($changes[0])) {
+ $value .= $changes[0];
+ }
+ if (drupal_strlen($changes[1])) {
+ $value .= ''. $changes[1] .'';
+ }
+
+ return $value;
+}
+
+function worddiff_render($result) {
+ return array(
+ 'old' => implode('', array_map('_worddiff_render_deletions', $result)),
+ 'new' => implode('', array_map('_worddiff_render_insertions', $result))
+ );
+}
+
diff --git l10n_community/l10n_community.css l10n_community/l10n_community.css
index fadadbe..63b3400 100644
--- l10n_community/l10n_community.css
+++ l10n_community/l10n_community.css
@@ -61,6 +61,7 @@ div.admin form {
/* Emphasized replaceable parts of translatables */
em.l10n-community-marker {
+ font-style: normal;
font-weight: bold;
}
@@ -195,6 +196,24 @@ ul.l10n-community-strings {
margin: 0;
}
+ul.l10n-community-strings code.removed {
+ background: #FFD8D8;
+}
+
+ul.l10n-community-strings code.removed del {
+ background: #FF8888;
+ text-decoration: none;
+}
+
+ul.l10n-community-strings code.inserted {
+ background: #DDF8CC;
+}
+
+ul.l10n-community-strings code.inserted ins {
+ background: #99FF99;
+ text-decoration: none;
+}
+
ul.l10n-community-strings li {
padding-left: 20px;
}
diff --git l10n_community/l10n_community.module l10n_community/l10n_community.module
index 0dc671d..5425606 100644
--- l10n_community/l10n_community.module
+++ l10n_community/l10n_community.module
@@ -1329,6 +1329,11 @@ function theme_l10n_community_copy_button() {
function l10n_community_format_text($string, $sid = NULL, $delta = NULL) {
static $path = NULL, $title = NULL;
+ if (is_array($string)) {
+ $diff = $string;
+ $string = $string['original'];
+ }
+
if (!isset($path)) {
$path = base_path() . drupal_get_path('module', 'l10n_community');
$title = t('line break');
@@ -1336,12 +1341,20 @@ function l10n_community_format_text($string, $sid = NULL, $delta = NULL) {
$original = check_plain($string);
+ if (isset($diff)) {
+ $string = '' . $diff['old'] . '
' . $diff['new'] . '';
+ }
+ else {
+ $string = $original;
+ }
+
// Replace all newline chars in the string with an indicator image.
$string = str_replace(
array("\n", "\\\\n"),
'
',
- check_plain($string)
+ $string
);
+
// Make all %, ! and @ marked pladeholders emphasized.
$string = preg_replace(
'~((%|!|@)[0-9a-zA-Z_-]+)~',