summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php')
-rw-r--r--www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php766
1 files changed, 766 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php b/www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php
new file mode 100644
index 00000000..fcd8a314
--- /dev/null
+++ b/www/wiki/extensions/Translate/ffs/MediaWikiComplexMessages.php
@@ -0,0 +1,766 @@
+<?php
+/**
+ * Classes for complex messages (%MediaWiki special page aliases, namespace names, magic words).
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2008-2010, Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Base class which implements handling and translation interface of
+ * non-message %MediaWiki items.
+ * @todo Needs documentation.
+ */
+abstract class ComplexMessages {
+ const LANG_MASTER = 0;
+ const LANG_CHAIN = 1;
+ const LANG_CURRENT = 2;
+ const PLACEHOLDER = 'languagecodeplaceholder';
+
+ protected $language;
+ protected $targetHtmlCode;
+ protected $targetDir;
+ protected $id = '__BUG__';
+ protected $variable = '__BUG__';
+ protected $data = [];
+ protected $elementsInArray = true;
+ protected $databaseMsg = '__BUG__';
+ protected $chainable = false;
+ protected $firstMagic = false;
+ protected $constants = [];
+
+ protected $tableAttributes = [
+ 'class' => 'wikitable',
+ 'border' => '2',
+ 'cellpadding' => '4',
+ 'cellspacing' => '0',
+ 'style' => 'background-color: #F9F9F9; border: 1px #AAAAAA solid; border-collapse: collapse;',
+ ];
+
+ public function __construct( $langCode ) {
+ $this->language = $langCode;
+
+ $language = Language::factory( $langCode );
+ $this->targetHtmlCode = $language->getHtmlCode();
+ $this->targetDir = $language->getDir();
+ }
+
+ public function getTitle() {
+ // Give grep a chance to find the usages:
+ // translate-magic-special, translate-magic-words, translate-magic-namespace
+ return wfMessage( 'translate-magic-' . $this->id )->text();
+ }
+
+ // Data retrieval
+ protected $init = false;
+
+ public function getGroups() {
+ if ( !$this->init ) {
+ $saved = $this->getSavedData();
+ foreach ( $this->data as &$group ) {
+ $this->getData( $group, $saved );
+ }
+ $this->init = true;
+ }
+
+ return $this->data;
+ }
+
+ public function cleanData( $defs, $current ) {
+ foreach ( $current as $item => $values ) {
+ if ( !$this->elementsInArray ) {
+ break;
+ }
+
+ if ( !isset( $defs[$item] ) ) {
+ unset( $current[$item] );
+ continue;
+ }
+
+ foreach ( $values as $index => $value ) {
+ if ( in_array( $value, $defs[$item], true ) ) {
+ unset( $current[$item][$index] );
+ }
+ }
+ }
+
+ return $current;
+ }
+
+ public function mergeMagic( $defs, $current ) {
+ foreach ( $current as $item => &$values ) {
+ $newchain = $defs[$item];
+ array_splice( $newchain, 1, 0, $values );
+ $values = $newchain;
+ }
+
+ return $current;
+ }
+
+ public function getData( &$group, $savedData ) {
+ $defs = $this->readVariable( $group, 'en' );
+ $code = $this->language;
+
+ $current = $savedData + $this->readVariable( $group, $code );
+
+ // Clean up duplicates to definitions from saved data
+ $current = $this->cleanData( $defs, $current );
+
+ $chain = $current;
+ if ( $this->chainable ) {
+ foreach ( Language::getFallbacksFor( $code ) as $code ) {
+ $fbdata = $this->readVariable( $group, $code );
+ if ( $this->firstMagic ) {
+ $fbdata = $this->cleanData( $defs, $fbdata );
+ }
+
+ $chain = array_merge_recursive( $chain, $fbdata );
+ }
+ }
+
+ if ( $this->firstMagic ) {
+ $chain = $this->mergeMagic( $defs, $chain );
+ }
+
+ $data = $group['data'] = [ $defs, $chain, $current ];
+
+ return $data;
+ }
+
+ /**
+ * Gets data from request. Needs to be run before the form is displayed and
+ * validation. Not needed for export, which uses request directly.
+ * @param WebRequest $request
+ */
+ public function loadFromRequest( WebRequest $request ) {
+ $saved = $this->parse( $this->formatForSave( $request ) );
+ foreach ( $this->data as &$group ) {
+ $this->getData( $group, $saved );
+ }
+ }
+
+ /**
+ * Gets saved data from Mediawiki namespace
+ * @return Array
+ */
+ protected function getSavedData() {
+ $data = TranslateUtils::getMessageContent( $this->databaseMsg, $this->language );
+
+ if ( !$data ) {
+ return [];
+ } else {
+ return $this->parse( $data );
+ }
+ }
+
+ protected function parse( $data ) {
+ $lines = array_map( 'trim', explode( "\n", $data ) );
+ $array = [];
+ foreach ( $lines as $line ) {
+ if ( $line === '' || $line[0] === '#' || $line[0] === '<' ) {
+ continue;
+ }
+
+ if ( strpos( $line, '=' ) === false ) {
+ continue;
+ }
+
+ list( $name, $values ) = array_map( 'trim', explode( '=', $line, 2 ) );
+ if ( $name === '' || $values === '' ) {
+ continue;
+ }
+
+ $data = array_map( 'trim', explode( ',', $values ) );
+ $array[$name] = $data;
+ }
+
+ return $array;
+ }
+
+ /**
+ * Return an array of keys that can be used to iterate over all keys
+ * @param string $group
+ * @return Array of keys for data
+ */
+ protected function getIterator( $group ) {
+ $groups = $this->getGroups();
+
+ return array_keys( $groups[$group]['data'][self::LANG_MASTER] );
+ }
+
+ protected function val( $group, $type, $key ) {
+ $array = $this->getGroups();
+ Wikimedia\suppressWarnings();
+ $subarray = $array[$group]['data'][$type][$key];
+ Wikimedia\restoreWarnings();
+ if ( $this->elementsInArray ) {
+ if ( !$subarray || !count( $subarray ) ) {
+ return [];
+ }
+ } else {
+ if ( !$subarray ) {
+ return [];
+ }
+ }
+
+ if ( !is_array( $subarray ) ) {
+ $subarray = [ $subarray ];
+ }
+
+ return $subarray;
+ }
+
+ /**
+ * @param string $group
+ * @param string $code
+ * @return array
+ */
+ protected function readVariable( $group, $code ) {
+ $file = $group['file'];
+ if ( !$group['code'] ) {
+ $file = str_ireplace( self::PLACEHOLDER, str_replace( '-', '_', ucfirst( $code ) ), $file );
+ }
+
+ ${$group['var']} = []; # Initialize
+ if ( file_exists( $file ) ) {
+ require $file; # Include
+ }
+
+ if ( $group['code'] ) {
+ Wikimedia\suppressWarnings();
+ $data = (array)${$group['var']} [$code];
+ Wikimedia\restoreWarnings();
+ } else {
+ $data = ${$group['var']};
+ }
+
+ return self::arrayMapRecursive( 'strval', $data );
+ }
+
+ public static function arrayMapRecursive( $callback, $data ) {
+ foreach ( $data as $index => $values ) {
+ if ( is_array( $values ) ) {
+ $data[$index] = self::arrayMapRecursive( $callback, $values );
+ } else {
+ $data[$index] = call_user_func( $callback, $values );
+ }
+ }
+
+ return $data;
+ }
+
+ // Data retrieval
+
+ // Output
+ public function header( $title ) {
+ $colspan = [ 'colspan' => 3 ];
+ $header = Xml::element( 'th', $colspan, $this->getTitle() . ' - ' . $title );
+ $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-original' )->escaped() . '</th>';
+ $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-current' )->escaped() . '</th>';
+ $subheading[] = '<th>' . wfMessage( 'translate-magic-cm-to-be' )->escaped() . '</th>';
+
+ return '<tr>' . $header . '</tr>' .
+ '<tr>' . implode( "\n", $subheading ) . '</tr>';
+ }
+
+ public function output() {
+ $colspan = [ 'colspan' => 3 ];
+
+ $s = Xml::openElement( 'table', $this->tableAttributes );
+
+ foreach ( array_keys( $this->data ) as $group ) {
+ $s .= $this->header( $this->data[$group]['label'] );
+
+ foreach ( $this->getIterator( $group ) as $key ) {
+ $rowContents = '';
+
+ $value = $this->val( $group, self::LANG_MASTER, $key );
+ if ( $this->firstMagic ) {
+ array_shift( $value );
+ }
+
+ $value = array_map( 'htmlspecialchars', $value );
+ // Force ltr direction. The source is pretty much guaranteed to be English-based.
+ $rowContents .= '<td dir="ltr">' . $this->formatElement( $value ) . '</td>';
+
+ $value = $this->val( $group, self::LANG_CHAIN, $key );
+ if ( $this->firstMagic ) {
+ array_shift( $value );
+ }
+
+ // Apply bidi-isolation to each value.
+ // The values can both RTL and LTR and mixing them in a comma list
+ // can mix things up.
+ foreach ( $value as &$currentTranslation ) {
+ $currentTranslation = Xml::element( 'bdi', null, $currentTranslation );
+ }
+ $value = $this->highlight( $key, $value );
+ $rowContents .= '<td>' . $this->formatElement( $value ) . '</td>';
+
+ $value = $this->val( $group, self::LANG_CURRENT, $key );
+ $rowContents .= '<td>';
+ $rowContents .= $this->editElement( $key, $this->formatElement( $value ) );
+ $rowContents .= '</td>';
+
+ $s .= Xml::tags( 'tr', [ 'id' => "mw-sp-magic-$key" ], $rowContents );
+ }
+ }
+
+ $context = RequestContext::getMain();
+
+ if ( $context->getUser()->isAllowed( 'translate' ) ) {
+ $s .= '<tr>' . Xml::tags( 'td', $colspan, $this->getButtons() ) . '<tr>';
+ }
+
+ $s .= Xml::closeElement( 'table' );
+
+ return Xml::tags(
+ 'form',
+ [
+ 'method' => 'post',
+ 'action' => $context->getRequest()->getRequestURL()
+ ],
+ $s
+ );
+ }
+
+ public function getButtons() {
+ return Xml::inputLabel(
+ wfMessage( 'translate-magic-cm-comment' )->text(),
+ 'comment',
+ 'sp-translate-magic-comment'
+ ) .
+ Xml::submitButton(
+ wfMessage( 'translate-magic-cm-save' )->text(),
+ [ 'name' => 'savetodb' ]
+ );
+ }
+
+ public function formatElement( $element ) {
+ if ( !count( $element ) ) {
+ return '';
+ }
+
+ if ( is_array( $element ) ) {
+ $element = array_map( 'trim', $element );
+ $element = implode( ', ', $element );
+ }
+
+ return trim( $element );
+ }
+
+ protected function getKeyForEdit( $key ) {
+ return Sanitizer::escapeId( 'sp-translate-magic-cm-' . $this->id . $key );
+ }
+
+ public function editElement( $key, $contents ) {
+ return Xml::input( $this->getKeyForEdit( $key ), 40, $contents, [
+ 'lang' => $this->targetHtmlCode,
+ 'dir' => $this->targetDir,
+ ] );
+ }
+
+ // Output
+
+ // Save to database
+
+ protected function getKeyForSave() {
+ return $this->databaseMsg . '/' . $this->language;
+ }
+
+ /**
+ * @param WebRequest $request
+ * @return string
+ */
+ protected function formatForSave( WebRequest $request ) {
+ $text = '';
+
+ // Do not replace spaces by underscores for magic words. See bug T48613
+ $replaceSpace = $request->getVal( 'module' ) !== 'magic';
+
+ foreach ( array_keys( $this->data ) as $group ) {
+ foreach ( $this->getIterator( $group ) as $key ) {
+ $data = $request->getText( $this->getKeyForEdit( $key ) );
+ // Make a nice array out of the submit with trimmed values.
+ $data = array_map( 'trim', explode( ',', $data ) );
+
+ if ( $replaceSpace ) {
+ // Normalise: Replace spaces with underscores.
+ $data = str_replace( ' ', '_', $data );
+ }
+
+ // Create final format.
+ $data = implode( ', ', $data );
+ if ( $data !== '' ) {
+ $text .= "$key = $data\n";
+ }
+ }
+ }
+
+ return $text;
+ }
+
+ /**
+ * @param WebRequest $request
+ * @throws MWException
+ */
+ public function save( $request ) {
+ $title = Title::newFromText( 'MediaWiki:' . $this->getKeyForSave() );
+ $page = WikiPage::factory( $title );
+
+ $data = "# DO NOT EDIT THIS PAGE DIRECTLY! Use [[Special:AdvancedTranslate]].\n<pre>\n" .
+ $this->formatForSave( $request ) . "\n</pre>";
+
+ $comment = $request->getText(
+ 'comment',
+ wfMessage( 'translate-magic-cm-updatedusing' )->inContentLanguage()->text()
+ );
+
+ $content = ContentHandler::makeContent( $data, $title );
+ $status = $page->doEditContent( $content, $comment );
+
+ if ( $status === false || ( is_object( $status ) && !$status->isOK() ) ) {
+ throw new MWException( wfMessage( 'translate-magic-cm-savefailed' )->text() );
+ }
+
+ /* Reset outdated array */
+ $this->init = false;
+ }
+
+ // Save to database
+
+ // Export
+ public function validate( array &$errors, $filter = false ) {
+ $used = [];
+ foreach ( array_keys( $this->data ) as $group ) {
+ if ( $filter !== false && !in_array( $group, (array)$filter, true ) ) {
+ continue;
+ }
+
+ $this->validateEach( $errors, $group, $used );
+ }
+ }
+
+ protected function validateEach( array &$errors, $group, &$used ) {
+ foreach ( $this->getIterator( $group ) as $key ) {
+ $values = $this->val( $group, self::LANG_CURRENT, $key );
+ $link = Xml::element( 'a', [ 'href' => "#mw-sp-magic-$key" ], $key );
+
+ if ( count( $values ) !== count( array_filter( $values ) ) ) {
+ $errors[] = "There is empty value in $link.";
+ }
+
+ foreach ( $values as $v ) {
+ if ( isset( $used[$v] ) ) {
+ $otherkey = $used[$v];
+ $first = Xml::element(
+ 'a',
+ [ 'href' => "#mw-sp-magic-$otherkey" ],
+ $otherkey
+ );
+ $errors[] = "Translation <strong>$v</strong> is used more than once " .
+ "for $first and $link.";
+ } else {
+ $used[$v] = $key;
+ }
+ }
+ }
+ }
+
+ public function export( $filter = false ) {
+ $text = '';
+ $errors = [];
+ $this->validate( $errors, $filter );
+ foreach ( $errors as $_ ) {
+ $text .= "#!!# $_\n";
+ }
+
+ foreach ( $this->getGroups() as $group => $data ) {
+ if ( $filter !== false && !in_array( $group, (array)$filter, true ) ) {
+ continue;
+ }
+
+ $text .= $this->exportEach( $group, $data );
+ }
+
+ return $text;
+ }
+
+ protected function exportEach( $group, $data ) {
+ $var = $data['var'];
+ $items = $data['data'];
+
+ $extra = $data['code'] ? "['{$this->language}']" : '';
+
+ $out = '';
+
+ $indexKeys = [];
+ foreach ( array_keys( $items[self::LANG_MASTER] ) as $key ) {
+ $indexKeys[$key] = $this->constants[$key] ?? "'$key'";
+ }
+
+ $padTo = max( array_map( 'strlen', $indexKeys ) ) + 3;
+
+ foreach ( $this->getIterator( $group ) as $key ) {
+ $temp = "\t{$indexKeys[$key]}";
+
+ while ( strlen( $temp ) <= $padTo ) {
+ $temp .= ' ';
+ }
+
+ $from = self::LANG_CURRENT;
+ // Abuse of the firstMagic property, should use something proper
+ if ( $this->firstMagic ) {
+ $from = self::LANG_CHAIN;
+ }
+
+ // Check for translations
+ $val = $this->val( $group, self::LANG_CURRENT, $key );
+ if ( !$val || !count( $val ) ) {
+ continue;
+ }
+
+ // Then get the data we really want
+ $val = $this->val( $group, $from, $key );
+
+ // Remove duplicated entries, causes problems with magic words
+ // Just to be sure, it should not be possible to save invalid data anymore
+ $val = array_unique( $val /* @todo SORT_REGULAR */ );
+
+ // So do empty elements...
+ foreach ( $val as $k => $v ) {
+ if ( $v === '' ) {
+ unset( $val[$k] );
+ }
+ }
+
+ // Another check
+ if ( !count( $val ) ) {
+ continue;
+ }
+
+ $normalized = array_map( [ $this, 'normalize' ], $val );
+ if ( $this->elementsInArray ) {
+ $temp .= '=> array( ' . implode( ', ', $normalized ) . ' ),';
+ } else {
+ $temp .= '=> ' . implode( ', ', $normalized ) . ',';
+ }
+ $out .= $temp . "\n";
+ }
+
+ if ( $out !== '' ) {
+ $text = "# {$data['label']} \n";
+ $text .= "\$$var$extra = array(\n" . $out . ");\n\n";
+
+ return $text;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Returns string with quotes that should be valid php
+ * @param string $data
+ * @throws MWException
+ * @return string
+ */
+ protected function normalize( $data ) {
+ # Escape quotes
+ if ( !is_string( $data ) ) {
+ throw new MWException();
+ }
+ $data = preg_replace( "/(?<!\\\\)'/", "\'", trim( $data ) );
+
+ return "'$data'";
+ }
+
+ // Export
+ public function highlight( $key, $values ) {
+ return $values;
+ }
+}
+
+/**
+ * Adds support for translating special page aliases via Special:AdvancedTranslate.
+ * @todo Needs documentation.
+ */
+class SpecialPageAliasesCM extends ComplexMessages {
+ protected $id = SpecialMagic::MODULE_SPECIAL;
+ protected $databaseMsg = 'sp-translate-data-SpecialPageAliases';
+ protected $chainable = true;
+
+ public function __construct( $code ) {
+ parent::__construct( $code );
+ $this->data['core'] = [
+ 'label' => 'MediaWiki Core',
+ 'var' => 'specialPageAliases',
+ 'file' => Language::getMessagesFileName( self::PLACEHOLDER ),
+ 'code' => false,
+ ];
+
+ $groups = MessageGroups::singleton()->getGroups();
+ foreach ( $groups as $g ) {
+ if ( !$g instanceof MediaWikiExtensionMessageGroup ) {
+ continue;
+ }
+ $conf = $g->getConfiguration();
+ if ( !isset( $conf['FILES']['aliasFileSource'] ) ) {
+ continue;
+ }
+ $file = $g->replaceVariables( $conf['FILES']['aliasFileSource'], 'en' );
+ if ( file_exists( $file ) ) {
+ $this->data[$g->getId()] = [
+ 'label' => $g->getLabel(),
+ 'var' => 'specialPageAliases',
+ 'file' => $file,
+ 'code' => $code,
+ ];
+ }
+ }
+ }
+
+ public function highlight( $key, $values ) {
+ if ( count( $values ) ) {
+ if ( !isset( $values[0] ) ) {
+ throw new MWException( 'Something missing from values: ' .
+ print_r( $values, true ) );
+ }
+
+ $values[0] = "<strong>$values[0]</strong>";
+ }
+
+ return $values;
+ }
+
+ protected function validateEach( array &$errors, $group, &$used ) {
+ parent::validateEach( $errors, $group, $used );
+ foreach ( $this->getIterator( $group ) as $key ) {
+ $values = $this->val( $group, self::LANG_CURRENT, $key );
+
+ foreach ( $values as $_ ) {
+ Wikimedia\suppressWarnings();
+ $title = SpecialPage::getTitleFor( $_ );
+ Wikimedia\restoreWarnings();
+ $link = Xml::element( 'a', [ 'href' => "#mw-sp-magic-$key" ], $key );
+ if ( $title === null ) {
+ if ( $_ !== '' ) {
+ // Empty values checked elsewhere
+ $errors[] = "Translation <strong>$_</strong> is invalid title in $link.";
+ }
+ } else {
+ $text = $title->getText();
+ $dbkey = $title->getDBkey();
+ if ( $text !== $_ && $dbkey !== $_ ) {
+ $errors[] = "Translation <strong>$_</strong> for $link is not in " .
+ "normalised form, which is <strong>$text</strong>";
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Adds support for translating magic words via Special:AdvancedTranslate.
+ * @todo Needs documentation.
+ */
+class MagicWordsCM extends ComplexMessages {
+ protected $id = SpecialMagic::MODULE_MAGIC;
+ protected $firstMagic = true;
+ protected $chainable = true;
+ protected $databaseMsg = 'sp-translate-data-MagicWords';
+
+ public function __construct( $code ) {
+ parent::__construct( $code );
+ $this->data['core'] = [
+ 'label' => 'MediaWiki Core',
+ 'var' => 'magicWords',
+ 'file' => Language::getMessagesFileName( self::PLACEHOLDER ),
+ 'code' => false,
+ ];
+
+ $groups = MessageGroups::singleton()->getGroups();
+ foreach ( $groups as $g ) {
+ if ( !$g instanceof MediaWikiExtensionMessageGroup ) {
+ continue;
+ }
+ $conf = $g->getConfiguration();
+ if ( !isset( $conf['FILES']['magicFileSource'] ) ) {
+ continue;
+ }
+ $file = $g->replaceVariables( $conf['FILES']['magicFileSource'], 'en' );
+ if ( file_exists( $file ) ) {
+ $this->data[$g->getId()] = [
+ 'label' => $g->getLabel(),
+ 'var' => 'magicWords',
+ 'file' => $file,
+ 'code' => $code,
+ ];
+ }
+ }
+ }
+
+ public function highlight( $key, $values ) {
+ if ( count( $values ) && $key === 'redirect' ) {
+ $values[0] = "<strong>$values[0]</strong>";
+ }
+
+ return $values;
+ }
+}
+
+/**
+ * Adds support for translating namespace names via Special:AdvancedTranslate.
+ * @todo Needs documentation.
+ */
+class NamespaceCM extends ComplexMessages {
+ protected $id = SpecialMagic::MODULE_NAMESPACE;
+ protected $elementsInArray = false;
+ protected $databaseMsg = 'sp-translate-data-Namespaces';
+
+ public function __construct( $code ) {
+ parent::__construct( $code );
+ $this->data['core'] = [
+ 'label' => 'MediaWiki Core',
+ 'var' => 'namespaceNames',
+ 'file' => Language::getMessagesFileName( self::PLACEHOLDER ),
+ 'code' => false,
+ ];
+ }
+
+ protected $constants = [
+ -2 => 'NS_MEDIA',
+ -1 => 'NS_SPECIAL',
+ 0 => 'NS_MAIN',
+ 1 => 'NS_TALK',
+ 2 => 'NS_USER',
+ 3 => 'NS_USER_TALK',
+ 4 => 'NS_PROJECT',
+ 5 => 'NS_PROJECT_TALK',
+ 6 => 'NS_FILE',
+ 7 => 'NS_FILE_TALK',
+ 8 => 'NS_MEDIAWIKI',
+ 9 => 'NS_MEDIAWIKI_TALK',
+ 10 => 'NS_TEMPLATE',
+ 11 => 'NS_TEMPLATE_TALK',
+ 12 => 'NS_HELP',
+ 13 => 'NS_HELP_TALK',
+ 14 => 'NS_CATEGORY',
+ 15 => 'NS_CATEGORY_TALK',
+ ];
+
+ protected function validateEach( array &$errors, $group, &$used ) {
+ parent::validateEach( $errors, $group, $used );
+ foreach ( $this->getIterator( $group ) as $key ) {
+ $values = $this->val( $group, self::LANG_CURRENT, $key );
+
+ if ( count( $values ) > 1 ) {
+ $link = Xml::element( 'a', [ 'href' => "#mw-sp-magic-$key" ], $key );
+ $errors[] = "Namespace $link can have only one translation. Replace the " .
+ 'translation with a new one, and notify staff about the change.';
+ }
+ }
+ }
+}