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"), ''. $title .'
', - check_plain($string) + $string ); + // Make all %, ! and @ marked pladeholders emphasized. $string = preg_replace( '~((%|!|@)[0-9a-zA-Z_-]+)~',