20 public const UNIT_MARKER_INVALID_CHARS =
"_/\n<>";
21 public const NEW_UNIT_ID =
'-1';
23 public const TVAR_OLD_SYNTAX_REGEX =
'~<tvar\|([^>]+)>(.*?)</>~us';
25 public const TVAR_NEW_SYNTAX_REGEX =
28 <tvar \s+ name \s* = \s*
29 ( (
' (?<key1> [^']* )
' ) | ( " (?<key2> [^"]* ) " ) | (?<key3> [^"'\s>]* ) )
30 \s* > (?<value>.*?) </tvar \s* >
37 public const TRANSLATIONLANGUAGE_REGEX =
'/{{\s*TRANSLATIONLANGUAGE\s*}}/';
46 public $oldText =
null;
51 protected $inline =
false;
53 private $canWrap =
true;
57 private static $properties = [
'version',
'id',
'text',
'type',
'oldText',
'inline' ];
59 public function __construct(
61 string $id = self::NEW_UNIT_ID,
63 string $oldText =
null
68 $this->oldText = $oldText;
71 public function setIsInline(
bool $value ):
void {
72 $this->
inline = $value;
75 public function isInline():
bool {
79 public function setCanWrap(
bool $value ):
void {
80 $this->canWrap = $value;
83 public function canWrap():
bool {
84 return $this->canWrap;
88 public function getText():
string {
93 public function getTextWithVariables():
string {
94 return $this->replaceVariablesWithNames( $this->text );
97 private function replaceVariablesWithNames(
string $text ):
string {
98 $variableReplacements = [];
99 foreach ( $this->loadVariables( $text ) as $variable ) {
100 $variableReplacements[$variable->getDefinition()] = $variable->getName();
103 return strtr( $text, $variableReplacements );
107 public function getTextForTrans():
string {
108 $variableReplacements = [];
109 foreach ( $this->getVariables() as $variable ) {
110 $variableReplacements[$variable->getDefinition()] = $variable->getValue();
113 return strtr( $this->text, $variableReplacements );
117 public function onlyTvarsChanged():
bool {
118 if ( $this->oldText ===
null ) {
122 $newText = $this->getTextWithVariables();
123 $oldText = $this->replaceVariablesWithNames( $this->oldText );
124 return $oldText === $newText;
128 public function getMarkedText():
string {
130 $header =
"<!--T:$id-->";
132 if ( $this->getHeading( $this->text ) !==
null ) {
133 $text = $this->text .
' ' . $header;
135 if ( $this->
inline ) {
136 $text = $header .
' ' . $this->text;
138 $text = $header .
"\n" . $this->text;
146 public function getOldText():
string {
147 return $this->oldText ?? $this->text;
151 public function getVariables(): array {
152 return $this->loadVariables( $this->text );
156 private function loadVariables(
string $text ): array {
160 preg_match_all( self::TVAR_OLD_SYNTAX_REGEX, $text, $matches, PREG_SET_ORDER );
161 foreach ( $matches as $m ) {
166 preg_match_all( self::TVAR_NEW_SYNTAX_REGEX, $text, $matches, PREG_SET_ORDER );
167 foreach ( $matches as $m ) {
171 '$' . ( $m[
'key1'] . $m[
'key2'] . $m[
'key3'] ),
180 public function serializeToArray(): array {
182 foreach ( self::$properties as $index => $property ) {
185 $data[$index] = $this->$property;
191 public static function unserializeFromArray( array $data ):
self {
193 $unit =
new self(
'' );
194 foreach ( self::$properties as $index => $property ) {
195 $unit->$property = $data[$index];
201 public function getTextForRendering(
203 Language $sourceLanguage,
204 Language $targetLanguage,
205 bool $wrapUntranslated,
206 ?Parser $parser =
null
213 $headingText = $this->getHeading( $msg->
definition() );
215 if ( $msg->
hasTag(
'fuzzy' ) ) {
217 $content = str_replace( TRANSLATE_FUZZY,
'', $content );
218 $attributes[
'class'] =
'mw-translate-fuzzy';
220 $translationLanguage = $targetLanguage->getCode();
222 $content = $this->getTextWithVariables();
223 if ( $wrapUntranslated ) {
224 $attributes[
'lang'] = $sourceLanguage->getHtmlCode();
225 $attributes[
'dir'] = $sourceLanguage->getDir();
226 $attributes[
'class'] =
'mw-content-' . $sourceLanguage->getDir();
228 $translationLanguage = $sourceLanguage->getCode();
231 if ( $this->canWrap() && $attributes ) {
232 $tag = $this->isInline() ?
'span' :
'div';
233 $content = $this->isInline() ? $content :
"\n$content\n";
234 $content = Html::rawElement( $tag, $attributes, $content );
237 $variableReplacements = [];
238 foreach ( $this->getVariables() as $variable ) {
239 $variableReplacements[$variable->getName()] = $variable->getValue();
244 $this->shouldAddAnchor(
252 $sectionName = substr( $parser->guessSectionNameFromWikiText( $headingText ), 1 );
253 $attributes = [
'id' => $sectionName ];
254 $content = Html::rawElement(
'span', $attributes,
'' ) .
"\n$content";
257 $content = strtr( $content, $variableReplacements );
260 $content = preg_replace(
261 self::TRANSLATIONLANGUAGE_REGEX,
262 $translationLanguage,
270 public function getIssues(): array {
271 $issues = $usedNames = [];
272 foreach ( $this->getVariables() as $variable ) {
273 $name = $variable->getName();
275 if ( !preg_match( $pattern, $name ) ) {
278 TranslationUnitIssue::WARNING,
279 'tpt-validation-not-insertable',
280 [ wfEscapeWikiText( $name ) ]
284 $usedNames[ $name ][] = $variable->getValue();
287 foreach ( $usedNames as $name => $contents ) {
288 $uniqueValueCount = count( array_unique( $contents ) );
289 if ( $uniqueValueCount > 1 ) {
291 TranslationUnitIssue::ERROR,
292 'tpt-validation-name-reuse',
293 [ wfEscapeWikiText( $name ) ]
298 return array_values( $issues );
302 private function getHeading(
string $text ): ?
string {
304 preg_match(
'/^(={1,6})[ \t]*(.+?)[ \t]*\1\s*$/', $text, $match );
305 return $match[2] ??
null;
308 private function shouldAddAnchor(
309 Language $sourceLanguage,
310 Language $targetLanguage,
311 ?
string $headingText,
316 if ( $headingText ===
null ) {
321 if ( $sourceLanguage->getCode() === $targetLanguage->getCode() ) {
332 if ( !$this->canWrap() ) {